概述
多线程核心知识梳理
线程状态
6个状态定义:java.lang.Thread.State
- new: 新建的尚未启动的线程的线程状态。
- Runnable: 可运行线程的线程状态,等待CPU调度。
- Blocked: 线程阻塞等待监视器锁定的线程状态。处于synchronized同步代码块或方法中被阻塞。
- Waiting: 等待线程的线程状态,需其他线程唤醒才执行。下列不带超时的方式:Object.wait、Thread.join、LockSupport.park。
- Timed Wating: 具有指定等待时间的等待线程的线程状态。下列带超时的方式:Thread.sleep、Object.wait、Thread.join、LockSupport.partNanos、LockSupport.parkUtil。
- Teminated: 终止线程的线程状态。线程正常完成执行或者出现异常。
线程中止
不正确的线程中止–Stop
Stop: 中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议用。
Destroy: JDK未实现该方法。
public class StopThread extends Thread {
private int i = 0, j = 0;
@Override
public void run() {
synchronized (this) {
// 增加同步锁,确保线程安全
++i;
try {
// 休眠10秒钟,模拟耗时操作
Thread.sleep(10000);
}catch(InterruptedException e) {
e.printStackTrace();
}
++j;
}
}
public void print() {
System.out.println("i=" + i + ",j=" + j);
}
}
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
StopThread thread = new StopThread();
thread.run();
// 休眠1秒,确定i变量自增成功
Thread.sleep(1000);
// 终止线程
thread.stop();
while (thread.isAlive()){
// 确保线程终止
}
thread.print();
}
}
理想输出: i = 1, j = 1
程序执行结果: i = 1, j = 0
没有保证同步代码块里面数据的一致性,破坏了线程安全
正确的线程中止–interrupt
如果目标线程在调用Object class的wait0、wait(long)或wait(long, int)方法、join()、join(long, int) 或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,拋出InterruptedException异常。
如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。
对于Demo3中的示例,stop改成interrupt后,最终输出为“i=1,j=1”,数据一致。
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
StopThread thread = new StopThread();
thread.run();
// 休眠1秒,确定i变量自增成功
Thread.sleep(1000);
// 终止线程
thread.interrupt();
while (thread.isAlive()){
// 确保线程终止
}
thread.print();
}
}
正确的线程中止–标志位
代码逻辑中,增加一个判断,用来控制线程执行的中止。如下所示:
public class Demo4 {
public volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
while (flag) {
System.out.println("运行中。。。");
Thread.sleep(1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(3000L);
flag = false;
System.out.println("程序运行结束");
}
}
线程通信
线程通信的方式
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。
涉及到线程之间相互通信,分为以下四类:
- 文件共享
- 网络共享
- 共享变量
- jdk提供的线程协调API
细分为:suspend/resume、wait/notify、park/unpark
文件共享
共享变量
线程协调–JDK API
JDK中对于需要多线程写作完成某一任务的场景,提供了对应API支持。
多线程协作的典型场景是:生产者-消费者模型。(线程阻塞、线程唤醒)
API–被弃用的suspend和resume
作用:调用suspend挂起目标线程,通过resume可以恢复线程执行。
Object baozipu = null;
/** 正常的suspend和resume */
public void suspenResumeTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (baozipu == null) {//如果没有包子,进入等待
System.out.println("1. 进入等待");
Thread.currentThread().suspend();
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
consumerThread.resume();
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
3.通知消费者
2.买到包子,回家!
被弃用的主要原因是容易写出死锁的代码。
suspend和resume死锁示例
同步代码中使用
/** 同步代码中使用 */
public void suspenResumeDeadLockTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
System.out.println("1. 进入等待");
synchronized (this) {
Thread.currentThread().suspend();
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
// 争取到锁以后再恢复consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
suspend比resume后执行
/** suspend比resume后执行*/
public void suspenResumeDeadLockTest1() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
System.out.println("1. 进入等待");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.currentThread().suspend();
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
consumerThread.resume();
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
3.通知消费者
wait/notify机制
这些方法只能由同一个对象锁的持有者线程调用,也就是写在同步代码块里面,否则会抛出IllegalMonitorStateException异常。
wait方法导致当前线程等待,加入该对象的等待集合中,并且释放当前持有的对象锁。
notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
注意: 虽然会wait自动解锁,但是对于顺序有要求, 如果在notify被调用之后才开始wait方法的调用,线程会永远处于WAITING状态。
/** 正常的wait和notify */
public void waitNotifyTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (baozipu == null) {//如果没有包子,进入等待
synchronized (this) {
try {
System.out.println("1. 进入等待");
this.wait(); // wait后释放当前对象锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
// 拿到当前对象锁
synchronized (this) {
this.notify();
}
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
3.通知消费者
2.买到包子,回家!
会导致永久等待的wait和notify的示例
/** 会导致永久等待的wait和notify */
public void waitNotifyDeadLockTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (baozipu == null) {//如果没有包子,进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1. 进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
synchronized (this) {
this.notify();
}
System.out.println("3.通知消费者");
}
运行结果:
3.通知消费者
1. 进入等待
Park/unpark机制
线程调用park则等待“许可”,unpark方法为指定线程提供“许可(permit)”。
不要求park和unpark方法的调用顺序
多次调用unpark之后,再调用park线程会直接运行。但是不会叠加,也就是说,连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。
/** 正常的park和unpark */
public void parkUnparkTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (baozipu == null) {//如果没有包子,进入等待
System.out.println("1. 进入等待");
LockSupport.park();
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
3.通知消费者
2.买到包子,回家!
park拿到锁之后不会自动释放锁,会导致死锁
死锁的park和unpark
/** 死锁的park和unpark */
public void parkUnparkDeadLockTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (baozipu == null) {//如果没有包子,进入等待
System.out.println("1. 进入等待");
// 拿到锁后挂起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2.买到包子,回家!");
});
consumerThread.start();
// 3秒之后,产生一个包子
Thread.sleep(3000L);
baozipu = new Object();
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3.通知消费者");
}
运行结果:
1. 进入等待
注意:不可以使用if语句判断,官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。伪唤醒是指线程并非因为notify、notifyall、 unpark等api调用而唤醒,是更底层原因导致的。以上代码均相同。
线程封闭
多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候,都要用到共享数据,所以线程封闭的概念就被提出来了。
数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术成为线程封闭。
线程封闭具体的体现有:
-
ThreadLocal
-
局部变量
ThreadLocal
ThreadLocal是Java中一种特殊的变量。
他是一个线程级别的变量,每个线程都有一个ThreadLocal,就是每个线程都拥有了自己独立的变量,竞争条件被彻底消除了,在并发模式下是绝对安全的变量。
用法:
ThreadLocal<T> var = new ThreadLocal<T>();
会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。
可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来替代方法传参的做法。
/**
* 封闭线程示例
*/
public class Demo7 {
// ThreadLocal变量,每一个线程都有一个副本,互不干扰
public static ThreadLocal<String> value = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
value.set("这是主线程设置的123");
String v = value.get();
System.out.println("线程1执行之前,主线程取到的值:" + v);
new Thread(() -> {
String v1 = value.get();
System.out.println("线程1取到的值:" + v1);
// 设置线程1的ThreadLocal
value.set("这是线程1设置的456");
v1 = value.get();
System.out.println("重新设置后,线程1取到的值:" + v1);
System.out.println("线程1执行结束!");
}).start();
// 等待所有线程执行结束后
Thread.sleep(5000L);
v = value.get();
System.out.println("线程1执行之后,主线程取到的值:" + v);
}
}
运行结果:
线程1执行之前,主线程取到的值:这是主线程设置的123
线程1取到的值:null
重新设置后,线程1取到的值:这是线程1设置的456
线程1执行结束!
线程1执行之后,主线程取到的值:这是主线程设置的123
栈封闭
局部变量的固有属性之一就是封闭在线程中。
它们位于执行线程的栈中,其他线程无法访问这个栈。
最后
以上就是文静小刺猬为你收集整理的多线程核心多线程核心知识梳理的全部内容,希望文章能够帮你解决多线程核心多线程核心知识梳理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复