目录
1、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
2、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
3、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
背景
在多线程高并发的情况下线程安全是我们需要关注的重要问题,造成线程安全的主要原因有:
1.存在共享数据(也称临界资源),非常容易理解就是各个线程都可以看见的数据。
2.存在多线程共同操作数据,存在多个线程操作共享数据的情况。
synchronized就可以解决这一安全问题,synchronized作为一个互斥锁,基本原理可以理解为:当存在多个线程操作共享数据时,保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,同时其他线程可以看到这个共享数据的更改。
一、synchronized的应用
synchronized关键字最主要有以下3种应用方式,分别为:
1、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
package com.zcm.juc;
public class T {
private int count = 10;
private Object o = new Object();
public void m() {
//任何线程要执行下面的代码,必须先拿到o的锁
synchronized(o) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
//同时也可以使用this来代替o,表示对当前对象加锁
//存在和下面实例方法同样的问题,统一讲解
public void m1() {
//任何线程要执行下面的代码,必须先拿到this的锁
synchronized(this) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
注意:synchronized锁定对象时,需要注意锁对象不能是Intger、Long等包装类以及String(主要是String常量,但是尽量把String直接排除)。因为包装类属于共享缓存模式,看上去锁是私有的,但其实在各自的范围内都是共享的,比如:在Integer默认缓存-127~128之间的数值,在此范围内所有的Integer其实是共享的,此时如果多个线程进行调用,会出现脏读现象。
同时锁对象也不能添加final关键字,因为你改变之后,对象头存储的锁信息出现问题。
/**
* 锁定某对象o,如果o的属性发生改变,不影响锁的使用
* 但是如果o变成另外一个对象,则锁定的对象发生改变
* 应该避免将锁定对象的引用变成另外的对象
* @author mashibing
*/
package com.zcm.juc;
import java.util.concurrent.TimeUnit;
/**
* @Author zhangchunming
* @Description //TODO 锁对象加final,让其不要改变
* @Date 21:51 2021/8/19
* @Param
* @return
**/
public class SyncSameObject {
/*final*/ Object o = new Object();
void m() {
synchronized(o) {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
SyncSameObject t = new SyncSameObject();
//启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
2、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
package com.zcm.juc;
public class T implements Runnable{
private static int i=0;
// 等同于在方法的代码执行时要synchronized(this)
public synchronized void count(){
i++;
}
@Override
public void run() {
// 循环值尽量多点,容易观察
for(int j=0;j<1000000;j++){
count();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new T());
//new新实例
Thread t2=new Thread(new T());
t1.start();
t2.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t1.join();
t2.join();
System.out.println(i);
i = 0;
T t = new T();
//new新实例
Thread t3=new Thread(t);
//new新实例
Thread t4=new Thread(t);
t3.start();
t4.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t3.join();
t4.join();
System.out.println(i);
}
}
注意:对比俩次结果可以发现在t1和t2情况下,因为重新new了实例对象,导致实际过程中存在俩个对象锁,t1和t2分别进入自己的对象锁,此时线程安全无法保证。但是在静态方法中就不会出现这种情况,因为静态方法指向的对象永远是当前的对象。
3、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
package com.zcm.juc;
public class T implements Runnable{
private static int i=0;
// 等同于在方法的代码执行时要synchronized(this)
public static synchronized void count(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
count();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new T());
//new新实例
Thread t2=new Thread(new T());
t1.start();
t2.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t1.join();
t2.join();
System.out.println(i);
i = 0;
T t = new T();
//new新实例
Thread t3=new Thread(t);
//new新实例
Thread t4=new Thread(t);
t3.start();
t4.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t3.join();
t4.join();
System.out.println(i);
}
}
二、synchronized实现原理
1.对象头、Monitor
其实synchronized的应用都可以看做对象锁,只不过锁的对象不同,所以在理解synchronized原理时,先要明白对象的组成以及对于synchronized锁信息的存储。在JVM中,对象可以分为俩种一种普通对象、一种数组对象,下面是他们的布局比较:
其中对象头是实现synchronized锁对象的基础,synchronized锁信息就是在对象头中进行存储,所以下面主要讲一下对象头:
类型指针是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
1)Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。下图是Java对象头的存储结构(32位虚拟
机):
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit |
---|---|---|---|---|
锁状态 | 对象HashCode值 | 对象分代年龄 | 0不是,1是 | 锁标志位 |
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机),这个是JVM虚拟机对synchronized优化时,对象头信息的变化状态:
在JVM未对synchronized优化之前(JDK6之前),synchronized就是重量级锁,此时指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。下面介绍monitor。
2)Monitor
Monitor可以理解为一个同步工具,也可以理解为一个同步对象(存储锁、线程信息)。所有的Java对象都是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下图一:
图一 图二
- Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
- EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
- RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
- Nest:用来实现重入锁的计数。
- HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
- Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。
图二表示ObjectMonitor的结构以及流程,在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合(就是没有抢到锁的线程),当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程(抢占到锁的线程),同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor(说明wait/notify等方法也依赖于monitor对象),owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
2、synchronized实现原理
在一开始我们就说过,synchronized的应用方式有三种,分别为:修饰代码块、修饰实例方法。修饰静态方法。这三种应用中,虽然基本原理都是基于进入和退出Monitor对象来实现同步的,但是修饰代码块与修饰方法实现细节是不相同,这里分开讲解。
1)修饰代码块时的底层原理
写一个最简单的修饰代码块的锁,并通过反编译工具进行反编译,下边代码块是锁的实现,以及部分反编译结果(只查看了锁的方法):
public class SyncTest01 {
public int i;
public void syncTask(){
//同步代码库
synchronized (this){
i++;
}
}
}
反编译结果:
//===========主要看看syncTask方法实现================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意:进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意:退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意:退出同步方法,出现多余的monitorexit,表明有方法异常执行
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字节码.......
}
从反编译结果中可知同步代码块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
2)修饰方法时的底层原理
同上面一样先写一个简单的程序并观察反编译结果:
public class SyncMethodTest {
public int i;
public synchronized void syncTask(){
i++;
}
}
反编译结果:
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
从反编译结果可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
三、synchronized的优化
1、Java虚拟机对synchronized的优化
Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在Java 6之后Java官方对从JVM层面对synchronized较大优化,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,出现了锁升级:无锁(刚new出来),偏向锁,轻量级、重量级。
1)偏向锁(在实际中70%的操作都是第一个线程)
偏向锁会偏向第一个访问锁的线程,在接下来的运行中如果没有其他线程访问这个锁,则偏向锁永远不会触发同步。连cas(轻量级锁)都不进行,提高了程序的运行性能。
在第一次进行同步时,会在对象头(Mark Word中)和栈中存储线程ID,当下次该线程进入时,判断是否还是这个线程ID,如果是,表示已经获取锁,在进入同步块中不需要使用cas进行加锁和解锁的操作;如果不是,表示有其他线程进行竞争,使用cas进行替换Mark word 中的线程ID,替换成功,说明上一个线程已经释放锁,此时替换线程ID;替换失败,表示存在竞争,设置偏向锁的标识为0,将锁升级为轻量级锁。
2)轻量级锁(乐观锁)
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁和重量级锁的区别就是是否经过操作系统的调度。多个线程在不同时段获取同一把锁,不存在锁竞争和线程的阻塞。
轻量级锁的加锁:JVM会为每个线程在当前线程的栈帧中创建用于储存锁记录的空间(displaced Mark word),如果线程在获取锁的时候发现是轻量级锁,会将锁的Mark word复制到displaced Mark word中,之后采用cas常识将mark word中的值替换为指向锁记录的指针,成功的话当前线程获得锁;失败说明其他线程已经获取到锁,之后自旋尝试获取锁。
自旋会消费cpu资源,多次的自旋会白白浪费cpu资源,jdk提供了适应性自旋,成功自旋的线程,下次自旋变多,反之变少,在自旋到一定程度还没有获取锁,则这个线程会阻塞,之后将锁升级为重量级锁。
轻量级锁的释放:当前线程会使用cas操作将displaced Mark word中的值赋值给锁的Mark word,如果没有发生竞争,此时复制成功;如果有其他线程因为自旋将锁升级为重量级锁,那么这个cas操作会失败, 此时释放锁并唤醒阻塞线程。
3)自旋锁与适应自旋锁
a)自旋锁
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
自旋锁就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
b)自适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
4)重量级锁(悲观锁)
重量级锁依赖于操作系统的互斥性,而操作系统中线程状态的转换需要时间较长,所以重量级锁的效率很低,但是被阻塞的线程不会消耗cpu。可以通过查看Mark word中锁的标识位和偏向锁标识位,来确定锁的状态
5)锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法。
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
6)锁升级
a、每一个线程在准备获取共享资源的时候,都会先检查Mark word中是不是存的自己的线程ID,是的话说明属于偏向锁。
b、如果MarkWord不是自己的Thread,锁升级,此时,使用cas进行线程的切换,新的线程根据Mark word中的线程ID通知之前的线程暂停,之前的线程将Mark word 的内容变为空。
c、俩个线程都把锁对象的HashCode复制到自己新建的用于存储锁对象的记录空间,接着通过cas操作,把锁对象的Mark word的内容修改为自己新建的记录空间的地址的方式竞争Markword。
d、成功执行CAS的获取资源,失败的进入自旋。
e、自旋的线程在自旋过程中,成功获得资源的,则整个锁的状态依然处于轻量级锁的状态,如果一直没有获得资源,即自旋失败,进入重量级锁,自旋线程阻塞,等待之前线程结束进行唤醒。
2、代码方面对synchronized的优化
1)锁细化
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
2)锁粗化
在大多数的情况下,进行锁细化是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如下实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
四、碎片化知识总结
1、可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。其实简单来说可重入就是一个对象可以上多个锁,在理解的时间可以想象父子类,子类重写父类的synchronized方法,而这个方法中有super的某一个synchronized方法,如果不能重入,出现死锁。代码如下:
package com.zcm.juc;
import java.util.concurrent.TimeUnit;
public class T {
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T {
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
2.线程中断(了解就行)
线程中断的方式有俩种,一种在线程执行方法过程中出现异常,另一种是直接使用interrupt()方法打断线程。在线程中断的,程序报错:InterruptedException。
出现异常时线程中断,其他线程继续执行,代码如下:
/**
* 程序在执行过程中,如果出现异常,默认情况锁会被释放
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
* 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
* 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
* 因此要非常小心的处理同步业务逻辑中的异常
* @author zcm
*/
package com.zcm.juc;
import java.util.concurrent.TimeUnit;
public class T {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while(true) {
count ++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 5) {
int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
第二种方法是直接使用interrupt()方法打断线程。基本没有人用,了解就好,配套方法:
//中断线程(实例方法)
public void Thread.interrupt();//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()
方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),演示代码:
public class InterruputSleepThread3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
//while在try中,通过异常中断就可以退出run循环
try {
while (true) {
//当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
//如果不进行阻塞的话,会发现interrupt不起作用
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
boolean interrupt = this.isInterrupted();
//中断状态被复位
System.out.println("interrupt:"+interrupt);
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
//中断处于阻塞状态的线程
t1.interrupt();
}
}
3、线程唤醒
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
需要注意的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
五、参考文档与致谢
文中如有问题,欢迎指正,谢谢。
《Java编程思想》
《深入理解Java虚拟机》
《实战Java高并发程序设计》