一:概述
java发生线程安全的有原因有两个因素:第一,存在共享资源(也称临界资源,不知道为什么取这破名字);第二,存在多条线程操作共享数据。为了解决此问题,我们需要在一个线程访问共享资源的时候,别的线程无法访问此资源,达到共享资源访问互斥的目的。在java中,关键字synchronized可以保证在同一时刻,只有一个线程可以访问某个方法或者模块代码,同时,synchronized还能保证共享资源在一个线程里面的变化可以反映到其他线程,也就是其他线程能够做到这个共享资源已经变化,从而取到最新的共享资源的值,这就是保证共享资源的可见性,完全代替volatile关键字。
二:synchronized的三种使用方式
synchronized关键字主要有以下三种使用场景:
1.修饰实例方法,此时锁住的是该实例对象,调用该实例方法时需要获取该实例的锁。
2.修饰静态方法,此时锁住的是该类对象,调用该类的静态方法时需要获得该类的锁。
3.修饰代码块,此时锁住的是该实例对象,执行该代码块时需要获得该实例的锁。
synchronized作用于实例方法:
public class AccountingSync implements Runnable {
//共享资源
static int i = 0;
public static void main(String[] args) throws InterruptedException {
AccountingSync as = new AccountingSync();
Thread t1 = new Thread(as);
Thread t2 = new Thread(as);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
//synchroniezed 修饰实例方法
public synchronized void increase() {
i++;
}
@Override
public void run() {
for(int j = 0 ; j < 10000 ; j++) {
increase();
}
}
}
输出结果如下:
20000
在上例中,i++不具备原子性,他实现的流程是先读,再加1,后写入结果;在读和写之间可以别的线程打断;假设increase方法没有使用关键字synchronized修饰,那么t1线程调用i++,在完成自增后,把数据往回写;再写入操作完成之前,t2可能也去执行自增,此时t1线程的执行被打断,由于t1自增的结果还没有写回去,所以t2拿到的i的值是t1自增前的值,这样就会使得最终结果比20000小。如果加了synchronized关键字,在t1完成自增后写入过程之前,因为t1没有释放实例as的锁,所以t2拿不到这把锁,此时t2是无法操作i的,必须等待t1把i自增后的结果写回完成并释放as的锁之后,t2才有可能拿到as的锁,接着执行自增操作。因为一个对象只有一把锁,就算t1在同步执行increase方法之前,t2也不能执行别的被synchronized修饰的实例方法;但是t2可以执行没有被synchronized修饰的方法。
如果有两个实例对象,他们的锁肯定就不一样了,如果t1去执行第一个实例对象的synchronized方法,此时t2去执行另一个对象synchronized方法,此时还是线程安全的;但是如果这两个方法都去操作共享资源,那么就会产生线程不安全的问题:
public class AccountingSyncFailure implements Runnable {
//共享资源
static int i = 0;
public static void main(String[] args) throws InterruptedException {
//创建两个Runnable对象
AccountingSyncFailure as = new AccountingSyncFailure();
AccountingSyncFailure as1 = new AccountingSyncFailure();
//两个Thread对应两个不同的Runnable对象
Thread t1 = new Thread(as);
Thread t2 = new Thread(as1);
t1.start();
t2.start();
// //join的含义是当前线程等待thread线程终止后才能从thread.join返回
t1.join();
t2.join();
System.out.println(i);
}
//synchroniezed 修饰实例方法
public synchronized void increase() {
i++;
}
@Override
public void run() {
for(int j = 0 ; j < 10000 ; j++) {
increase();
}
}
}
在上例中,虽然创建了两个实例as1和as2,但是他们都去访问了共享资源i,所以最终的结果肯定比20000小。
synchronized修饰类方法
synchronized修饰类方法时,作用的是当前类的锁,如果线程t1调用实例的非静态 synhronized方法,那么另一个线程t2是可以调用这类的静态 synchronized方法的,不会发生互斥现象,因为访问静态synchronized方法需要的是持有类的锁,而访问实例synchronized方法需要的是持有对象的锁,不是同一把锁:
public class TestStatic implements Runnable{
static int i = 0;
public static void main(String[] args) throws InterruptedException {
TestStatic ts = new TestStatic();
Thread t1 = new Thread(ts);
Thread t2 = new Thread(ts);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
//类方法,访问该方法需要获得类的锁,就是Class对象的锁
public static synchronized void increase(){
i++;
}
//实例方法,访问该方法需要获得实例对象的锁
public synchronized void increase1() {
i++;
}
@Override
public void run() {
for(int j = 0 ; j < 10000 ; j++) {
increase();
}
}
}
synchronized修饰同步代码块
除了修饰方法,synchronized还可以修饰代码块,有些方法,可能只需要部分代码同步,其他的代码不会发生线程安全问题的话,此时只需要将需要同步的代码块用synchronized修饰即可:
public class TestCode implements Runnable {
static TestCode tc = new TestCode();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(tc);
Thread t2 = new Thread(tc);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
//tc也可以用this代替,建议用this
synchronized (tc) {
//这里可以根据自己的需要添加无需同步的逻辑
for(int j = 0 ; j < 10000 ; j++) {
i++;
}
}
}
}
三:synchronized的实现原理
注意,这是虚拟机的运行时数据区,并不是所谓的java内存模型。虚拟机栈存放栈帧,栈帧里面存放局部变量表,操作数栈,返回地址等信息;本地方法栈是与native方法相关的,不管;堆大家都知道,真正存放对象的地方;方法区(JDK1.8以后叫元数据区)是存放类信息和常量池等信息的。堆和方法区是线程共享的。假装有下面的代码:
A a = new A()
o创建后,对象分布如下:从图中可以看出,堆里面的对象可以分为三部分:
1.对象头,分为Mark Word 和 类型指针两部分,类型指针指向方法区的类信息;Mark Word里面存放的是对象自身的运行时数据,比如hash码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。
2.实例数据,这里存放的就是对象的成员属性的值了,包括父类的成员属性,如果存放数组的话,还有数组的长度。
3.对齐填充,由于虚拟机规定对象的起始地址必须是8字节的整数倍,如果不足8字节的某个整数倍,那么写入空数据填充,这个不太关注。
从上面的分析看来,跟synchronized相关的,就是对象头了,对象头是synchronized实现的基础,重点分析。
虚拟机一般用2个字节来存放对象的头信息,如果该对象是数组类型的话,那么用3个字节来存放头信息,多出来的那个用来存储数组长度。虚拟机使用markOop类型来描述Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间,32位虚拟机的markOop实现如下:
1.hash: 保存对象的哈希码
2.age: 保存对象的分代年龄
3.biased_lock: 偏向锁标识位
4.lock: 锁状态标识位
5.JavaThread*: 保存持有偏向锁的线程ID
6.epoch: 保存偏向时间戳
而不同的锁状态,Mark Word中存储的数据又不一样:
我们先分析重量级锁也就是通常说synchronized的对象锁,从图中可以看出,该锁的标记位是10,其中指针ptr指向的是monitor(管程或者监视器锁)的起始地址;每个实例对象都有一个monitor对象与之关联,实例对象和monitor对象之间的关系有多种实现方式,如monitor可以与实例对象一起创建、销毁,或者当线程试图获得对象锁时自动生成。当一个monitor对象被一个线程持有时,它便处于锁定状态。monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor对象有两个队列:_WaitSet和_EntryList;这两个集合用来保存ObjectMonitor对象列表,因为每个等待锁的线程会被封装成ObjectMonitor对象;_owner指向持有ObjectMonitor对象的线程;当多个线程同时访问同步代码时,首先会进入_EntryList集合,当线程获得对象的monitor后进入_owner属性,并把_owner属性设置为当前线程,同时将计数器_count加一;若线程调用wait()方法,该线程将放弃当前持有的monitor对象,然后_owner置空,_count减一,同时该线程进入_WaitSet集合等待被唤醒;若当前持有monitor对象的线程执行完毕,也会放弃monitor对象,并将_owner置空,_count减一,以便其他的线程获得该锁(等同于获得monitor对象)。调用过程如下:综上可知,monitor对象存在于对象头里面,这也是为什么我们常说锁的是对象,不是锁方法或者代码块的原因。下面看一个简单的例子:
public class Test{
public int i;
public void add(){
synchronized(this){
i++;
}
}
}
这个例子非常简单,就是一个整形成员变量i和一个add方法,add里面的代码块添加了synchronized修饰,首先通过javac -g Test.java进行编译,然后用javap -verbose Test来查看他的字节码:
//class文件的路径
Classfile /home/tuhao/Test.class
//文件创建的时间和大小
Last modified 2018-8-6; size 452 bytes
/MD5值
MD5 checksum afb042fe0aa113f37c5dd70e791cdcfe
//由Test.java这个文件编译而来
Compiled from "Test.java"
//类名
public class Test
//源文件
SourceFile: "Test.java"
//此文件支持的JDK最小版本
minor version: 0
//此文件支持的最大JDK版本(51代表JDK1.7)
major version: 51
//此类的修饰符,含义可以去网上查
flags: ACC_PUBLIC, ACC_SUPER
//此类的常量池,常量池非常重要
Constant pool:
#1 = Methodref #4.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#22 // Test.i:I
#3 = Class #23 // Test
#4 = Class #24 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTest;
#14 = Utf8 add
#15 = Utf8 StackMapTable
#16 = Class #23 // Test
#17 = Class #24 // java/lang/Object
#18 = Class #25 // java/lang/Throwable
#19 = Utf8 SourceFile
#20 = Utf8 Test.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = NameAndType #5:#6 // i:I
#23 = Utf8 Test
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/Throwable
{
//成员属性i和他的修饰符
public int i;
flags: ACC_PUBLIC
//Test的构造函数
public Test();
flags: ACC_PUBLIC
//构造函数调用的流程,通过虚拟机指令来表示
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTest;
//重点关注add方法
public void add();
//add函数调用的流程,通过虚拟机指令来表示
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
//将局部变量表的第0个位置的参数(this)压入栈顶
0: aload_0
//复制操作数栈栈顶的值,并插入到栈顶
1: dup
//弹出栈顶的数据,存入局部变量表的第2个位置
2: astore_1
//执行monitorenter指令,java虚拟机规范是这样解释这个指令的:进入一个对象的 monitor
3: monitorenter //重点关注monitorenter指令
//将局部变量表的第一个参数(this)压入操作数栈中
4: aload_0
//复制操作数栈栈顶的值,并插入到栈顶
5: dup
//获取属性i的值
6: getfield #2 // Field i:I
//将常量1压入栈顶(用于++)
9: iconst_1
//将栈里面的数据相加(i加上常量1)
10: iadd
//把自增后的结果写会给i
11: putfield #2 // Field i:I
////将局部变量表的第二个参数(this)压入操作数栈中
14: aload_1
//退出对象的monitorexit
15: monitorexit //重点关注monitorexit指令
//执行第24条指令
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 14
line 8: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 this LTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class Test, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
对于字节码不懂的,可以看我之前写的笔记:https://www.jianshu.com/p/635aea3a0ae2
通过字节码可以看出,在虚拟机层面实现同步的指令是monitorenter和monitorexit,monitorenter用于同步代码开始的位置,monitorexit用于同步代码结束的位置;当执行monitorenter指令的时候,执行同步代码块的线程就会试图获取(不一定能成功)当前实例对象所对应的 monitor 的所有权,那么:
1.如果对象的 monitor 的进入计数器为 0,那调用同步代码的线程可以成功进入 monitor,以及将计数器值设置为 1;调用线程就是 monitor 的所有者。
2.如果当前线程已经拥有对象的 monitor 的所有权,那它可以重入这 个 monitor,重入时需将进入计数器的值加 1。
3.如果其他线程已经拥有对象的 monitor 的所有权,那当前线程将被阻塞,直到 monitor 的进入计数器值变为 0 时,重新尝试获取 monitor 的 所有权。
注意,一个monitorenter指令可能会与一个或多个monitorexit指令配合实现Java 语言中 synchronized 同步语句块的语义。但monitorenter和monitorexit指令不会用来实现 synchronized方法的语义,尽管它们确实可以实现类似的语义。当一个 synchronized 方法被调用时,自动进入对应的 monitor,当方法返回时,自动退出 monitor,这些动作是 Java 虚拟机在调用和返回指令中隐式处理的,所以在上面的例子中,如果将synchronized修饰add方法而不是add方法里面的代码块,那么编译出来的字节码中是没有monitorenter和monitorexit指令指令的,不过在add方法的修饰符里面有个synchronized。在 Java 语言里面,同步的概念除了包括 monitor 的进入和退出操作以外,还包括有等待(Object.wait)和唤醒(Object.notifyAll 和 Object.notify)。这些操作包含在 Java 虚拟机提供的标准包 java.lang 之中,而不是通过 Java 虚拟机的指令集来显式支持(没事多看看java虚拟机规范,很有好处,而且此规范真的不难)。
四:synchronized的优化
上面分析了synchronized的使用方法和实现原理,但是必须注意到synchronized是一个重量级锁,效率较低,因为管程(monitor)是依赖于操作系统的Mutex Lock来实现的,在切换线程的时候,需要从用户态切换到内核态,这个切换需要比较长的时间,所以早期的synchronized是比较低效的。JDK1.6后,官方从虚拟机层面对synchronized进行了较大幅度的优化,为了减少获得锁和释放锁所带来的性能消耗,java引入和轻量级锁和偏向锁。
1.偏向锁
java官方解释,经过大量实验表明,大多数情况下,锁不存在多线程竞争,而是同一把锁经常被同一线程多次获取(既然是官方说的,我们就姑且相信吧);因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价,从而引入了偏向锁。偏向锁的核心思想是(完全没有必要去跟源码):如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word(上面介绍过,还贴了两张图)就进入偏向模式,此时Mark Word的结构也将变成偏向锁结构,锁标志位是01,此时并不会触发同步。如果此线程再次请求锁时,将无需再次获取锁,而是直接执行代码,这样就省去了申请锁(还有执行完了释放锁)的资源消耗。所以,在锁竞争不激烈的情况下,偏向锁能够大幅度提高性能。但是,在竞争比较激烈的场景下,偏向锁就失去了作用;因为竞争激烈的的场景,前一次申请锁的线程很有可能不是这次申请锁的线程;此时,偏向锁失败,失败后并不是直接升级为重量级锁,而是升级为轻量级锁。
总的来说,偏向锁就是通过消除资源无竞争下的同步语义,达到提高性能的目的,偏向锁的获取过程如下:
a.访问对象头Mark Word中偏向锁的标记位是否设置成了1,锁标记是否为01, 确认为可偏向状态。
b.如果是可偏向状态,检查对象头保存的线程ID是否是当前线程的ID,如果是执行同步代码。
c.如果Mark Word保存的线程ID不是当前的线程ID,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中的线程ID更新为当前线程ID,然后执行同步代码。
d.如果竞争失败,那么到达全局安全点(safepoint,这个时间点上没有字节码在执行)后时,此前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码;撤销偏向锁的时候会有stop the world现象,也就是卡一下,时间很短,跟GC类似。
e.执行同步代码
2.轻量级锁
在锁竞争激烈的情况下,如果偏向失败,升级为轻量级锁,此时Mark Word的结构也将变成轻量级锁的结构,锁标记位是00;轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。轻量级锁的后偶去过程如下:
a.在执行同步代码时,如果对象锁状态为无锁(此时锁标记位是01,偏向锁标记位是0),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存放对象头的Mark Word拷贝,这个空间也叫做Displace Lock Record;此时,对象头和栈的状态如下(盗图):
b.拷贝对象头的Mark Word到当前线程的栈帧的Lock Record中。
c.拷贝成功后,虚拟机使用CAS操作尝试将对象头的Mark Word的更新为指向Lock Record的指针,并将Lock Record里面的_owner指针指向对象头的Mark Word;如果更新成功,则执行步骤d,否则执行步骤e;此时,对象头和栈的状态如下(盗图):d.如果更新成功,那么该线程就拥有了该对象的锁,并且对象头的Mark Word设置成00(轻量级锁的标记位)。
e.如果更新失败,虚拟机首先检查对象头的Mark Word是否指向当前线程的栈帧,如果指向,说明当前线程获得了该对象的锁,那么直接执行同步代码;如果不指向,说明多个线程在竞争同一把锁,轻量级锁要升级为重量级锁,将锁状态置为10(重量级锁标记位),Mark Word中存储的就是指向重量级锁的指针(?),后面等待锁的线程也要进入阻塞状态,而当前线程则通过自旋锁来获取锁,自旋是为了不让线程阻塞,采用轮询的方式去获取锁。
3.自旋锁
轻量级锁失败后,虚拟机为了避免竞争失败的线程挂起,还会采取自旋锁来优化。假装t1线程竞争失败后,t2正在执行同步代码;如果t1失败后立马挂起,就会由用户态转到内核态,这个开销是较大的;如果t2执行同步代码很快,共享资源马上得到释放,轮到t1去执行,可是t1正在老老实实的切换(快来欺负老实人)状态,好不容易等他切到挂起状态,又发现轮到自己访问共享资源了,然后又老老实实的切回来,多费事。自旋锁就是虽然一个线程虽然现在竞争失败了,但是假设持有锁的线程很快就能执行完毕,那么失败的线程等等再去访问共享资源又有何妨?没必要真的取切换状态,因为切换的成本太高了。根据官方解释,经过大量实验表明,大多数情况下,线程持有锁的时间不会太长(既然是官方说的,我们就姑且相信吧),所以就让竞争失败的线程做若干次空循环(这就是所谓的自旋),经过循环,再去访问共享资源,此时之前持有锁的线程很有可能已经访问完毕了,这样这个失败的线程就可以持有锁了,然后执行同步代码。当然了,也不能老在那循环,因为空循环也是占用CPU资源的,所以多次循环后还没有拿到锁,那就真的挂起了。
3.锁消除
锁消除是一种更为彻底的优化手段。虚拟机在进行JIT编译(即时编译)时,通过扫描上下文,去除不可能存在共享资源竞争的锁,这样可以省去无谓的申请锁的时间和开销。比如下面的例子:
public class TestBuffer {
public static void main(String[] args) {
TestBuffer tb = new TestBuffer();
for(int i = 0 ; i < 10000; i++) {
tb.add("a", "b");
}
}
public void add(String s1 , String s2) {
//StringBuffer本身就是线程安全的,append也被synchronized
//修饰过,sb又是局部变量,并不会被其他线程使用,所以sb不会存
//在资源竞争的问题所以append方法的synchronized可以被消除掉
StringBuffer sb = new StringBuffer();
sb.append(s1).append(s2);
}
}
当然,上面仅仅是举一个例子,毕竟在一个for循环里面去大量创建StringBuffer对象并不是上面好的写法。
四:synchronized的关键点
1.synchronized的重入性
如果一个线程持有了一个对象的锁,然后再次访问该对象的共享资源时,这种现象叫做重入, 请求将会成功。比如线程在获得对象锁后,去执行这个对象的同步方法,在执行方法的过程中又去执行另个同步方法,也就是拿到这个对象的锁后再去请求该锁,这是允许的。总结起来就是,一个线程拿到对象锁后再次请求锁,这是允许的,这就是synchronized的重入性。
public class TestAgain implements Runnable{
static TestAgain ta = new TestAgain();
static int a , b = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(ta);
Thread t2 = new Thread(ta);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a : " + a + " , b : " + b);
}
@Override
public void run() {
for(int i = 0 ; i < 10000; i++) {
//申请当前对象的锁
synchronized(this) {
a++;
//再次请求,这是允许的,也会成功的
increase();
}
}
}
//同步方法
public synchronized void increase() {
b++;
}
}
在上例中,synchronized(this)已经拿到了该对象的锁,然后在执行同步方法increase的时候,又会去申请该对象的锁,这是可以的,没毛病。需要注意的是,子类继承父类时,子类也可以通过重入性调用父类的同步方法。记得上面介绍montior的时候有个属性_count,每次重入时,_count都会+1。
2.线程中断与synchronized
java提供了下面三种方法使得线程中断:
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于被阻塞状态,或者试图执行一个阻塞操作时,使用Thread.interrupt()中断该线程,此时会抛出一个InterruptedException异常,同时中断状态将被复位,也就是从中断状态变成非中断状态:
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread() {
public void run() {
//无限循环
try {
//while在try语句块里面,通过异常中断可以退出run方法
while(true) {
//当线程处于阻塞状态,线程必须捕获,无法向外抛出
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("Interrupt when sleeping");
boolean interrupt = this.isInterrupted();
//中断状态被复位
System.out.println("interrrupt : " + interrupt);
}
};
};
//启动线程
t1.start();
//这里也睡2秒
TimeUnit.SECONDS.sleep(2);
//主动中断处于阻塞状态的线程
t1.interrupt();
}
}
这里创建一个子线程,在子线程的while循环里面使得子线程无限睡眠;然后在主线程睡眠两秒,然后主动中断处于睡眠状态的子线程,输出结果如下:
Interrupt when sleeping
interrrupt : false
可以看到,子线程的中断状态果然被复位了。
但是中断操作对于正在等待获取锁对象的线程来说,并不起作用;也就说,如果一个线程正在等待对象锁,那么这个线程要么继续等待,要么拿到锁,执行被synchronized修饰的代码,就算你手动中断也无效:
public class SynchronizedBlock implements Runnable{
//在构造器里面创建子线程执行同步方法
public SynchronizedBlock() {
new Thread() {
public void run() {
test();
};
}.start();
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlock sb = new SynchronizedBlock();
Thread t = new Thread(sb);
t.start();
TimeUnit.SECONDS.sleep(1);
//主动中断线程,但是run方法的log将不会被打印
t.interrupt();
}
//随便定义一个同步方法
public synchronized void test() {
System.out.println("call test method");
while(true) {
}
}
//自己的run方法
@Override
public void run() {
while(true) {
//如果主线程中断了,那么打出log
if(Thread.interrupted()) {
System.out.println("线程中断");
}else {
//没有中断就调用test方法
test();
}
}
}
}
上例中,在构造函数里面创建一个子线程并运行,然后在main方法里面再运行线程,然后主动中断线程,输出结果如下:
call test method
可以看到,"线程中断"这行log没有打印出来,说明正在等待对象锁的线程是无法被打断的。
摘自:https://blog.csdn.net/javazejian/article/details/72828483 (略有修改)
引用:https://blog.csdn.net/zqz_zqz/article/details/70233767