多线程中的锁

时间:2020-07-01 00:26:29   收藏:0   阅读:83

多线程中的锁

首先讲讲锁的分类

锁的分类

  1. 公平锁/非公平锁
  2. 可重入锁(递归锁)
  3. 独享锁/共享锁
  4. 独享锁/共享锁
  5. 互斥锁/读写锁
  6. 乐观锁/悲观锁
  7. 分段锁
  8. 偏向锁/轻量级锁/重量级锁
  9. 自旋锁/自适应自旋锁
  10. 锁粗化/锁消除

公平锁和非公平锁

线程挂起和线程真正运行之间存在着很长的时间差

公平锁

非公平锁

可重入锁(递归锁)

可重入锁指的是一个线程获取了一个锁,此线程重新获取这个锁,不会造成死锁。

Synchronized和ReentrantLock都是可重入锁。

可重入锁的实际应用

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("FunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("FunctionB");
    }
}

如上代码: 当一个带有锁的方法调用另外一个带有锁的方法, 如果不是可重入锁,就会产生死锁,因为无法获取到第二把锁。因为可重入锁可以实现锁的递归,即锁的外层嵌套锁。

独享锁/共享锁

独享锁

共享锁

互斥锁/读写锁

互斥锁

读写锁

乐观锁/悲观锁

悲观锁

乐观锁

分段锁

分段锁出自jdk1.7中的ConcurrentHashMap,在jdk1.8中去掉了分段锁,但是依然有分段的影子在里面。

? ConcurrentHashMap在1.7中为了提高并发效率,将Map划分成了很多个Segment, 每一个Segment都实现了RenentrantLock,这个锁就叫做分段锁,只有对每一个分段Segment进行操作的时候才对这个分段进行上锁,不影响其他线程对其他的Segment进行操作。

偏向锁/轻量级锁/重量级锁

偏向锁

轻量级锁

重量级锁

自旋锁/自适应自旋锁

自旋锁

减少线程阻塞造成的线程切换

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:

如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。

“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。

缺点

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间

缺点

然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值

锁粗化/锁消除

锁粗化

当一个线程高频地请求, 同步和释放锁,会消耗系统资源。在这种情况下把多次锁请求合并成一次锁请求,减小锁请求,同步,释放带来的性能损耗。

锁粗化的情况:

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
    //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
    synchronized(lock){
        //do other thing
    }
}

合并请求后的代码

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}

上面做法的前提是, 两个锁请求之间的工作可以迅速做完。如果不能迅速做完,合并之后会导致同步代码块执行需要花费很长时间,极大影响了多线程的工作

合并前代码

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}

合并后代码

synchronized(lock){
    for(int i=0;i<size;i++){
    }
}

锁清除

锁清除是发生在编译器级别的一种锁优化方式。

有时候代码完全不需要加锁,却执行了加锁操作,这个时候编译器就会进行锁清除优化。

例子:StringBuffer的append操作已经是线程安全的了

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

可能在实际使用中并不需要进行加锁处理, 这里StringBuffer作为局部变量使用, 函数执行完, 变量就会被清除,每一个线程都会拥有自己的局部变量, 不会涉及到并发问题, 因此也没必要进行加锁操作;

public class Demo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int size = 10000;
        for (int i = 0; i < size; i++) {
            createStringBuffer("Hyes", "为分享技术而生");
        }
        long timeCost = System.currentTimeMillis() - start;
        System.out.println("createStringBuffer:" + timeCost + " ms");
    }
    public static String createStringBuffer(String str1, String str2) {
        StringBuffer sBuf = new StringBuffer();
        sBuf.append(str1);// append方法是同步操作
        sBuf.append(str2);
        return sBuf.toString();
    }
}

这个时候可以通过编译器来消除同步锁。

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!