带你深入理解多线程 --- 锁策略篇

Posted 满眼*星辰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你深入理解多线程 --- 锁策略篇相关的知识,希望对你有一定的参考价值。

这里写目录标题

乐观锁

它认为一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突,则直接修改,如果有冲突,则返回失败

CAS

CAS概念

全称Compare and swap,字面意思:”比较并交换“,是乐观锁的一种机制

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

组成部分

V:内存值
A:旧值
B:新值

机制原理

V == A ?
true(没有并发冲突) -> V = B :
false(并发冲突) -> A = B , continue

  • 如果内存中的值等于旧值,则没有并发冲突,直接将旧值替换为新值即可
  • 如果内存中的值不等于旧值,则进行自旋,将自己的旧值修改为内存值,然后再次进行比较

乐观锁的实现

使用 Atomic.* 相关的类

也可以解决线程不安全的问题

我们写一个线程不安全的示例,一个线程+100次,一个线程-100次
如果最后结果为0,则线程安全
如果最后结果不为0,则线程不安全

    private static AtomicInteger count = new AtomicInteger(0);
    //最大循环次数
    private static final int MAXSIZE = 100000;
    public static void main(String[] args) throws InterruptedException 
        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < MAXSIZE; i++) 
                    count.getAndIncrement();
                
            
        );
        t1.start();

        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < MAXSIZE; i++) 
                    count.getAndDecrement();
                
            
        );
        t2.start();

        t1.join();
        t2.join();

        System.out.println("结果:" + count);
    


最后结果为0,则使用 Atomic.* 是线程安全的

CAS底层实现原理

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

也就是:UnSafe类调用了C++的本地方法,通过调用操作系统的 Atomic::cmpxchg(原子指令)来实现CAS操作;

因为硬件予以了支持,软件层面才能做到

所以乐观锁性能比较高

存在ABA问题

CAS缺点:ABA问题 -》 AtomicInteger是存在ABA问题
A:旧值
B:新值

举例:银行转账

ABA问题代码实现

在常规情况下,没有问题

    private static AtomicReference money = new AtomicReference(100);

    public static void main(String[] args) 
        //转正线程1(-100)
        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0);
                System.out.println("第一次转账:" + res);
            
        );
        t1.start();

        //转正线程2(-100)
        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0);
                System.out.println("第二次转账:" + res);
            
        );
        t2.start();

    

意外情况:中途+100:

    private static AtomicReference money = new AtomicReference(100);

    public static void main(String[] args) throws InterruptedException 
        //转正线程1(-100)
        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0);
                System.out.println("第一次转账:" + res);
            
        );
        t1.start();

        t1.join();

        //转入100元
        Thread t3 = new Thread(new Runnable() 
            @Override
            public void run() 
                //+100
                boolean res = money.compareAndSet(0,100);
                System.out.println("转入100元:" + res);
            
        );
        t3.start();

        t3.join();

        //转正线程2(-100)
        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0);
                System.out.println("第二次转账:" + res);
            
        );
        t2.start();

    

解决ABA问题

ABA同一解决方案:
增加版本号,每次修改之后更新版本号

    private static AtomicStampedReference money = new AtomicStampedReference(100,1);

//    private static AtomicReference money = new AtomicReference(100);

    public static void main(String[] args) throws InterruptedException 
        //转正线程1(-100)
        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0,1,2);
                System.out.println("第一次转账:" + res);
            
        );
        t1.start();

        t1.join();

        //转入100元
        Thread t3 = new Thread(new Runnable() 
            @Override
            public void run() 
                //+100
                boolean res = money.compareAndSet(0,100,2,3);
                System.out.println("转入100元:" + res);
            
        );
        t3.start();
        
        t3.join();

        //转正线程2(-100)
        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                boolean res = money.compareAndSet(100,0,1,2);
                System.out.println("第二次转账:" + res);
            
        );
        t2.start();

    

AtomicStampedReference(解决)和AtomicReference
对比的是引用,而非值

解决方案:设置应用程序的参数(-D),调整Integer的高速缓存最大值

悲观锁

它认为通常情况下会出现并发冲突,所以它一开始就会加锁

synchronized就是悲观锁

共享锁/非共享锁(独占锁)

共享锁:一把锁可以被多个程序拥有,这就叫共享锁

非共享锁:一把锁只能被一个线程拥有,这就叫非共享锁

共享锁有哪些?读写锁中的读锁

非共享锁有哪些?synchronized锁

读写锁

概念

定义:就是将一把锁分为两个,一个用于读数据的锁(也叫做读锁),另一把锁叫作写锁。

特点:读锁是可以被多个线程同时拥有的,而写锁则只能被一个线程拥有

java中的读写锁:ReentrantReaderWriterLock(默认是非公平的,true-公平)

读写锁的优势:锁的粒度更加小,性能也更高

