标签:官方 范围 tran 升级 strong 拷贝 互斥量 释放 刷新
Java实现锁的方式主要有2种,一是synchronized,二是并发包java.util.concurrent中Lock接口的实现类ReentrantLock。需要知道的是前者是关键字,JVM原生的亲儿子来着的,后者是封装类,未来JVM改进肯定是先改进synchronized关键字。
1.volatile
修饰后保证变量的内存可见性,禁?volatile变量与普通变量重排序。volatile有着与锁相同的内存语义,所以可以作为?个 “轻量级”的锁来使?。
说白了就是修饰后的变量每一次变动都会立即刷新到主存,获取的也是主存的最新值,实现多线程同步。
有一个坑,Java中的运算不是原子操作,volatile变量在高并发的情况下也是线程不安全的。高并发情况下一般都配合synchronized使用。
在一些情况下,volatile的同步机制性能要大于锁(synchronized或者并发包java.util.concurrent包里的锁),但是由于JVM对锁实行许多消除和优化,很难量化volatile比synchronized快多少。volatile的读操作性能消耗和普通变量几乎没有什么差别,不过写操作则可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行,但总开销还是比锁低。
2.synchronized
用此关键字对代码上锁,锁住的东西是一个对象,也叫对象锁。synchronized的使用方式有3种。被修饰的方法不能重写。
(1)修饰代码块,被修饰的代码叫做“临界区”,它规定,临界区的代码同一时刻只能由同一个线程执行。
public class TestThread { public static void main(String[] args) { Object object=new Object(); synchronized(object) { //临界区 } } }
(2)修饰普通方法,锁为当前对象/实例。
//关键字在实例方法上,锁为当前实例 public synchronized void test1() { //code } // 关键字在代码块上,锁为括号里面的对象,二者等价 public void test2() { synchronized (this) { //code } }
(3)修饰静态方法,锁为当前的Class对象。
//关键字在静态方法上,锁为当前Class对象 public static synchronized void test3() { //code } //二者等价 public void test4() { synchronized (this.getClass()) { // code } }
3.一些锁的概念
(1)公平锁
公平锁指多个线程等待一个锁时,按照申请锁的先后顺序来依次获得锁;
非公平锁就不保证这一点,释放锁后所有申请锁的线程都有机会获得锁,例如synchronized。
(2)自旋锁与自适应自旋
互斥同步对性能最大的影响就是阻塞的实现,挂起线程和恢复线程都需要开销。
自旋锁就是要减少这类情况带来的系统开销,例如线程A在申请锁X的时候,发现锁X被线程B占用,但是线程A不挂起,继续申请,这个申请操作叫做自旋,需要耗费处理器资源。如果稍微等一会就拿到锁X,那就不用花费挂起线程的开销,赚大了;如果要等很久才能拿到锁X,这个等待时间耗费的处理器资源可能更大,那就亏死了。可以通过判断自旋次数 来停止继续自旋,用传统的方式挂起线程,避免耗费太多资源。
自适应自旋(JDK1.6引入)就是 针对有些锁可能要用很久 使得 申请的线程自旋次数很多,有的锁可能一下子就好 使得 申请的线程自旋次数很少,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。同一个锁对象上,如果上一个申请锁的线程自旋很少次数就获得锁,那么本次的自旋次数可以持续相对更长时间;如果上一个申请锁的线程自旋好多好多次才拿到锁,那可能不自旋了,免得浪费时间,直接挂起。自适应自旋使得JVM对程序锁的状况预测越来越准确,减少开销。
(3)锁消除
指JVM在运行时,对一些代码要求同步,但是检测到不可能就不存在共享数据竞争,那么就会将那个运用于同步的锁消除。锁消除的判定依据来源于逃逸分析的数据支持,如果判断一段代码,堆上的所有数据都不会逃逸出去从而被其他线程访问到,就可以把它当作栈上的数据来对待,认为它们线程私有,不用加锁。
疑点:加不加锁程序员自己不知道吗?还要靠JVM?
解答:有些同步措施不是程序员自己加的。例如一些代码语法看着没锁,其实有锁,但又不需要锁,就锁消除。
(4)锁粗化
原则上我们加锁的代码块都设置的尽量小,使得需要同步的操作数量尽可能变小,如果存在锁竞争,等待锁的线程也能快点拿到锁。但如果一系列操作都是对同一个锁进行加锁解锁,这样频繁互斥同步会导致不必要的性能损耗。锁粗化针对这种情况,把加锁的同步范围扩展(粗化)到整个操作序列的外部,即原本加锁许多次,现在只加锁一次。
(5)轻量级锁
JDK1.6加入,“轻量级”是相对 使用操作系统互斥量来实现的传统锁而言,传统锁叫“重量级”锁。他的本意是 在没有多线程竞争资源的时候,不同重量级锁,减少开销。
进入同步块之前的 加轻量级锁的过程,例如线程A要获取锁Obejct。首先,通过锁X的对象头的信息看锁Obejct有没有被其他线程锁定,如果没有,就在自己的栈帧中创建一个名为锁记录LockRecord的空间,用于存储锁X的对象头目前的MarkWord的拷贝,官方把这份拷贝加了一个Displaced前缀,即DisplacedMarkWord,这时候线程A的栈帧与锁Object的对象头如图所示:
然后,JVM通过CAS操作 尝试将对象锁Object的MarkWord更新为指向 栈帧锁记录的指针。
如果这个操作成功了,那么线程A就获得了锁Object,并改一下Obejct的MarkWord的锁标志位,表示处于轻量级锁定状态。
如果CAS操作失败了,JVM就检查一下对象的MarkWord是否指向线程A的栈帧,如果是则说明线程A拥有了锁Object,可以继续执行同步块代码了。
否则说明锁Object被其他线程抢了,存在两个以上的线程抢一个锁时,这个锁就被升级为重量级锁(改锁标志位),则线程A和后面需要用这个锁的线程都要进入阻塞状态。
轻量级锁的解锁过程也是通过CAS操作来实现的,如果锁Object的Mark Word仍然指向着线程A的锁记录,那就用CAS操作把对象当前的MarkWord和线程中复制的DisplacedMarkWord替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁的意义就是优化 没有竞争的锁 却仍然互斥同步带来的开销。
(6)偏向锁
JDK1.6加入,偏向锁的“偏”是偏心的意思,意味着这个锁会偏向第一个获得它的线程。举例说明,如果锁X第一次被线程A使用了,那么锁X在对象头设置为“偏向模式”,偏向线程A,下一次线程A要使用锁X时,不需要进行同步操作(Locking,Unlocking,对MarkWord的Update等);如果有线程B要获取锁X,那么锁X会“撤销偏向”恢复到 未锁定或者轻量级锁定状态,后续又是轻量级加锁过程。
(7)小结:除了公平锁是概念之外,其他知识点都是锁的优化,不一定时时刻刻都是对程序好的。在具体问题分析的前提下,可以修改参数来开启优化,提高性能。
参考&引用
《深入浅出Java多线程》
《深入理解Java虚拟机第二版》
标签:官方 范围 tran 升级 strong 拷贝 互斥量 释放 刷新
原文地址:https://www.cnblogs.com/shoulinniao/p/12634424.html