多线程常见锁策略—乐观悲观、自旋、读写、ABA问题及解决

一、常见锁策略

1.乐观锁

定义

乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正
式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

执行流程
在这里插入图片描述

实现
Atomic*家族

AtomicInteger count = new AtomicInteger(0);//int count = 0;
count.getAndIncrement();//i++
count.incrementAndGet();//++i
System.out.println(count.getAndIncrement());
//AtomicInteger实现线程安全
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++;
                    count.getAndIncrement();
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <MAXSIZE ; i++) {
                    //count--;
                    count.getAndDecrement();
                }
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果"+count);

问题

并不总是能处理所有问题,所以会引入一定的系统复杂度

问题2

Integer高速缓存问题 (-128~127)超出范围的值会重新new对象,造成结果与预期不相符。
解决方案:设置应用程序的参数(-D),设置Integer高速缓存最大值。

2.悲观锁

定义

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,会出现并发冲突,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
例如:synchronized

问题

总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。

3.可重入锁

定义

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

/**Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的。
*/
private static Object lock = new Object();

    public static void main(String[] args) {
        synchronized (lock){
            System.out.println("第一次");
            synchronized (lock){
                System.out.println("第二次");
            }
        }
    }

4.读写锁

定义

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而生。
读写锁(readers-writer lock),将一个锁分成两个,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
读锁和写锁互斥:防止读脏数据。
例如:ReentrantReadWriteLock

优点

粒度小,性能高

public static void main(String[] args) throws InterruptedException{
        //创建读写锁
        ReentrantReadWriteLock readwriteLock = new ReentrantReadWriteLock(true);//公平性
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readwriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readwriteLock.writeLock();

        //线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(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();
                }
            }
        });

    }

在这里插入图片描述

5.共享锁

定义

一把锁可以被多个线程拥有,这就叫共享锁
例如:读写锁的读锁、
非共享锁:synchronized

6.自旋锁

定义

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

问题

如果发生死锁则会一直自旋,所以会带来一定的额外开销

6.公平锁

定义

锁的获取顺序必须和线程方的获取顺序保持一致,就叫公平锁。执行时有序,结果可预测
new ReentrantLock(true)
非公平锁:默认锁策略,性能更高
new ReentrantLock()/ new ReentrantLock(false)/synchronized

7.常见面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
    1.乐观锁->CAS->Atomic*,CAS是由V、A、B组成,然后执行的时候使用V==A对比,结果为true表明没有冲突,可以直接修改否则不可以修改。CAS是通过调用C++实现的UnSafe中的本地方法(ComparaAndSwap)来实现,C++是通过调用操作系统Atomic::cmpxchg(原子指令)来实现
    2.悲观锁->synchronized在java中是将锁的ID存放到对象
  1. 是否了解什么读写锁么?
    读写锁(readers-writer lock),将一个锁分成两个,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
    读锁和写锁互斥:防止读脏数据。
    例如:ReentrantReadWriteLock
  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
    通过死循环一直尝试获取锁,如果发生死锁则会一直自旋,所以会带来一定的额外开销
  1. synchronized 是可重入锁么?

二、CAS

定义

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比 较)
  2. 如果比较相等,将 B 写入V。(交换)
  3. 返回操作是否成功。

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

原理

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

缺点

ABA问题:A:旧值;B:预期新值

银行转账为例
第一次转账(转出100):V(100) A(100) B(0)——>V(100)——>V(100) == A(100)——>true——>V(0)
第二次转账(转入100):V(0) A(0) B(100)——>V(100)——>V(0) == A(0)——>true——>V(100)
第三次转账(转出100):V(100) A(100) B(0)——>V(0)——>V(100) == A(100)——>true——>V(0)
当我第一次转出后,此时银行卡内转入100,加入我误操作点击两次,系统识别不出就会继续进行第二次转账,100元就会消失。

package thread0527;

import java.util.concurrent.atomic.AtomicReference;

public class ThreadDemo93 {
    private static AtomicReference money = new AtomicReference(100);//初始金额100元

    public static void main(String[] args) throws InterruptedException {
        //转账一:(-100)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(100,0);//转账操作
                System.out.println("第一次转账(-100)"+result);
            }
        });
        t1.start();
        t1.join();
        //
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(0,100);
                System.out.println("转入100元"+result);
            }
        });
        t3.start();
        t3.join();

        //转账二:(-100)
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(100,0);//转账操作
                System.out.println("第二次转账(-100)"+result);
            }
        });
        t2.start();
    }
}

在这里插入图片描述
解决方案

增加版本号,每次修改后更新版本号

package thread0527;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ThreadDemo94 {
    private static AtomicStampedReference money = new AtomicStampedReference(100,1);
    //private static AtomicReference money = new AtomicReference(100);//初始金额100元

    public static void main(String[] args) throws InterruptedException {
        //转账一:(-100)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(100,0,1,2);//转账操作
                System.out.println("第一次转账(-100)"+result);
            }
        });
        t1.start();
        t1.join();
        //
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(0,100,2,3);
                System.out.println("转入100元"+result);
            }
        });
        t3.start();
        t3.join();

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

}

程序执行结果
在这里插入图片描述
面试问题:CAS底层实现原理。

java层面CAS的实现的UNSafe类,UnSafe类调用C++的本地方法,通过调用操作系统的Atomic::cmpxchg(原子指令)来实现CAS操作。

在这里插入图片描述

三、synchronized 背后的原理

面试题:

  1. 什么是偏向锁?
    对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,
    降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁
    的,只有当其他线程尝试竞争偏向锁才会被释放。
  1. java 的 synchronized 是怎么实现的,有了解过么?
    无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
    偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
    偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态
    轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
    当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
    重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的
    Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

3.synchronized锁优化(重点)
JDK1.6锁升级的过程:无锁->偏向锁(第一个线程第一次访问,将线程ID存储在对象投中的偏行锁标识)->轻量级锁(自旋)->重量级锁

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值