读写锁代码实现

    public static void main(String[] args) 
        //创建一个读写锁
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0,
                TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));

        //任务1:读锁
        executor.execute(new Runnable() 
            @Override
            public void run() 
                //加锁
                readLock.lock();
                try 
                    //业务逻辑处理
                    System.out.println(Thread.currentThread().getName() + "执行了读操作:" + new Date());
                    Thread.sleep(3000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 finally 
                    //释放锁
                    readLock.unlock();
                
            
        );

        //任务2:读锁
        executor.execute(new Runnable() 
            @Override
            public void run() 
                //加锁
                readLock.lock();
                try 
                    //业务逻辑处理
                    System.out.println(Thread.currentThread().getName() + "执行了读操作:" + new Date());
                    Thread.sleep(3000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 finally 
                    //释放锁
                    readLock.unlock();
                
            
        );

        //任务3:写锁
        executor.execute(new Runnable() 
            @Override
            public void run() 
                //加锁
                writeLock.lock();
                try 
                    //业务逻辑处理
                    System.out.println(Thread.currentThread().getName() + "执行了读操作:" + new Date());
                    Thread.sleep(3000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 finally 
                    //释放锁
                    writeLock.unlock();
                
            
        );

        //任务4:写锁
        executor.execute(new Runnable() 
            @Override
            public void run() 
                //加锁
                writeLock.lock();
                try 
                    //业务逻辑处理
                    System.out.println(Thread.currentThread().getName() + "执行了读操作:" + new Date());
                    Thread.sleep(3000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 finally 
                    //释放锁
                    writeLock.unlock();
                
            
        );
    


可以看到读操作没有进行休眠3秒,说明读锁是共享的

注意事项:读写锁中的读锁和写锁是互斥的。(防止同时读写锁产生的脏数据,所以读写锁的读锁和写锁是互斥的)

公平锁和非公平锁

概念

公平锁:锁的获取顺序必须和线程方法的先后顺序保持一致,就叫做公平锁

非公平锁:锁的获取顺序和线程获取锁的前后顺序无关,就叫做非公平锁(默认所策略)

优点

非公平锁的优点:性能比较高

公平锁的优点:执行是有序的,所以结果也是可以预期的

java实现

公平锁:new ReentrantLock(true)

非公平锁:new RenntrantLock() / new RenntrantLock(false) / synchronized

自旋锁

通过死循环一直尝试获取锁

while(true) 
	if(尝试获取锁) 
		return;
	

synchronized (轻量级锁) 是自旋锁

可重入锁

当一个线程获取到一个锁之后,可以重复的进入

代表:synchronized,Lock

代码示例

    //创建锁
    private static Object lock = new Object();

    public static void main(String[] args) 
        //第一次进入锁
        synchronized (lock) 
            System.out.println("第一次进入锁");
            synchronized (lock) 
                System.out.println("第二次进入锁");
            
        
    


打印了两次,证明synchronized是可重入锁

总结锁策略的问题

你是怎么理解乐观锁和悲观锁的,具体怎么实现?

  1. 它认为一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突,则直接修改,如果有冲突,则返回失败。
    乐观锁 -》 CAS -》 Atomic*,CAS组成是由V(内存值)A(预期值)B(新值)组成,然后执行的时候是使用V==A对比,如果结果为true则表明没有冲突,则可以直接修改,否则不能修改。
    CAS是通过调用C++提供的UnSafe中的本地方法(CompareAndSwapXXX)来实现的,C++通过调用操作系统的Atomic::cmpxchg(原子指令)来实现的
  2. 它认为通常情况下会出现并发冲突,所以它一开始就会加锁
    悲观锁 - 》 synchronized 在Java 中是将锁的ID存放到对象头来实现的放到;
    synchronized 在jvm层面是通过监视器来实现的;
    synchronized在操作系统层面是通过互斥锁mutex实现的

有了解什么是读写锁吗?

  • 读写锁就是将锁的粒度分的更细,分为读锁和写锁,并且读锁和写锁之间是互斥的,互斥的原因是为了防止脏数据的诞生(在修改的时候也在读取)
  • 读锁是可以多个线程同时拥有的,所以它是共享锁,而写锁是互斥的,它是独占锁,在一个线程操作的时候其他线程不能操作
  • 读写锁在java里面可以使用ReentrantReaderWriterLock来去创建,通过.readLock 和 .writeLock来分别得到读锁和写锁

什么是自旋锁,为什么要使用自旋锁策略呢,其缺点是什么?

自旋锁就是通过自己的循环来尝试获取锁,如果获取到了锁他才会停止

初衷是在短期间内进行轻量级的锁定。

如果是死锁,或者锁执行周期过长,那么就会一直尝试获取锁
自旋锁的缺点:如果发生死锁则会一直自旋(循环),所以会带来一定的额外开销

java对于自旋锁的策略:使其有最大执行次数,比如超过64次,则将锁放入队列中进行排队,下次再进行自旋

synchronized 锁优化(锁消除)

jdk1.6锁升级的过程:

  1. 无锁
  2. 偏向锁(第一个线程第一次访问)将线程id存储在对象头中的偏向锁标识
  3. 轻量级锁(自旋)
  4. 重量级锁

以上是关于带你深入理解多线程 --- 锁策略篇的主要内容,如果未能解决你的问题,请参考以下文章

深入Java多线程锁策略

《深入理解Java虚拟机》笔记04 -- 并发锁

带你深入理解 Redis分布式锁...

iOS底层探索之多线程(十八)——锁篇章的完结篇(手把手两种方式带你实现一个读写锁!)

深入理解Python中的GIL(全局解释器锁)

这才叫细:带你深入理解Redis分布式锁