一.线程基础
1. 基础概念
1.1 进程和线程
概念:
进程:运行中的程序。指操作系统分配资源(比如内存等)的最小单位。
线程:程序执行的最小单位。或者叫CPU调度的最小单位。
区别:
- 进程就是线程的容器,线程需要利用进程中的一些资源,处理代码,指令,最终实现进程锁预期的结果。
- 同一个进程下的线程会共享进程中的资源。同时线程也拥有自身的独立存储空间(CPU)。而进程之间都是相互独立的。
- 一个进程中至少有一个线程。进程就是一个进程。
- 开销方面:线程的创建和终止的时间比较短。线程之间的切换速度比进程之间的切换速度快很多。
- 进程之间通讯麻烦很多,需要借助操作系统的内核才可以实现。而线程之间通讯则方便很多,比如借助exchange等方法。
1.2 多线程
多线程是指一个进程中同时运行多个线程。
多线程目的:是为了提高CPU的利用率。可以通过避免一些网络IO或者磁盘IO的等待时间,让CPU去调用其他线程。可以大幅度提高程序的效率。
多线程的局限:
- 如果线程特别多,那么CPU切换线程上下文时,会额外造成很大的消耗。
- 任务的拆分需要依赖业务的场景。有一些异构化的任务,很难对任务拆分(比如对库存进行扣除操作)。
- 线程安全问题:虽然多线程带来了一定的性能提升,但是做一些操作时,比如线程操作临界资源(共享资源),可能会发生一些数据不一致的安全问题,如果涉及到锁操作,甚至可能发生死锁问题。
1.3 串行、并行、并发
串行:类似队列,以串行化的方式进行操作,只有前一个处理完,后一个才能开始处理。
并行:并行就是同时处理。多核CPU同时调度多个线程,是真正多个线程同时运行。单核CPU是并发,无法做到并行。
并发:(这里的并发并不是三高中的高并发,而是多线程中的并发概念,也就是CPU调度线程的概念)指的是CPU在极短的时间内,反复切换执行不同的线程。看似是并行,实际上却是CPU极速切换,速度太快。
1.4 同步异步、阻塞非阻塞
同步异步:(指的是消息)执行某个功能后,被调用者是否会主动反馈信息。主动通知就是异步,否则就是同步。
- 同步:一次调用,一次返回,整个过程就是同步调用的过程。
- 异步:一次调用,没有返回或者不关心返回,而是通过后面的回调或者状态消息通知获取结果。
阻塞和非阻塞:(指的是进程/任务)执行某个功能后,调用者是否需要一直等待结果的反馈。
- 阻塞:调用方线程在等待结果返回过程中,线程被挂起,(调用方不能处理其它事情)等结果返回后,唤醒线程;
- 非阻塞:调用方线程在等待结果的过程中,线程没有被挂起,(可以处理其它事情)。
两个概念的侧重点不同。
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。
同步阻塞:烧水时,水不会通知你,你需要时不时去看查看(同步)。烧水过程中,你需要一直在旁边,不能干其他事(阻塞)。
同步非阻塞:烧水时,水不会通知你,你需要时不时去看查看(同步)。烧水过程中,你不需要一直在旁边,可以去做其他事(非阻塞)。
异步阻塞:水烧开时,水会通知你,你不需要去看查看(异步)。烧水过程中,你需要一直在旁边,不能干其他事(阻塞)。这种情况很少。
异步非阻塞:水烧开时,水会通知你,你不需要去看查看(异步)。烧水过程中,你不需要一直在旁边,可以去做其他事(非阻塞)。
异步非阻塞的效率是最高的。
2. 线程的创建
2.1 继承Thread类
package com.xqm.juc.threadBasic;
public class Test01 {
public static void main(String[] args) {
MyJob myJob=new MyJob();
myJob.start();
}
}
class MyJob extends Thread{
@Override
public void run() {
System.out.println("继承Thread");
}
}
由于一个类只能继承一个类,因此这种方式很少用。
2.2 实现Runnable方法
package com.xqm.juc.threadBasic;
public class Test02 {
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
// 作为Thread的有参构造传进去
Thread thread=new Thread(myRunnable);
thread.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable");
}
}
实现了Runnable方法后,需要new一个Thread,然后将Runnable作为参数传进去。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
在工作中,最常用的方式:
- 匿名内部类
- lambda表达式
匿名内部类:
package com.xqm.juc.threadBasic;
public class Test03 {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是匿名内部类");
}
});
thread.start();
}
}
lambda:
package com.xqm.juc.threadBasic;
public class Test04 {
public static void main(String[] args) {
Thread thread=new Thread(()->{
System.out.println("这是lambda表达式");
});
thread.start();
}
}
2.3 实现Callable方法,重写call方法,配合FutureTask
由于Thread和Runnable线程方式是没有返回值的,因此如果需要返回值,就需要实现Callable方法。
实现Callable方法是同步非阻塞的,需要自己去查询结果。
package com.xqm.juc.threadBasic;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test05 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable=new MyCallable();
// 创建FutureTask,传入Callable
FutureTask futureTask=new FutureTask(myCallable);
Thread thread=new Thread(futureTask);
thread.start();
// 获取结果
Object o = futureTask.get();
System.out.println(o);
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
System.out.println("实现callable方法");
int count=0;
for (int i = 0; i < 100; i++) {
count+=i;
}
return count;
}
}
2.4 直接通过线程池创建线程
JDK自带线程池、druid线程池等等。
3. 线程的使用
3.1 线程的状态
线程状态有5种、6种、7种,看如何划分。
3.1.1 五种状态
5种状态一般是针对传统的线程状态来说
3.1.2 六种状态
6种状态是为java中线程准备的,在Thead类中有state枚举。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:Thread线程被创建出来,但是还没执行start方法。
- RUNNABLE:Thread线程调用start方法,线程状态就转变成RUNNABLE,不管CPU是否调度。
- BLOCKED:synchronized没有拿到同步锁,被阻塞状态。
- WAITING:调用wait方法就会处于这个状态,需要被手动唤醒。
- TIMED_WAITING:调用sleep或者有时间的等待,线程到时间会自动唤醒。
- TERMINATED:线程执行完毕或者被异常终止,线程结束状态。
java代码验证:
NEW:
package com.xqm.juc.threadBasic;
public class Test06 {
public static void main(String[] args) {
Thread thread=new Thread();
// NEW
System.out.println(thread.getState());
}
}
RUNNABLE:
package com.xqm.juc.threadBasic;
public class Test07 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (true){
}
});
thread.start();
Thread.sleep(500);
// RUNNABLE
System.out.println(thread.getState());
}
}
BLOCKED:
package com.xqm.juc.threadBasic;
public class Test08 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread thread = new Thread(() -> {
// 线程拿不到锁,处于Blocked状态
synchronized (o) {
}
});
synchronized (o) {
thread.start();
Thread.sleep(500);
// BLOCKED
System.out.println(thread.getState());
}
}
}
WAITING:
package com.xqm.juc.threadBasic;
public class Test09 {
public static void main(String[] args) throws InterruptedException {
Object o=new Object();
Thread thread = new Thread(() -> {
synchronized (o){
try {
//需要被手动唤醒
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(500);
// WAITING
System.out.println(thread.getState());
}
}
TIMED_WAITING:
package com.xqm.juc.threadBasic;
import java.util.concurrent.TimeUnit;
public class Test10 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
Thread.sleep(100);
// TIMED_WAITING
System.out.println(thread.getState());
}
}
TERMINATED:
package com.xqm.juc.threadBasic;
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("线程结束");
});
thread.start();
Thread.sleep(100);
// TERMINATED
System.out.println(thread.getState());
}
}
3.1.3 七种状态
七种状态:
3.2 线程常用方法
3.2.1 获取当前线程
有一个currentThread()的静态方法。
package com.xqm.juc.threadBasic;
public class Test12 {
public static void main(String[] args) {
// 获取到当前main线程的对象
Thread thread = Thread.currentThread();
// Thread[main,5,main] - 名字-优先级-组名字
System.out.println(thread);
// 获取线程名字 main
System.out.println(thread.getName());
}
}
3.2.2 线程的名字
通过setName或者在创建参数中设置线程的名字。如果没有设置,就会自动生成Thread0,Thread1…
在线程运行时设置有意义的名称,方便后期排查错误。
package com.xqm.juc.threadBasic;
public class Test13 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// Thread-0 如果没有指定名称
System.out.println(Thread.currentThread().getName());
});
thread.start();
Thread thread1 = new Thread(() -> {
// 指定名称 Thread-模块功能
System.out.println(Thread.currentThread().getName());
},"Thread-模块功能");
// 或者通过setName
// thread1.setName("指定名称");
thread1.start();
}
}
3.2.3 线程优先级
其实就是CPU调度的优先级,默认都是5。最大是10,最小是1。
设置优先级有时可能不是那么有效的,取决于电脑的配置。如果电脑配置很好,实际用处不是很大,即便一个设置了10,一个设置了1,也可能同时会运行完,或者设置了1的线程会先运行完。电脑配置越差,优先级作用体现越明显。
package com.xqm.juc.threadBasic;
public class Test14 {
public static void main(String[] args) {
Thread thread=new Thread(()->{
System.out.println("我是Thread");
});
Thread thread1=new Thread(()->{
System.out.println("我是Thread1");
});
thread.setPriority(10);
thread1.setPriority(1);
thread1.start();
thread.start();
}
}
3.2.4 线程的让步
可以通过Thread的静态方法yeild,将线程从运行态转变为就绪态,让出CPU的时间片。但是可能在让出之后,还是会立即获得CPU的时间片,重新变为运行态。
package com.xqm.juc.threadBasic;
public class Test15 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if (i == 50) {
Thread.yield();
}
System.out.println("thread=" + i);
}
});
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("thread1=" + i);
}
});
thread.start();
thread1.start();
}
}
3.2.5 线程的休眠
sleep有两个方法重载。
- 第一个是native修饰的,让线程转为等待状态的方法
- 第二个是可以传毫秒和纳秒的方法。如果纳秒大于等于0.5毫秒,或者纳秒不为0同时毫秒为0,那就给毫秒+1
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
sleep会抛出InterruptedException异常。调用sleep不会释放自身的锁。
3.2.6 线程的强占
join()方法。是Thread对象调用的方法。在某一个线程下去调用join方法。
package com.xqm.juc.threadBasic;
public class Test16 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1=" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t2=" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main=" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 1) {
// 这里会等到t1执行完之后才会继续执行
t1.join();
// 这里会等待t1执行2s之后,main会继续执行
// t1.join(2000);
}
}
}
}
由于t1.join()在主线程中调用,实际就是t1线程插队到main线程,main线程就会被挂起,等待t1执行完才会继续执行。
注意:如果在等待期间,比如join(10000),10s,而两秒后t1结束了,这时候main会立即被唤醒,而不是等待10s后才会被唤醒。
3.2.7 守护线程
守护线程的含义就是在后台运行,直到所有的线程结束之后,守护线程会自动结束。
主线程默认是非守护线程。直到所有的非守护线程结束,JVM才会结束。
设置守护线程:
t1.setDaemon(true);
3.2.8 线程的等待和唤醒
wait和motify、notifyAll。每个对象都有自己的锁池和等待池。
可以让获取到锁的线程,通过wait()方法进入到锁的等待池,并且会释放锁资源。
可以让在等待池中的线程,通过notify或者notifyAll方法,将线程唤醒,添加到锁池中,去争夺锁资源。
notify是随机唤醒等待池中一个线程,notifyAll是将等待池中的所有线程唤醒,添加到锁池中。
wait和notify和notifyAll必须放在锁中,也就是synchronized中,否则会抛出异常。因为只有拿到锁资源后才能进行wait,没有锁资源才能进行notify。
package com.xqm.juc.threadBasic;
public class Thread17 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
sync();
});
Thread thread1 = new Thread(() -> {
sync();
});
thread.start();
thread1.start();
try {
Thread.sleep(12000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Thread17.class) {
// 只会唤醒一个
Thread17.class.notify();
}
}
public static synchronized void sync() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
// 线程进入等待池
Thread17.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
结果:
thread和thread1会先执行5次,然后随机唤醒一个线程,执行完剩下的,另一个线程会一直阻塞下去。
3.3 线程结束方法
线程结束的方式很多,自然执行完毕结束,或者人为结束,或者抛出异常结束。
3.3.1 stop方法(不用)
强制让线程结束,无论线程在干嘛。
package com.xqm.juc.threadBasic;
public class Test18 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
Thread.sleep(1500);
thread.stop();
System.out.println(thread.getState());
}
}
结果:
TERMINATED
3.3.2 破坏死循环-使用共享变量
使用共享变量的时候,需要加上volatile关键字,不然会有线程可见性的问题。
这种方式用的也不多,有的线程会通过死循环来保证线程一直运行,通过共享变量来破坏死循环,达到退出线程的目的。
package com.xqm.juc.threadBasic;
public class Test19 {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
}
System.out.println("任务结束");
});
thread.start();
// 等待thread启动
Thread.sleep(500);
flag=false;
// 等待flag生效
Thread.sleep(500);
System.out.println(thread.getState());
}
}
结果:
任务结束
TERMINATED
3.3.3 interrupt
通过 Thread.currentThread().isInterrupted()
查看打断标记位是否为false。
package com.xqm.juc.threadBasic;
public class Test20 {
public static void main(String[] args) {
// 查看线程是否被打断,实际就是查看标记位是否为false
System.out.println(Thread.currentThread().isInterrupted());
// 再次查看
Thread.currentThread().interrupt();
// 变成了true,实际上只是改变了标记位,并不是直接打断了线程
System.out.println(Thread.currentThread().isInterrupted());
// 返回当前线程的interrupted标记位,并进行归位为false true
System.out.println(Thread.interrupted());
// 这里输出为false,因为标记位归位了
System.out.println(Thread.interrupted());
}
}
结果:
false
true
true
false
进行线程的退出:
package com.xqm.juc.threadBasic;
public class Test21 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
// 执行业务
}
System.out.println("退出循环");
});
thread.start();
Thread.sleep(500);
thread.interrupt();
Thread.sleep(500);
System.out.println(thread.getState());
}
}
结果:
退出循环
TERMINATED
实际上打断还是共享变量,标记位。
通过打断WAITING和TIMED_WAITING状态的线程,从而抛出异常自行处理。
package com.xqm.juc.threadBasic;
public class Test22 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while (true){
// 获取任务
// 拿到任务并执行任务
// 如果没有任务,让线程休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 这里可以做出自己的处理
System.out.println("基于打断标记位进行退出");
e.printStackTrace();
return;
}
}
});
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
结果:
基于打断标记位进行退出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xqm.juc.threadBasic.Test22.lambda$main$0(Test22.java:11)
at com.xqm.juc.threadBasic.Test22$$Lambda$1/565760380.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
这种是阻塞进行中断
package com.xqm.juc.threadBasic;
public class Test23 {
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
synchronized (object) {
// 获取任务
// 拿到任务并执行任务
}
System.out.println("线程结束");
});
synchronized (object) {
// 主线程拿到锁,开启thread线程
thread.start();
// 留时间让thread线程开启
Thread.sleep(500);
// thread线程获取不到锁,会进入阻塞状态,这时候打断
thread.interrupt();
// 线程休眠
Thread.sleep(500);
// 大于线程状态
System.out.println(thread.getState());
Thread.sleep(5000);
}
// 结果:
// BLOCKED
// 线程结束
}
}
结果:
BLOCKED
线程结束