终于明白了JavaAPI里面的锁
锁的出现: 第一次接触到锁的概念是在java多线程遇见的锁,再写多线程用到了synchronized和lock两种锁,采用锁是为了保证线程的安全,每个线程都存在自己私有和共有的数据区,私有的数据区只对内开放,如果另一个线程A需要访问到线程B的私有数据时,直接访问是不可达的,此时需要线程B将自己的数据刷新到线程共享的数据区,此时线程A再去将线程共享区的数据加载刷新到自己的私有数据区。所以不难发现线程安全问题主要在于线程的共享数据区,如果一个线程C将A线程获取的值修改了,那么A线程获取的就是错误信息,所以需要对线程synchronized加锁,意味着共享数据区同时只能一个线程进行操作。
锁的分类
在java语言里面有很多锁的定义,例如:
1.乐观锁: 是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入
值是否一样,一样则更新,否则失败。
2.悲观锁: 是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。
3.自旋锁: 原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程
也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁
的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
4.公平锁/非公平锁(后文提及,此处不过多介绍)
锁的状态:无锁状态、偏向锁、轻量级锁、重量级锁,随着锁的竞争,锁的状态可以升级,升级的方向是单向的,无锁状态——偏向锁——轻量级锁——重量级锁。(主要针对synchronized锁的优化)
互斥锁(排它锁)
互斥锁的实现通常有两种方式:
synchronized(同步方法块)和 ReentrantLock(可重入锁)(比较)
1.可重入性
如下代码,类似于嵌套使用锁,synchronized和ReentrantLock都支持重入性
synchronized public void add() {
i+=10;
}
synchronized public void delete() {
add();
}
2.锁的释放
每个对象(非NULL)都可以充当锁的对象(后文细说),而且锁只能被持有者释放,所有锁都满足的性质。当然除非当前线程发生异常,异常时synchronized锁会被JVM自动释放,不会死锁,而Lock必须调用unlock释放锁,计算发生异常时也会一种持有锁。其实可以把锁看成是一种资源。
3.锁的申请
在锁申请的时候ReentrantLock可以设定申请时间,在一定时间内还未获取锁时就放弃获取,不会一直造成线程阻塞lock也可以通过isLocked()去获取锁的状态,而synchronized会一直等待下去,会造成线程阻塞。
4.锁的中断
ReentrantLock锁可以通过lockInterruptibly()方法,支持线程中断,停止获取锁,synchronized不支持中断。
5.是否支持公平锁/非公平锁
所谓的公平锁,意味着当前锁释放之后,锁的下一个获得者就是锁等待队列中的第一个元素,意味着先到先获取锁,相对来讲是公平的。而非公平锁就是考验每个线程的运气了(哈哈哈),ReentrantLock在创建时就可以设置为公平锁或者是非公平锁,而synchronized不支持。
重头戏synchronized的底层实现原理
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。可以将synchronized修饰的方法、变量、代码块看成是一个单线程操作。
下列锁的几种方式:
public synchronized void increase(){//修饰普通的方法,此时获取的锁是当前对象的锁
i++;
}
public static synchronized void increase(){//修饰静态方法,此时获取的锁对象是类class对象的锁
i++;
}
synchronized(instance){//同步代码块,使用同步代码块对变量i进行同步操作,锁对象为instance
//如果当前有其他线程正持有该对象锁,那么新到的线程就必须等
//待,这样也就保证了每次只有一个线程执行i++;操作。当然除了
//instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁
for(int j=0;j<1000000;j++){
i++;
}
}
通过下面代码测试,可以得出increaseOne和increaseTwo并不是互斥的,而是并发执行,因为锁的对象不是同一个锁对象。increaseOne锁住的是this对象的锁对象,而increaseTwo是TestSynchronized .class的锁对象。因此通过class对象锁可以控制静态 成员的并发操作。
需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态
synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
package 每日一讲;
public class TestSynchronized implements Runnable{
private static int i;
public static synchronized void increaseTwo() {
i++;
}
public synchronized void increaseOne() {
i++;
}
public void run() {
for(int i=0;i<10000;i++) {
//increaseTwo();
increaseTwo();
increaseOne();
//increaseOne();
}
}
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
TestSynchronized ts = new TestSynchronized();
Thread t1 = new Thread(ts);
Thread t2 = new Thread(ts);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);//此种情况输出的值小于40000
}
}
synchronized 作用范围总结:
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized 的底层
实现模型:
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner:当前已经获取到所有资源的线程被称为Owner;
!Owner:当前释放锁的线程。
synchronized 是java里面的一个关键字,看不见太多的源码状况,而且synchronized 底层使用c++语言实现的,我可以通过反汇编的形势去查看部分原理。
在讲之前我们首先得明白在JMV里一个对象得结构是什么?(讨论为什么非NULL对象可以充当锁对象)
一个实例对象是存储在堆内存中,堆又是线程共享得区域,对象得引用一般是在栈中。对象在堆里的结构如下图所示:
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
实例变量: 存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可
重点研究对象头的Mark Word:
HotSpot虚拟机对象头部分包含两类信息,第一类是存储自身得运行时数据区,如hash码、GC分代、锁状态标志、线程持有锁得状态、偏向线程ID、偏向时间戳等。另一部分时类型指针,及对象指向它得类型元数据得指针,Java虚拟机通过这个指针来确定该对象属于哪个实例。
原理: java虚拟机中的synchronized 是基于进入和退出(monitorenter 和 monitorexit )管理(monitor管理或监视器锁)对象来实现的,由方法调用指令读取运行时常量池中的ACC_SYNCHRONIZED 标志隐式实现。
synchronized 属于重量级锁,由上表可以看出锁的标识位是10,指针指向的便是monitor对象的起始地址,所以每个对象都与monitor相关联,当一个monitor被某个线程持有后,该线程就处于锁定状态,所有我们通常所说的获取锁对象就是间接的获取Monitor对象。在HotSpot虚拟机中,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 ;
}
对应上面的实现模型图,可以得知_owner 表示持有ObjectMonitor对象的线程,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1。
测试代码,验证结论:
这是上面测试代码的反汇编:看不出效果!!!
Compiled from "TestSynchronized.java"
public class TestSynchronized implements java.lang.Runnable {
public TestSynchronized();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static synchronized void increaseTwo();
Code:
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
public synchronized void increaseOne();
Code:
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
public void run();
Code:
0: iconst_0
1: istore_1
2: iload_1
3: sipush 10000
6: if_icmpge 22
9: invokestatic #3 // Method increaseTwo:()V
12: aload_0
13: invokevirtual #4 // Method increaseOne:()V
16: iinc 1, 1
19: goto 2
22: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: new #5 // class TestSynchronized
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: new #7 // class java/lang/Thread
11: dup
12: aload_1
13: invokespecial #8 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
16: astore_2
17: new #7 // class java/lang/Thread
20: dup
21: aload_1
22: invokespecial #8 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
25: astore_3
26: aload_2
27: invokevirtual #9 // Method java/lang/Thread.start:()V
30: aload_3
31: invokevirtual #9 // Method java/lang/Thread.start:()V
34: aload_2
35: invokevirtual #10 // Method java/lang/Thread.join:()V
38: aload_3
39: invokevirtual #10 // Method java/lang/Thread.join:()V
42: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
45: getstatic #2 // Field i:I
48: invokevirtual #12 // Method java/io/PrintStream.println:(I)V
51: return
}
第二个案例:(测试代码块底层原理)
为了验证进入同步代码块前后的变化
package 每日一讲;
public class SyncCodeBlock {
public int i;
public void syncTask() {
//同步代码块
synchronized (this) {
i++;
}
}
}
反汇编之后:
Compiled from "SyncCodeBlock.java"
public class SyncCodeBlock {
public int i;
public SyncCodeBlock();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void syncTask();
Code:
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 //正常情况下跳转到24行,运行结束
19: astore_2
20: aload_1
21: monitorexit//退出同步方法,异常结束时对monitor对象的释放
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
测试方法底层的原理
package 每日一讲;
public class SyncMethod {
public int i;
public synchronized void syncTask() {
i++;
}
}
反汇编:
Compiled from "SyncMethod.java"
public class SyncMethod {
public int i;
public SyncMethod();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
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访问标志来辨别一个方法是否声明为同步方法。
共享锁
按照锁的互斥性来区分锁的话,还有一类锁叫共享锁。共享锁其实就是在同一时间可以被多个线程线程申请,使用最多的场景就是读写锁。
在生活中一些商城,只摆放了部分的商品展示出来(规格不同,外观相同),避免对大量仓库进行访问,此时所有顾客访问只能观看,并不能带走。有的时候通过MAP来缓存商品部分信息供给顾客访问,相当于一个缓存区,并不携带过多商品信息。那么此时便可以供更多的顾客去访问,但是不能进行修改,多个线程查看缓存信息是非常安全的。
共享锁的金典应用场景是在读写锁上,又知道读操作并不会影响到数据的安全性,所以读的时候可以考虑不必加锁,只有当读和写一起到来时,此时便要进行加锁,共享锁的目的就是在于控制读和写不能同时进行操作。
此时读操作便是共享锁,而写操作只能是互斥锁。
锁的实现原理
从上synchronized获取锁资源的流程可以看出锁的原理就在于两个队列上,一个阻塞队列和条件等待队列。正如上面说讲,一个线程获取到锁资源之后,会将竞争这一把锁的其他线程,存放到阻塞队列中。而如果在运行时调用了wait方法,该线程将会处于等待状态,便会进入条件等待队列,等待其他线程的唤醒,此时释放自己持有的锁。
Object.wait方法,会使当前线程进入等待状态,并且释放锁。
通常条件等待会使用while语句,避免条件不满足时被误唤醒,故使用while对条件进行再一次的判断。
当被唤醒后,并不立即去执行while条件判断,而是需要重新去申请锁,即可能会进入到阻塞队列。
锁的优化
关于锁的优化原则就是非必要不建议使用锁!!采用锁会严重的影响到CPU的执行效率。
1.减少锁的持有时间,降低其他线程阻塞等待的时间,需要加锁的代码块进行加锁,有时候没必要将整个方法加锁或者整个类加锁。
2.减小锁的粒度,将大的对象拆分成小的对象,降低并行度和锁的竞争。锁的竞争低了,才会使偏向锁、轻量级锁的执行概率增加。最典型的ConcurrentHashMap。
3.锁分离,例如读写锁,读读共享锁,读写和写读互斥。
4.锁消除,主要是编译器处理,JIT如果发现不可能被共享的对象,就可以消除这些对象的锁操作,例如在单线程操作下,使用StringBuffer时,编译器就会进行消除,不存在安全问题时。
5.使用锁时考虑执行效率,例如RockMq消息中间件,同时一个文件写入时,需要记录当前位置,然后另一个线程追加,此时写入位置并不安全,所有消息中间件采用控制锁的范围,确保了被锁包含的代码执行效率高,例如写入内存,耗时低。