语法:
synchronized(锁对象){
临界区
}
• synchronized加在成员方法上锁住的是this对象,
• synchronized加在静态方法上锁住的是类对象(xxx.Class)
特性:
• 可重入:synchronized的锁对象中维护了一个计数器(recursions),会记录线程获得锁的次数。一个线程多次执行synchronized,可以重复获取同一把锁。好处是可以避免死锁。
• 不可中断:一个线程获得锁后,另一个线程想要获得锁,必须处于等待或阻塞状态,并且在等待或阻塞的过程中不可被中断
底层原理:
synchronized的锁对象会关联一个monitor监视器,它是由JVM创建的一个C++对象,当线程进入同步代码块时就会执行monitorenter指令,如果锁对象没有关联monitor,就会创建一个monitor并与其关联。monitor内部有两个重要的成员变量owner和recursions,owner记录了拥有这把锁的线程,recursions记录了线程获取锁的次数。当一个线程获得了monitor之后,其他线程只能等待。当线程执行完同步代码块之后会执行monitorexit指令,此时recursions减1,当减到0时,当前线程释放锁,这时其他被这个monitor阻塞的线程便可以去尝试获取这个monitor的所有权。即使同步代码块中出现异常,也会自动释放锁。
monitor的结构:
ObjectMonitor (){
_header = NuLL;
_count =0;
_waiters = 0;
_recursions = 0; // 线程的重入次数
_object = NULL; // 存储该monitor的对象
_owner = NULL; // 标识拥有该monitor的线程
_waitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 多线程竞争锁时的单向列表
FreeNext = NULL;
_EntryList = = NULL; // 处于b1ock状态的线程,会被加入到该列表
_SpinFreq = 0;
_spinClock = 0;
OwnerIsThread = 0;
}
monitor的原理:
当第一个线程到来时,发现并没有线程持有对象锁,它会直接成为活动线程,进入 RUNNING 状态。
接着又来了几个线程,要争抢对象锁。此时,这些线程发现锁已经被占用了,就先进入 EntryList 缓存起来,进入 BLOCKED 状态。
处于活动状态的线程执行完毕退出了,或者由于某种原因执行了wait 方法,释放了对象锁,就会进入 WaitSet 队列。
这时EntryList里的线程(只会唤醒一个线程)重新争抢对象锁,成功抢到锁的线程成为活动线程
而在 WaitSet中 的线程,执行了锁的 notify 或者 notifyAll 命令之后会转移到 EntryList 中,重新进行锁的争夺
synchronized与Lock的区别:
1、synchronized是关键字,Lock是一个接口
2、synchronized会自动释放锁,Lock必须手动释放锁
3、synchronized是不可中断的,Lock可以中断可以不中断
4、synchronized是非公平锁,Lock既支持非公平锁也支持公平锁
5、synchronized可以锁代码块和方法,Lock只能锁代码块
6、通过Lock可以知道线程有没有拿到锁,synchronized不能
7、Lock可以使用读锁来提高多线程读效率
CAS
比较与交换,它依赖三个值:内存中的值、旧的预估值和要修改的新值,如果内存中的值等于旧的预估值,就将内存中的值设为新值
CAS底层实现原理:
悲观锁和乐观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都会加锁。synchronized和ReentrantLock都属于悲观锁,性能较差
乐观锁:总是假设最好的情况,每次去拿数据的时候都不会加锁,但是在更新的时候会判断一下在此期间别人有没有修改过这个数据,如果没有则更新,如果有则重试。适用于竞争不激烈、多核CPU的场景。但如果竞争激烈就会频繁重试,反而影响效率。CAS就属于乐观锁
JAVA对象的布局
• 对象头:Mark World + Klass pointer(类型指针)
• 实例数据:类中定义的成员变量
• 对齐数据:作为占位符,可有可无
在64位虚拟机下,Mark World是64bit,其存储结构如下:
Synchronized优化
锁升级
无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁
锁只能升级不能降级
• 偏向锁
原理:
当线程第一次进入同步代码块并获得锁时,虚拟机会把对象头中的锁标志位设为 “01” ,同时会通过CAS操作将获取到这个锁的线程的ID记录在对象头的Mark World中,如果CAS操作成功,那么持有偏向锁的线程以后每次进入与这个锁相关的同步代码块时,虚拟机都可以不用再进行任何同步操作,提高了偏向锁的效率。但是偏向锁只适用于一个线程反复获得同一把锁的场景。
偏向锁的释放:
1、必须等待全局安全点(在这个点所有线程都会停下来)
2、暂停拥有偏向锁的线程并判断锁对象是否处于偏向锁状态
3、将偏向锁标志置为0,锁标志位置为01(无锁)或00(轻量级锁)
注意:偏向锁在java1.6之后默认是启用的,但在应用程序启动几秒钟之后才激活,可以使用 -xx:BiasedLockingStartupDelay=0
参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以使用xx:-UseBiasedLocking=false
参数关闭偏向锁。
• 轻量级锁
当另一个线程参与到偏向锁的竞争时,会先判断 Mark Word 中保存的线程 ID 是否与这个线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁
原理:
判断当前对象是否处于无锁状态(0,01),如果是则JVM会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,然后将当前对象的Mark World复制到Lock Record中,再将Lock Record中的owner指向当前对象。JVM会利用CAS操作尝试将对象的Mark World更新为指向Lock Record的指针,如果成功表示竞争到锁,然后将锁标志位置为00,如果失败则判断当前对象的Mark World是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,可以直接执行同步代码块,否则说明该对象的锁已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变为10,后面等待的线程将会进入阻塞状态。轻量级锁适用追求响应速度的场景
轻量级锁的释放:
1、取出保存在Lock Record中的数据
2、用CAS操作将取出的数据替换回当前对象Mark World,如果成功则说明释放锁成功
3、如果失败,说明有其他线程尝试获取该锁,则需要将轻量级锁膨胀为重量级锁
• 自旋锁
发生在轻量级锁升级为重量级锁的过程中,由于重量级锁是通过monitor实现的,它会阻塞和唤醒线程,而线程的阻塞和唤醒需要CPU从用户态转变为内核态,为了尽量避免锁升级带来的性能开销,允许线程在请求锁时进入一个循环等待的状态,这种机制就是自旋锁。因为自旋等待是需要占用处理器时间的,如果锁被占用的时间很短,自旋等待可以提高性能,但如果锁被占用的时间很长,自旋等待就只会白白消耗处理器资源,反而会使性能下降。自旋默认的次数是10次,可以使用参数-XX:PreBlockSpin
来更改。
JDK6中引入了适应性自旋锁。自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
• 重量级锁
在轻量级锁的状态下如果出现锁竞争,会先进行自旋,如果自旋失败就会将轻量级锁升级为重量级锁,重量级锁适用于追求高吞吐量的场景
锁消除
是指虚拟机的即时编译器(JIT)在运行时,根据逃逸分析,对不可能存在共享数据竞争的锁进行消除。
锁粗化
如果JVM探测到一连串细小的操作都使用同一个对象进行加锁,就会将同步代码块的范围放大,放到这串操作的外面,这样就只需要加一次锁即可。
HashCode方法的调用对Java锁的影响
一个对象在调用原生hashCode方法后(来自Object的,未被重写过的),该对象将无法进入偏向锁状态,起步就会是轻量级锁。若hashCode方法的调用是在对象已经处于偏向锁状态时调用,它的偏向状态会被立即撤销,并且锁会升级为重量级锁。
平时写代码如何对synchronized进行优化
1、减少synchronized的范围:同步代码块中的代码尽量短,减少同步代码块中代码的执行时间
2、降低synchronized的锁的粒度:将一个锁拆分为多个锁提高并发度。比如HashTable和ConcurrentHashMap
3、读写分离:读的时候不加锁,写入和删除时加锁