【Java并发】锁优化

锁的四种状态

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是不可以降级。

重量级锁

sychronized就是重量级锁。

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。JDK为了sychronized的优化,引入了轻量级锁和偏向锁。

一个依据:“对于绝大部分的锁,在整个同步周期内都是不存在竞争的。”

这是轻量级锁和偏向锁的依据。

轻量级锁

JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。

以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。


下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。


轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。

当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。


如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

偏向锁

如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译 代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。(只有一次CAS

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态偏向锁可以提高有同步但竞争比较少的程序性能。要是大多数锁都是被多个不同线程访问,反而会影响程序的性能。可以设置参数禁止偏向锁。

比较

åç§éç对æ¯​​

其他锁优化

乐观锁与悲观锁

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

CAS

Atomic底层的实现就是CAS。

CAS是一个原子操作。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。


2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。


3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。


4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。


5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。


6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。


7.线程1进行SWAP,把地址V的值替换为B,也就是12。

Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

public class AtomicBooleanTest implements Runnable{
	private static AtomicBoolean flag = new AtomicBoolean(true);

    public static void main(String[] args) {
        AtomicBooleanTest ast = new AtomicBooleanTest();
        Thread thread1 = new Thread(ast);
        Thread thread = new Thread(ast);
        thread1.start();
        thread.start();
    }
    @Override
    public void run() {
        System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
        if (flag.compareAndSet(true,false)){
            System.out.println(Thread.currentThread().getName()+""+flag.get());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag.set(true);
        }else{
            System.out.println("重试机制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            run();
        }

    }





输出:
thread:Thread-0;flag:true
thread:Thread-1;flag:true
Thread-0false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:false
重试机制thread:Thread-1;flag:false
thread:Thread-1;flag:true
Thread-1false复制代码

compareAndSet(boolean expect,boolean update)方法,分为两个步骤 :compare(true)方法和Set(false)方法,是compare(true)是等于true后,就马上设置共享内存为false,这个时候,其它线程无论怎么走都无法走到只有得到共享内存为true时的程序隔离方法区。

CAS缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

自旋锁(空转打圈儿)

当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环(while循环中啥也不做),再次申请 锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。避免线程挂起导致的花费。

互斥同步对性能影响最大的是阻塞,即线程的挂起和恢复。许多应用中,共享数据的锁定状态只会持续很短的一段时间。如果有两个以上的处理器,能让两个或者以上的线程并行执行,我们就可以让后面请求锁的线程等待一下,但是并不放弃处理器的执行时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。

自适应的自旋锁:

自适应的自旋锁意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有者的线程正在运行中,那么虚拟机认为这次自旋也很有可能再次成功,因此会自旋等待较长的时间。相反的是,假如对于某个锁,自旋等待很少成功,那么以后获取这个锁的时候即有可能省略掉这个过程。

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除.

主要通过逃逸分析来判定。

何为逃逸?

当一个对象在方法中被定义后,如果被外部方法所引用,甚至可能会被外部线程所访问到,称为线程逃逸。

如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。

因为代码中会有许多隐形的锁,比如String。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。

例如在一个for循环里枷锁,就可以把锁提到外面。


优化方法

  1. 减少锁持有时间:尽可能减少同步代码块,加快同步代码块执行速度。
  2. 减少锁的粒度:分段锁概念
  3. 锁粗化
  4. 锁分离(读写锁)
  5. 使用CAS + 自旋的形式:CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。


猜你喜欢

转载自juejin.im/post/5c949918e51d45155076b10b