思维导图:
1、乐观锁和悲观锁
1.1、乐观锁
乐观锁
是一种乐观思想,假定当前环境是读多写少
,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。 Java中的乐观锁:CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。 如图所示,可以同时进行读操作,读的时候其他线程不能进行写操作。
乐观锁的实现方式
-
版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
- ABA问题:在CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
- 解决方法:Juc包提供了一个
AtomicStampedReference
,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
-
最常采用的是
CAS算法
,Java原子类中的递增操作就通过CAS自旋实现的。
1.2、悲观锁
写多读少
,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改。悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
悲观锁的实现方式:
-
synchronized
关键字 -
Lock
的实现类都是悲观锁
悲观锁适合写操作
多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
阿里巴巴代码规范
- 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
- 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
2、锁的是什么
2.1、8锁案例
class Phone{//资源类
public static synchronized void sendEmail(){ //发送邮件
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e){e.printStackTrace();}
System.out.println("-------------sendEmail");
}
public synchronized void sendSMS(){ //发送短信
System.out.println("-------------sendSMS");
}
public void hello(){
System.out.println("-------------hello");
}
}
/**
* - 题目:谈谈你对多线程锁的理解,8锁案例说明
* - 口诀:线程 操作 资源类
* 1. 标准访问 a b 两个线程,请问先打印邮件还是短信? 邮件
* 2. sendEmail方法中停3秒,请问先打印邮件还是短信? 邮件
* 3. 添加一个普通的hello方法,请问先打印邮件还是hello? hello
* 4. 两部手机,请问先打印邮件(这里有个3秒延迟)还是短信? 短信
* 5. 两个静态同步方法(synchroized前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?邮件
* 6. 两个静态同步方法(synchroized前加static,3秒延迟也在),有2部手机,先打印邮件还是短信?邮件
* 7. 一个静态同步方法,一个普通同步方法,一部手机,请问先打印邮件还是短信?短信
* 8. 一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信?短信
*/
public class lock8 {//线程类
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒,保证a线程先启动
try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
phone.sendSMS();
//phone.hello();
//phone2.sendSMS();
},"b").start();
}
}
2.2、原理解释
-
1-2中
- 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,
锁的是当前对象this
,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法
- 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,
-
3中
- 普通方法 hello() 并未和其他synchronized修饰的方法产生争抢
-
4 中
- 锁在两个不同的对象/两个不同的资源上,不产生竞争条件
-
5-6中
static+synchronized - 类锁 ; Phone phone = new Phone();中 加到了左边的
Phone
上- 对于
普通同步方法
,锁的是当前实例对象
,通常指this,具体的一部手机,所有的普通同步方法用的都是同一把锁→实例对象本身。 - 对于
静态同步方法
,锁的是当前类的Class对象
,如Phone,class唯一的一个模板。 - 对于
同步方法块
,锁的是synchronized括号内的对象
。
- 对于
-
7-8中
- 一个加了对象锁,一个加了类锁,不产生竞争条件
8锁-3个体现
8种锁的案例实际体现在3个地方
-
作用于
代码块
,对括号里配置的对象加锁。 -
作用于
实例方法
,当前实例加锁,进入同步代码块前要获得当前实例的锁。 -
作用于
静态方法
,当前类加锁,进去同步代码前要获得当前类对象的锁。
2.3、小总结
-
当一个线程
试图访问同步代码时它首先必须得到锁
,正常退出或抛出异常时必须释放锁
; -
所有的
普通同步方法
用的都是同一把锁,也就是实例对象本身
, 本类this也就是说如果一个实例对象的普通同步方法获取锁之后,该实例的其他普通同步方法必须等待获取锁的方法释放锁之后才能获取锁
-
所有的
静态同步方法
用的也是同一把锁,也就是类对象本身
,即唯一模板Class -
具体
实例对象this 和唯一模板Class
,这两把锁是两个不同的对象
,所以静态同步方法与普通同步方法之间不会有竞态条件,但是一旦一个静态同步方法获取锁之后,其他的静态同步方法都必须等待该方法释放锁之后才能获取锁
2.4、字节码角度分析synchronized实现
文件反编译技巧
- 文件反编译
javap -c ***.class
文件反编译,-c表示对代码进行反汇编 - 假如需要更多信息
javap -v ***.class
,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)
synchronized同步代码块
/**
* 锁同步代码块
*/
public class LockSyncDemo {
Object object = new Object();
public void m1(){
synchronized (object){
System.out.println("-----hello synchronized code block");
}
}
public static void main(String[] args) {
}
}
从target中找到LockSyncDemo.class文件,右键,终端中打开,然后执行 javap -c LockSyncDemo.class
D:\hnit_project\LeetCode\target\classes\com\hh\demo>javap -c LockSyncDemo.class
Compiled from "LockSyncDemo.java"
public class com.hh.demo.LockSyncDemo {
java.lang.Object object;
public com.hh.demo.LockSyncDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
public void m1();
Code:
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String -----hello synchronized code block
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
public static void main(java.lang.String[]);
Code:
0: return
}
总结
-
synchronized同步代码块,实现使用的是monitorenter和monitorexit指令(monitorexit可能有两个)
-
那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
synchronized普通同步方法
/**
* 锁普通的同步方法
*/
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static void main(String[] args) {
}
}
- 类似于上述操作,最后调用
javap -v LockSyncDemo.class
.....
public synchronized void m2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //请注意该标志
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String ------hello synchronized m2
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this com/hh/admin/controller/LockSyncDemo;
......
- 总结
- 调用指令将会检查方法的访问标志是否被设置。如果设置了,执行线程将会先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized静态同步方法
/**
* 锁静态同步方法
*/
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static synchronized void m3(){
System.out.println("------hello synchronized m3---static");
}
public static void main(String[] args) {
}
}
......
public static synchronized void m3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //访问标志 区分该方法是否是静态同步方法
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String ------hello synchronized m3---static
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
......
2.5、反编译synchronized锁的是什么
管程
-
管程:
Monitor(监视器)
,也就是我们平时说的锁。监视器锁 -
信号量及其操作原语“封装”在一个对象内部 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
-
执行线程就要求先成功持有管程
,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
为什么任何一个对象都可以成为一个锁?
- 溯源
- Java
Object
类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。 ObjectMonitor.java
→ObjectMonitor.cpp
→objectMonitor.hpp
- Java
ObjectMonitor.cpp
中引入了头文件(include)objectMonitor.hpp
140行
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0;//锁的重入次数
_object = NULL;
_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- 追溯底层可以发现每个对象天生都带着一个
对象监视器
。
3、公平锁和非公平锁
ReentrantLock抢票案例
class Ticket
{
private int number = 30;
ReentrantLock lock = new ReentrantLock();
//ReentrantLock lock = new ReentrantLock(true);
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class SaleTicketDemo
{
public static void main(String[] args)
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"c").start();
}
}
非公平锁
-
默认是非公平锁
-
非公平锁可以插队,买卖票不均匀。
-
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成
优先级翻转
或饥饿的状态
(某个线程一直得不到锁)。在卖票这个案例中,c线程可能会一直操作资源 ,ab两线程会出现锁饥饿 。
公平锁
-
ReentrantLock lock = new ReentrantLock(true);
-
买卖票一开始a占有,后面a b c a b c a b c均匀分布
-
是指多个线程按照
申请锁的顺序
来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。
为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
-
恢复挂起的线程到真正锁的获取
还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片
,尽量减少 CPU 空闲状态时间。 -
使用多线程很重要的考量点是
线程切换的开销
,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点
,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量
,很显然非公平锁是比较合适的,因为节省很多线程切换时间
,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
4、可重入锁(又名递归锁)
-
可重入锁是指
同一个线程
在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。 -
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是很搞笑。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
4.1、“可重入锁”详细解释
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)
- 一句话:一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入。自己可以获取自己的内部锁。
4.2、可重入锁种类
隐式锁Synchronized
synchronized
是java中的关键字,默认是可重入锁,即隐式锁
在同步块中
public class ReEntryLockDemo {
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
//-----外层调用
//-----中层调用
//-----内层调用
在同步方法中
public class ReEntryLockDemo
{
public synchronized void m1()
{
//指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
System.out.println(Thread.currentThread().getName()+"\t"+"-----come in m1");
m2();
System.out.println(Thread.currentThread().getName()+"\t-----end m1");
}
public synchronized void m2()
{
System.out.println("-----m2");
m3();
}
public synchronized void m3()
{
System.out.println("-----m3");
}
public static void main(String[] args)
{
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
/**
* main -----come in m1
* -----m2
* -----m3
* main -----end m1
*/
Synchronized的重入实现机理
- 回看上方的
ObjectMoitor.hpp
140行
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0;//锁的重入次数
_object = NULL;
_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
-
ObjectMoitor.hpp底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
_count _owner
-
首次加锁
:当执行monitorenter
时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
。 -
重入
:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1
,否则需要等待,直至持有线程释放该锁。 -
释放锁
:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1
。计数器为零代表锁已被释放。
显式锁Lock
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁
注意:lock
unlock
要成对
public class ReEntryLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
{
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
}
}
}
//t1 ----come in 外层调用
//t1 ------come in 内层调用
假如lock
unlock
不成对,单线程可以,但多线程下会出问题
public class ReEntryLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
} finally {
lock.unlock();
}
} finally {
//lock.unlock();//-------------------------不成对 多线程情况
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("t2 ----外层调用lock");
}finally {
lock.unlock();
}
},"t2").start();
}
}
//t1 ----come in 外层调用
//t1 ------come in 内层调用
//(t2 ----外层调用lock 假如不成对,这句话就不显示了)
5、死锁
5.1、死锁是什么
-
死锁
-
是指两个或两个以上的线程在执行过程中,因
争夺资源而造成的一种互相等待的现象
,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。 -
a跟b两个资源互相请求对方的资源
-
- 死锁产生的原因
- 系统资源不足
- 资源分配不当
- 进程运行推进的顺序不合适
5.2、死锁代码case
public class DeadLockDemo {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
new Thread(()->{
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"\t 持有a锁,想获得b锁");
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//使得线程b也启动
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"\t 成功获得b锁");
}
}
},"A").start();
new Thread(()->{
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"\t 持有b锁,想获得a锁");
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"\t 成功获得a锁");
}
}
},"B").start();
}
}
5.3、如何排查死锁
纯命令
jps -l
查看当前进程运行状况jstack 进程编号
查看该进程信息
图形化
win
+r
输入jconsole
,打开图形化工具,打开线程
,点击检测死锁
。
5.4、小总结
指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)
锁的知识先聊到这里,若是想全面了解,可以查看我老大的博客: