目录
2.3 wait(long n) 和 sleep(long n)区别
1、Monitor(管程)
1.1 Java对象头
一个Java对象的结构:对象头、实例数据、对其填充
32位虚拟机下,Java对象头结构,其中Klass Word
是一个指针,指向对应的class对象,来确定我们对象的类型。
普通对象:
数组对象:多一个数组长度标记
其中Mark Word
结构为:
1.2 Monitor原理
管程,指的是管理共享变量以及对共享变量的操作过程,让它们支持并发。也就是管理类的成员变量和成员方法,让这个类是线程安全的。
synchronized
关键字和wait(),notify(),notifyAll()
这三个方法的背后都是基于管程实现的。每个Java对象都对应一个Monitor
对象(这是操作系统的对象,Java层面看不到),当使用 synchronized 给对象上锁(重量级),这个Java对象就会关联到这个Monitor
对象(将该对象的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必须锁的是同一个对象才有效。
1.3 synchronized原理
synchronized
的三个主要作用:
1)原子性:确保线程互斥的访问同步代码
2)可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的
3)有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”.
直接锁对象时的反编译:
static final Object lock=new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
0 getstatic #2 <com/concurrent/test/Test17.lock>
# 取得lock的引用(synchronized开始了)
3 dup
# 复制操作数栈栈顶的值放入栈顶,即复制了一份lock的引用
4 astore_1
# 操作数栈栈顶的值弹出,即将lock的引用存到局部变量表中
5 monitorenter
# 将lock对象的Mark Word置为指向Monitor指针
6 getstatic #3 <com/concurrent/test/Test17.counter>
9 iconst_1
10 iadd
11 putstatic #3 <com/concurrent/test/Test17.counter>
14 aload_1
# 从局部变量表中取得lock的引用,放入操作数栈栈顶
15 monitorexit
# 将lock对象的Mark Word重置,唤醒EntryList
16 goto 24 (+8)
# 下面是异常处理指令,可以看到,如果出现异常,也能自动地释放锁
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
1.monitorenter:线程执行monitorenter指令时尝试获取monitor的所有权
2.monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
锁方法时的反编译:
package com.paddx.test.concurrent;
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
不会在字节码指令中体现! 不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED
标示符
1.4 synchronized进阶原理
1)轻量级锁
应用场景:
虽然有多个线程要对一个对象进行加锁,但是加锁的时间是错开的(也就是它们不存在竞争),这时候可以使用轻量级锁来优化。因为CAS存在一定的开销,因此在有竞争的情况下,轻量级锁反而比传统的重量级锁更慢。轻量级锁对使用者是透明的,其语法仍然是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。
②首先,让锁记录中的对象引用指向该对象,并且尝试使用cas替换object对象的Mark Word
③如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 表示轻量级锁
④如果cas替换失败,会有两种情况:
a.如果是其它线程已经持有了该对象的轻量级锁,那么表示有其它线程竞争,将进入锁膨胀阶段
b.如果是自己的线程已经持有了该锁(锁重入),那么再添加一条Lock Record作为重入的计数(虚拟机会首先会见检查对象的Mark Word是否指向当前线程的栈帧,那直接进入同步块继续执行就好了)
释放锁过程:
当线程退出 synchronized 代码块的时候:
①如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
②如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
a.成功则解锁成功
b.失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
2)锁膨胀
如果在尝试加轻量级锁的过程中,cas 操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
锁膨胀过程:
①当Thread-1进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。
②这时 Thread-1 就会加轻量级锁失败,然后进入锁膨胀流程:
a.首先,为对象申请Monitor锁(也就是之前的关联操作,将Mark Word指向重量级锁),让Object指向这个重量级的锁
b.然后,自己进入Monitor 的EntryList(等待队列) 变成BLOCKED状态。
③这时,当Thread-0 退出 synchronized 同步块时,发现对象头指向Monitor,就会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 Thread-1 线程
3)自旋优化
应用场景:
当重量级锁竞争的时候,可以使用自旋来优化。也就是如果当前线程要访问一个同步代码块,而这时另外一个线程已经持锁在执行了,当前线程会先循环访问几圈(自旋),如果自旋成功,那么当前线程就可以不用线程上下文切换就获得了锁(提高性能)。当然如果锁被占用的时间越短,自旋的效果越好,因为自旋是会白白消耗处理器资源的。
因为挂起线程和恢复线程的操作都需要转入内核态来完成,会大大影响性能!
①自旋重试成功
②自旋重试失败:还会进入阻塞状态
注意:
①自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
②在 Java 6 之后自旋锁是自适应的,自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
③Java 7 之后不能控制是否开启自旋功能、
4)偏向锁
为了进一步提升性能,java6开始,只有第一次使用 CAS(轻量级锁那块) 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。(减少了CAS次数)
主要作用是优化同一个线程多次获取一个锁的情况
偏向状态
偏向锁状态时Mark Word的格式
一个对象创建过程:
①如果开启了偏向锁(默认是开启的),那么对象刚创建之后,它的Mark Word最后三位的值就是101,并且这时它的 Thread,epoch,age 都是 0 ,在加锁的时候才会去设置这些值。
②另外注意的是偏向锁是有延迟的:它不会在程序启动的时候就立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0
③另外还需要注意的是:处于偏向锁的对象解锁后,线程ID仍存储在Mark Word中。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置位”01”,偏向模式设置为“1”。也就是“101”,表示进入偏向模式。并且采用CAS的方式把获取到这个锁的线程ID记录在对象的Mark Word中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作
使偏向锁失效的几种情况:
①调用了对象的HashCode方法:其中的原因就是偏向锁状态的Mark Word无法再存放31位的HashCode了,而轻量级锁是把HashCode存放到线程栈帧的锁记录中,重量级锁存放到Monitor中。
②多个线程使用该对象:当有另外的线程来访问加了偏向锁的对象时,会升级为轻量级锁。
当一个线程来之后,如果发现对象被锁定,那么就升级为轻量级锁;如果发现这个对象没被锁定,已经解锁了,那就撤销偏向或者重偏向。
③调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁):wait/notify 方法这种方法只有重量级锁才有,所有会升级锁!
批量重偏向
如果对象被多个线程访问,但是没有竞争,这时候偏向了线程1的对象是有机会再重新偏向线程2的,也就不需要撤销再升级为重量级锁。其发生的条件是:
超过20个对象对同一线程如线程1撤销偏向时,那么第20个及以后的对象才可以将撤销对线程1的偏向这个动作变为批量将第20个及以后的对象偏向线程2.
· 批量撤销
当撤销偏向锁的阈值超过 40 以后,就会将整个类的对象都改为不可偏向的。
5)锁消除
JIT即时编译器会给热点代码,进行字节码优化。
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持;如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
为什么要进行锁消除?
有许多同步措施并不是我们程序员自己加入的,同步的代码在Java程序中出现的频繁程度也许超过了我们的想象。
例如:
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作:
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
上面每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象,但是即时编译之后,会消除掉这个锁。
6)锁粗化
应用场景:
当加锁的范围太小时,可能会导致对同一个对象反复加锁和解锁,这样也会大大损耗性能;因此这时候,就需要考虑扩大锁的范围,减少加锁的次数!!
7)总结
对于 synchronized 锁来说,锁的升级主要是通过 Mark Word 中的锁标记位于线程是否竞争来实现的!
synchronized
关键字对对象的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成了重量级锁。
偏向锁是为了优化一个线程锁重入的情况。当一个线程执行了一个 synchronized 方法的时候,会优先加上偏向锁(如果允许的话),这个方法所在的对象就会在 Mark Work 处设为偏向锁标记;当这个线程再次访问同一个 synchronized 方法的时候,就会检查这个对象的 Mark Word 的偏向锁标记,再判断一下这个字段记录的线程 ID 是不是和我们这个相同,如果相同,就无需再CAS操作了。如果不同,则会升级为轻量级锁。
2、wait-notify(等待-通知机制)、
2.1 wait-notify原理
1)Owner线程发现有条件不满足时,调用wait
方法,即可以进入WaitSet
变为WAITING
状态。(线程6种状态之一)
2)处在BLOCKED
和WAITING
状态的线程都处于阻塞状态,不占用CPU的时间片。
3)BLOCKED
线程会在Owner线程释放时被唤醒;WAITING
线程会在Owner线程调用notify
或者notifyAll
时被唤醒,但其唤醒之后并不是立刻获得锁,仍需要重新进入EntryList队列种重新进行竞争。
注意:wait-notify只有在获得对象锁(重量级锁)时才能用!
2.2 相关API
-
obj.wait()
让进入 object 监视器的线程到 waitSet 等待 -
obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒 -
obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
wait()
是无限制等待;wait(long n)
是有时限的等待(n毫秒)。
2.3 wait(long n) 和 sleep(long n)区别
1)Sleep 是 Thread 类的静态方法;Wait 是 Object 的方法,Object 又是所有类的父类,所以所有类都有Wait方法。
2)Sleep 在阻塞的时候不会释放锁,而 Wait 在阻塞的时候会释放锁,它们都会释放 CPU 资源。
3)Sleep 不需要与 synchronized 一起使用,而 Wait 需要与 synchronized 一起使用(对象被锁以后才能使用)
4)线程状态都是TIMED_WAITING
final知识:
final修饰的对象,其引用只能指向该对象!指向不能变,但是对象内部的属性是可以变的!
2.4 wait-notify的使用
1)当线程不满足某些条件,需要暂停运行时,可以使用wait。这样会将对象的锁释放,让其他线程能够继续运行。而如果此时使用的是sleep,会导致所有线程都进入阻塞(因为sleep不会释放锁),导致所有线程还是都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行!
2)当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法,而且需要注意的是我们在wait时,要用while(解决虚假唤醒)防止被唤醒后条件还是不满足,这样就可以多次判断!
synchronized (lock) {
while(//不满足条件,一直等待,避免虚假唤醒) {
lock.wait();
}
//满足条件后再运行
}
//另一个线程
synchronized (lock) {
//唤醒所有等待线程
lock.notifyAll();
}
3、park&unpark
3.1 基本使用
它们是LockSupport类中的方法:
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
park之后,线程处于WAITING
状态!
特点:与Object的wait-notify的区别:
1)wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
2)park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,不是那么精确
3)park & unpark 可以先 unpark,而 wait & notify 不能先 notify.
3.2 park & unpark原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter, _cond 和 _mutex
两种情况:
1.先调用park,再调用unpark的过程
1)先调用park
①当前线程调用了Unsafe.park() 方法
②检查 _counter ,发现其为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
③线程进入 _cond 等待队列阻塞
④设置_counter = 0
2)再调用unpark
①调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
②唤醒 _cond 等待队列中的 Thread_0
③Thread_0 恢复运行
④设置 _counter 为 0
2. 先调用upark再调用park的过程
①先调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
②然后,当前线程调用 Unsafe.park() 方法
③检查 _counter ,发现已经为 1,这时线程无需阻塞,继续运行
④将 _counter 设置为 0
4、活跃性问题
并发编程需要注意三个问题:安全性问题、活跃性问题和性能问题
其中,安全性问题也就是之前我们说到的多个线程同时读写共享资源时的情况;而活跃性问题,指的是某个操作无法执行下去。我们常见的“死锁”就是一种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”。
4.1 死锁
死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
一般情况下,使用细粒度的锁虽然可以提高并行度,提升效率,但也容易发生死锁;或者说一个线程需要获得多把锁时,就容易产生死锁现象。
public static void main(String[] args) {
final Object A = new Object();
final Object B = new Object();
new Thread(()->{
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
}
}
}).start();
new Thread(()->{
synchronized (B) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
}
}
}).start();
}
1)死锁产生的条件
只有以下这四个条件都发生时才会出现死锁:
①互斥:共享资源 X 和 Y 只能被一个线程占用
②占有且等待:线程 t1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
③不可抢占:其他线程不能强行抢占线程 t1 占有的资源
④循环等待:线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源,就是循环等待
所以说只要破坏上述其中一个条件,就可以成功的避免死锁!
2)防止死锁办法
互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。但是其它三个条件是有办法被破坏的:
①破坏占有且等待条件
理论上讲,要破坏这个条件,我们可以一次性申请所有资源。
②破坏不可抢占条件
破坏不可抢占条件的核心是要能够主动释放它占有的资源,但这一点 synchronized 是做不到的。需要依赖于java.util.concurrent
这个包下面提供的 Lock。(ReentranLock)
③破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源。
3)定位死锁
检测死锁可以使用 jconsole工具;或者使用 jps 定位进程 id,再用 jstack 根据进程 id 定位死锁。
4.2 活锁
活锁:活锁出现在两个线程互相改变对方的结束条件,谁也无法结束。(有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况)
例子:路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”
解决活锁的办法:
等待一个随机时间!
死锁和活锁区别:
-
死锁是因为线程互相持有对象想要的锁,并且都不释放,最后线程阻塞,停止运行的现象。
-
活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。(没有阻塞)
4.3 饥饿
饥饿:某些线程因为优先级太低,导致一直无法获得资源的现象。
5、ReentranLock
5.1 基本语法
// 获取ReentrantLock对象;对象级别的锁
private ReentrantLock lock = new ReentrantLock();
// 加锁
lock.lock();
try {
// 需要执行的代码
}finally {
// 释放锁
lock.unlock();
}
5.2 特点
相对于synchronized
:
1)可中断:它可以被中断;而synchronized不可以
2)可以设置超时时间:在等待队列中等待一定时间后,放弃争抢锁,可以去执行一些其它的洛逻辑;
3)可以设置为公平锁:防止线程饥饿的现像(synchronized是非公平锁)
4)支持多个条件变量: Lock有多个WaitSet,不同的条件可以到不同的WaitSet中等待;而synchronized只有一个WaitSet,不同条件不满足了,都到一个WaitSet中等待。
5)支持可重入:与synchronized一样。
1)可重入
可重入锁:同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
不可重入锁:在第二次获得锁的时候自己也会被锁住
2)可打断
可打断:如果某个线程处于lock阻塞状态,其它线程可以调用interrupt
方法让其停止阻塞,获得锁失败(也就是处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行)
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 加锁,可打断锁
//如果有竞争,就会进入阻塞队列,可以被其它线程打断!!!
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
// 被打断,返回,不再向下执行
return;
}finally {
// 释放锁
lock.unlock();
}
});
lock.lock();
try {
t1.start();
Thread.sleep(1000);
// 打断
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
3)锁超时
①lock.tryLock()
方法尝试获得锁,会返回获取锁是否成功,true和false。
②lock.tryLock(long timeout, TimeUnit unit)
方法可以指定等待时间,其中参数long timeout为等待时间,TimeUnit 为时间单位。也就是超过一定时间一直没有获得锁,那么就返回false获得锁失败。
尝试获得锁失败:
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();
}
}
这个tryLock()就可以主动释放自己手中的锁,来避免死锁的情况!
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();//会释放锁资源的
}
}
}
}
4)公平锁
ReentranLock
默认也是不公平的!
公平锁:先到等待队列的,先拿到锁。
// 默认是不公平锁,需要在创建时指定为公平锁 ReentrantLock lock = new ReentrantLock(true);
注意:一般不会这么设置,会降低并发度!
5)条件变量
1)synchronized
中的条件变量就是那个WaitSet休息室,当条件不满足时进入WaitSet等待
2)ReentranLock
的条件变量则支持多个条件变量(一个ReentrantLock对象可以同时绑定多个Condition对象)。意思就是:
-
synchronized 是那些不满足条件的线程都在一间休息室等消息
-
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用:
-
await
前需要获得锁(对应wait) -
await执行后,会释放锁,然后进入condtionObject等待
-
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁(signal()、signalAll()和notify和notifyAll是等价的,只不过signal()唤醒的是一个condtionObject(休息室)中的任意一个线程)
-
竞争 lock 锁成功后,从 await 后继续执行