1.概述
2. 线程与进程
2.1 进程与进程
- 进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器(这里感觉要学了计算机组成原理之后会更有感觉吧!)
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集. 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP, 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2.2 并行与并发
并发
在单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
并行
多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
同步和异步的概念
- 以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步
- 多线程可以使方法的执行变成异步的,比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。
3. java线程
3.1 创建和运行线程
方法一,直接使用 Thread
// 构造方法的参数是给线程指定名字,,推荐给线程起个名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
方法二,使用 Runnable 配合 Thread (推荐)
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
Thread t2 = new Thread(task2, "t2");
t2.start();
// 使用lambda表达式简化
Runnable r1 = () -> log.debug("hello"););
new Thread(r1).start();
// 或者
new Thread(() -> {
log.debug("hello");
}).start();
方法三,FutureTask 配合 Callable
FutureTask 是对 Runnable 的一个扩展, 由于Runnable的run方法返回值是void, 不能很好地在线程之间传递数据, 因此诞生了FutureTask. FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
3.2 Thread的常见方法
3.2.1 start 与 run
直接调用 run()
是在主线程中执行了 run()
,没有启动新的线程 使用 start()
是启动新的线程,通过新的线程间接执行 run()
方法 中的代码
3.2.2 sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出
InterruptedException
异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】 - 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
建议用 TimeUnit 的
sleep()
代替 Thread 的sleep()
来获得更好的可读性yield
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
小结
yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
3.2.4 join
在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行 Test10.java
static int r = 0;
public static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r); // 结果为:10
log.debug("结束");
}
小结
- 调用 join() 会使等待线程状态切换到WAITING状态
- join()方法传入一个等待时间的参数, 当等待超时时, 则不会继续等待.
3.2.5 sleep,yiled,wait,join 对比
关于join的原理和这几个方法的对比:看这里
- sleep 不释放锁、释放cpu
- wait 释放锁、释放cpu
- join 释放锁、抢占cpu
- yiled 不释放锁、释放cpu
补充:
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
3.2.6 interrupt 方法详解
情况一、打断 sleep,wait,join 的线程
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态, 被打断时效果一样, 以sleep为例:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("线程任务执行");
try {
Thread.sleep(10000); // wait, join
} catch (InterruptedException e) {
log.debug("被打断");
//e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
t1.interrupt();
log.debug("t1是否被打断?{}",t1.isInterrupted());
}
// 输出:
"t1是否被打断? false"
打断 sleep 的线程, 被打断的进程会报InterruptedException异常, 并且会清空中断状态,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。
情况二、打断正常运行的线程
打断正常运行的线程, 线程并不会暂停, 被打断线程的中断状态会置为true, 可以判断Thread.currentThread().isInterrupted();
的值来手动停止线程.
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
终止模式之两阶段终止模式
Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会.
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
private Thread thread;
public TwoPhaseTermination(){
thread = new Thread(()->{
log.debug("开始");
while (true) {
if (Thread.currentThread().isInterrupted()){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控...");
} catch (InterruptedException e) {
e.printStackTrace();
// 重设打断标记
Thread.currentThread().interrupt();
}
}
}, "t1");
}
public void start(){
thread.start();
}
public void stop(){
thread.interrupt();
}
}
情况三、打断park线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("park...");
LockSupport.park(); // 线程会停在这里, 状态为WAITING
log.debug("unpack...");
log.debug("打断标记:{}",Thread.currentThread().isInterrupted()); // 打断标记:true
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("t1在park时的状态 : {}", t1.getState()); // t1在park时的状态 : WAITING
t1.interrupt();
}
3.3 守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true);
方法变成守护线程
垃圾回收器线程就是一种守护线程,Tomcat 中的 Acceptor 和 Poller 线程也都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
3.4 线程的状态
3.4.1 五种状态
五种状态的划分主要是从操作系统的层面进行划分的
- 初始状态,仅仅是在语言层面上创建了线程对象,即
Thead thread = new Thead();
,还未与操作系统线程关联 - 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
- 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3.4.3 六种状态
这是从 Java API 层面来描述的,我们主要研究的就是这种。
- NEW 跟五种状态里的初始状态是一个意思
- RUNNABLE 是当调用了
start()
方法之后的状态,注意,Java API 层面的RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行) BLOCKED
,WAITING
,TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
4. 共享模型之管程
临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区
竞态条件的概念 : 多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件
为了避免临界区中的竞态条件发生,由多种手段可以达到
- 阻塞式解决方案:synchronized ,Lock
- 非阻塞式解决方案:原子变量
4.1 阻塞式解决方案之synchronized
使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换.
class Number{
private static int count;
// 静态方法锁住的是Number.class对象
public static synchronized void a(){
count++;
System.out.println(count);
}
// 成员方法锁住的是this
public synchronized void b(){
count--;
System.out.println(count);
}
}
4.2 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
4.3 Monitor
Java 对象头
以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;
数组对象
其中 Mark Word 结构为
Monitor 原理(锁原理)
Monitor被翻译为监视器或者说管程
每个java对象都可以关联一个Monitor,如果使用synchronized
给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针
- 刚开始时Monitor中的Owner为null
- 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
- 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入EntryList中变成BLOCKED状态
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析
synchronized 原理进阶
轻量级锁
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
,假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference
- 让锁记录中的Object reference指向对象,并且尝试用cas(compare and swap)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中
- 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示
- 如果cas失败,有两种情况
- 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
- 如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数
- 当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一
- 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象
- 成功则解锁成功
- 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态
- 当Thread-0 退出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才有意义。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
偏向锁
在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了
**
偏向状态
一个对象的创建过程
- 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.
- 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-
XX:BiasedLockingStartupDelay=0
来禁用延迟 - 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
- 实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试
synchronized原理图
4.4 Wait/Notify
Wait/Notify原理
- 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态。
- 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:
- BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
- WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
- BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。
注:只有当对象加锁以后,才能调用 wait 和 notify 方法
API介绍
下面的三个方法都是Object中的方法; 通过锁对象来调用
- wait(): 让获得对象锁的线程到waitSet中一直等待
- wait(long n) : 当该等待线程没有被notify, 等待时间到了之后, 也会自动唤醒
- notify(): 让获得对象锁的线程, 使用锁对象调用notify去waitSet的等待线程中挑一个唤醒
- notifyAll() : 让获得对象锁的线程, 使用锁对象调用notifyAll去唤醒waitSet中所有的等待线程
它们都是线程之间进行协作的手段
, 都属于Object对象的方法
, 必须获得此对象的锁, 才能调用这些方法
@Slf4j(topic = "c.WaitNotifyTest")
public class WaitNotifyTest {
static final Object obj = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
synchronized (obj) {
log.debug("执行...");
try {
// 只有获得锁对象之后, 才能调用wait/notify
obj.wait(); // 此时t1线程进入WaitSet等待
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...");
}
}, "t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行...");
try {
obj.wait(); // 此时t2线程进入WaitSet等待
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...");
}
}, "t2").start();
// 让主线程等两秒在执行,为了`唤醒`,不睡的话,那两个线程还没进入waitSet,主线程就开始唤醒了
Thread.sleep(1000);
log.debug("唤醒waitSet中的线程!");
// 只有获得锁对象之后, 才能调用wait/notify
synchronized (obj) {
// obj.notify(); // 唤醒waitset中的一个线程
obj.notifyAll(); // 唤醒waitset中的全部等待线程
}
}
}
13:01:36.176 guizy.WaitNotifyTest [t1] - 执行...
13:01:36.178 guizy.WaitNotifyTest [t2] - 执行...
13:01:37.175 guizy.WaitNotifyTest [main] - 唤醒waitSet中的线程!
13:01:37.175 guizy.WaitNotifyTest [t2] - 其它代码...
13:01:37.175 guizy.WaitNotifyTest [t1] - 其它代码...
Sleep(long n) 和 Wait(long n)的区别 (重点)
不同点
- Sleep是Thread类的静态方法,Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
- Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁 (不释放锁的话, 其他线程就无法唤醒该线程了)
- Sleep方法不需要与synchronized一起使用,而Wait方法需要与synchronized一起使用(wait/notify等方法, 必须要使用对象锁来调用)
相同点
- 阻塞状态都为TIMED_WAITING (限时等待)
优雅地使用 wait/notify
synchronized (lock) {
while(//不满足条件,一直等待,避免虚假唤醒) {
lock.wait();
}
//满足条件后再运行
}
synchronized (lock) {
//唤醒所有等待线程
lock.notifyAll();
}
例如:
@Slf4j(topic = "guizy.WaitNotifyTest")
public class Main {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(); // 此时进入到waitset等待集合, 同时会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
Sleeper.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
11:19:25.275 guizy.WaitNotifyTest [小南] - 有烟没?[false]
11:19:25.282 guizy.WaitNotifyTest [小南] - 没烟,先歇会!
11:19:25.282 guizy.WaitNotifyTest [小女] - 外卖送到没?[false]
11:19:25.283 guizy.WaitNotifyTest [小女] - 没外卖,先歇会!
11:19:26.287 guizy.WaitNotifyTest [送外卖的] - 外卖到了噢!
11:19:26.287 guizy.WaitNotifyTest [小女] - 外卖送到没?[true]
11:19:26.287 guizy.WaitNotifyTest [小女] - 可以开始干活了
11:19:26.288 guizy.WaitNotifyTest [小南] - 没烟,先歇会!
4.5 park与unpark
两个都是LockSupport类的静态方法
park : 暂停当前线程, 线程进入WAIT状态
unpark : 恢复当前线程
一个现象 : 先调用unpark(), 再调用park(), 则线程不会暂停
4.6 线程状态转换
- 情况一:NEW –> RUNNABLE
当调用了 t.start() 方法时,由 NEW –> RUNNABLE - 情况二: RUNNABLE <–> WAITING
- 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
- 竞争锁成功,t 线程从 WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 WAITING –> BLOCKED
**
情况三:RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
情况四: RUNNABLE <–> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
情况五: RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
- 情况六:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING, 注意是当前线程在 t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况七:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况八:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
- 情况九:RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
- 情况十: RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
4.7 ReentrantLock
ReentrantLock是java.util.concurrent.locks中的一个可重入锁类。在高竞争条件下有更好的性能,且可以中断。深入剖析ReentrantLock的源码有助于我们了解线程调度,锁实现,中断,信号触发等底层机制,实现更好的并发程序。
和 synchronized 相比具有的的特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (先到先得)
- 支持多个条件变量( 具有多个 WaitSet)
- synchronized 发生异常会自动释放锁, 不会造成死锁,而ReentrantLock不会主动释放锁
基本语法
// 获取ReentrantLock对象
private ReentrantLock reentrantLock = new ReentrantLock();
// 加锁
reentrantLock.lock();
try {
// 需要执行的代码
}finally {
// 释放锁
reentrantLock.unlock();
}
可打断
使用lockInterruptibly()方法上锁时, 在未能未能获得锁而进入阻塞状态时,可被其他线程打断而放弃竞争锁
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
reentrantLock.lockInterruptibly();
while (true) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " : 我睡了");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " : 被打断");
// 打断后的处理
} finally {
reentrantLock.unlock();
}
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("开始打断");
t1.interrupt();
}
注 : 已经获得了锁的进程也会被打断而放弃锁资源
锁超时
使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true ,反之则返回 false 。
并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中 timeout 为最长等待时间,TimeUnit 为时间单位
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 判断获取锁是否成功,最多等待1秒
if(!lock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("获取失败");
// 获取失败,不再向下执行,直接返回
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
// 被打断,不再向下执行,直接返回
return;
}
System.out.println("得到了锁");
// 释放锁
lock.unlock();
});
lock.lock();
try{
t1.start();
// 打断等待
t1.interrupt();
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
公平锁
// 默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
4.8 ReadWriteLock
Lock readLock(); Lock writeLock();
这是个接口,ReentrantReadWriteLock是这个接口的实现类
4.9 线程通信
五、共享模型之内存
1、Java 内存模型(JMM)
JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM 体现在以下几个方面
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响
2、可见性
1)退不出的循环
首先看一段代码:
public static boolean run = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(run) {
}
}, "t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 Stop");
run = false;
}
解决方法
- 使用 volatile (易变关键字)
- 它可以用来修饰成员变量和静态成员变量(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
public static volatile boolean run = true; // 保证内存的可见性