乐观锁和悲观锁
悲观锁
- 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- 悲观锁的实现方式
synchronized
关键字
Lock
的实现类都是悲观锁
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public synchronized void m1() { }
ReentrantLock lock = new ReentrantLock(); public void m2() { lock.lock(); try { }finally { lock.unlock(); } }
|
乐观锁
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁的实现方式
- 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
- ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
- 解决方法:Juc包提供了一个
AtomicStampedReference
,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
- 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升。
1 2 3 4
|
private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet();
|
从8种情况演示锁的案例,看看我们到底锁的是什么
8锁案例
- 阿里巴巴代码规范
- 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
- 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
8锁演示
8锁案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 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"); } }
public class lock8 { public static void main(String[] args) { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(()->{ phone.sendEmail(); },"a").start();
try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{ phone.sendSMS(); },"b").start(); } }
|
8锁原理
- 1.2中
- 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法
- 3中
- hello并未和其他
synchronized
修饰的方法产生争抢
- 4 中
- 锁在两个不同的对象/两个不同的资源上,不产生竞争条件
- 5.6中
static
+synchronized
- 类锁 **** phone = new Phone();
中 加到了左边的Phone上
- 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁→实例对象本身。
- 对于静态同步方法,锁的是当前类的Class对象,如Phone,class唯一的一个模板。
- 对于同步方法块,锁的是synchronized括号内的对象。
synchronized(o)
- 7.8中一个加了对象锁,一个加了类锁,不产生竞争条件
8锁-3个体现
- 8种锁的案例实际体现在3个地方-相当于总结
- 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁。
- 作用于代码块,对括号里配置的对象加锁。
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
字节码角度分析synchronized实现
文件反编译技巧
- 文件反编译
javap -c ***.class
文件反编译,-c表示对代码进行反汇编
- 假如需要更多信息
javap -v ***.class
,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)
synchronized同步代码块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
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文件,右键,open in terminal,然后
javap -c LockSyncDemo.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class com.zhang.admin.controller.LockSyncDemo { java.lang.Object object;
public com.zhang.admin.controller.LockSyncDemo(); Code: 0: aload_0 1: invokespecial #1 4: aload_0 5: new #2 8: dup 9: invokespecial #1 12: putfield #3 15: return
public void m1(); Code: 0: aload_0 1: getfield #3 4: dup 5: astore_1 6: monitorenter 7: getstatic #4 10: ldc #5 12: invokevirtual #6 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同步代码块,实现使用的是
moniterenter
和moniterexit
指令(moniterexit
可能有两个)
- 那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
synchronized普通同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public class LockSyncDemo {
public synchronized void m2(){ System.out.println("------hello synchronized m2"); }
public static void main(String[] args) {
} }
|
- 类似于上述操作,最后调用
javap -v LockSyncDemo.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ..... public synchronized void m2(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 3: ldc #3 5: invokevirtual #4 8: return LineNumberTable: line 11: 0 line 12: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/zhang/admin/controller/LockSyncDemo; ......
|
- 总结
- 调用指令将会检查方法的****访问标志是否被设置。如果设置了,执行线程会将先持有monitore然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized静态同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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) {
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ...... 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 3: ldc #5 5: invokevirtual #4 8: return LineNumberTable: line 15: 0 line 16: 8 ......
|
- 总结
- ******, ******访问标志区分该方法是否是静态同步方法。
反编译synchronized锁的是什么
概念-管程
- 管程概念
- 管程:Monitor(监视器),也就是我们平时说的锁。监视器锁
- 信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
- 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
大厂:为什么任何一个对象都可以成为一个锁?
- 溯源
- Java
Object
类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
ObjectMonitor.java
→ObjectMonitor.cpp
→objectMonitor.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ObjectMonitor.cpp`中引入了头文件(include)`objectMonitor.hpp 140行 ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
|
- 追溯底层可以发现每个对象天生都带着一个对象监视器。
提前熟悉锁升级
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位
公平锁和非公平锁
ReentrantLock抢票案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Ticket { private int number = 30; ReentrantLock lock = new ReentrantLock();
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(); } }
|
非公平锁
- 默认是非公平锁
- 非公平锁可以插队,买卖票不均匀。
- 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)
公平锁
ReentrantLock lock = new ReentrantLock(true);
- 买卖票一开始a占优,后面a b c a b c a b c均匀分布
- 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。
为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
AQS提前了解
可重入锁(又名递归锁)
可重入锁说明
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”详细解释
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)
- 一句话:一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入。自己可以获取自己的内部锁。
可重入锁种类
隐式锁Synchronized
Synchronized的重入实现机理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 140行 ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _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这样的可重入锁
感觉所谓的显式隐式即是指显示/隐式的调用锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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(); } } }
|
- 假如
lock
unlock
不成对,单线程情况下问题不大,但多线程下出问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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 { } }, "t1").start();
new Thread(() -> { lock.lock(); try { System.out.println("t2 ----外层调用lock"); }finally { lock.unlock(); } },"t2").start();
} }
|
死锁及排查
死锁是什么
- 死锁
- 是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
- a跟b两个资源互相请求对方的资源
- 死锁产生的原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
请写一个死锁代码case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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();} 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(); } }
|
如何排查死锁
纯命令
jps -l
查看当前进程运行状况
jstack 进程编号
查看该进程信息
图形化
win
+ r
输入jconsole
,打开图形化工具,打开线程
,点击 检测死锁
。
小总结-重要
- 指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)