乐观锁认为一般情况下不会发生并发冲突,只有在数据进行更新时候,才会去检测并发冲突,如果没有检测到冲突则直接进行数据修改,若有冲突则返回失败。
CAS:全称为Compare and swap,字面意思“比较并交换”,是乐观锁的一种机制。当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程会收到操作失败的信号,因此可以看出,CAS是乐观锁。
CAS组成部分:V:内存中的值
A:预期的旧值
B:新值
实现原理:如果V==A为true,此时没有并发冲突,将B赋值给V,成功修改;当V!=A,产生了并发冲突,进行自旋,将旧值A修改为内存中的值V,然后再进行比较。
使用Atomic相关类实现乐观锁,同时它可以解决线程不安全的问题。
示例:对变量count=0进行100次++操作和100次- -操作,去验证线程是否可以解决线程不安全。
public class TestDemo5 { private static AtomicInteger count = new AtomicInteger(0); //设置最大循环次数 private static final int MAXSIZE = 100000; public static void main(String[] args) throws InterruptedException { //这里创建线程执行任务,可以用join等待线程执行完;而线程池无法等待某个线程执行结束 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); // AtomicInteger count = new AtomicInteger(0); // count.getAndIncrement(); // count.incrementAndGet(); // System.out.println(count); //结果:2 } }
运行结果为:0,表明使用AtomicInteger是线程安全的。
CAS底层实现原理:
在Java层面来看,CAS是通过Unsafe类去实现的,Unsafe类调用c++实现的本地方法,通过调用操作系统的Atomic::cmpxchg(原子指令)实现CAS操作。
CAS优点:性能比较高
CAS缺点:存在ABA问题(Atomic类带来的ABA)
A:预期旧值
B:新值
1)执行转账操作,不小心多点击一次转账:
正常情况下,代码执行是正常的结果
public class TestDemo1 { 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 flag = money.compareAndSet(100,0); System.out.println("第一次转账:"+flag); } }); t1.start(); //转账线程2 -100元 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean flag = money.compareAndSet(100,0); System.out.println("第二次转账:"+flag); } }); t2.start(); } }
可以发现第一次转账成功为true,第二次转账失败返回false.
2)中途意外获得一次入账(ABA问题产生)
public class TestDemo2 { 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 flag = money.compareAndSet(100,0); System.out.println("第一次转账:"+flag); } }); t1.start(); t1.join(); //转入100元 Thread t3 = new Thread(new Runnable() { @Override public void run() { //+100 boolean flag = money.compareAndSet(0,100); System.out.println("转入100元"+flag); } }); t3.start(); t3.join(); //转账线程2 -100元 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean flag = money.compareAndSet(100,0); System.out.println("第二次转账:"+flag); } }); t2.start(); } }
明显看到,第一次转账和第二次转账都为true,不是原本预期的结果;之所以第二次转账成功是因为原本内存的值已经被线程3修改。
统一解决方案:每次修改之后增加版本号
使用AtomicStampedReference类:
AtomicStampedReference atomicStampedReference = new AtomicStampedReference();
/** * 解决ABA问题 */ public class TestDemo3 { private static AtomicStampedReference money = new AtomicStampedReference( 100,1); //传递初始值和版本号 public static void main(String[] args) { //转账线程1 -100元 Thread t1 = new Thread(new Runnable() { @Override public void run() { //传递旧值和新值 boolean flag = money.compareAndSet( //四个参数分别是:预期旧值 新值 预期旧版本号 新版本号 100,0,1,2); System.out.println("第一次转账:"+flag); } }); t1.start(); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } //转入100元 Thread t3 = new Thread(new Runnable() { @Override public void run() { //+100 //转入是第二次操作 旧版本号 2 新版本号 3 boolean flag = money.compareAndSet(0,100, 2,3); System.out.println(flag); } }); t3.start(); try { t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } //转账线程2 -100元 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean flag = money.compareAndSet(100,0, 1,2); System.out.println("第二次转账:"+flag); } }); t2.start(); } }
完美解决了上面存在的ABA问题!
面试题:AtomicReference和AtomicStampedReference的区别是什么呢?
AtomicReference会产生ABA问题,而AtomicStampedReference不会产生ABA问题。
知识点补充:我们把转账数额由100改成1000时,运行结果 false false false,这是因为Integer高速缓存的问题,具体我们看一下Integer源码中IntegerCache类
分析:Integerg高速缓存范围:-128 ~127,当我们设置1000时候出现false,实际上对比的是引用而不是数值;100存在于高速缓存范围内,因此获得对象都是同一个,而1000在范围之外,需要去new,因而获得是两个不同的对象 。
解决方案:调整Integer高速缓存的边界值,对当前程序进行应用程序参数设置
它认为程序通常情况下会出现并发冲突,所以在一开始就会进行加锁(比如synchronized就是悲观锁)。
共享锁定义:一把锁可以被多个程序拥有,这就叫共享锁(比如读写锁中读锁)。
非共享锁定义:一把锁只能被一个线程拥有,这就叫非共享锁(比如synchronized是非共享锁)。
读写锁定义:将一把锁分为两部分,一个是读数据的锁(读锁),另外一个是写数据的锁(写锁);读锁是可以被多个线程同时拥有的,而写锁在一个时间段内只能被一个线程拥有。
读写锁的优势:锁的粒度更细,性能更高。
读写锁的具体实现:ReentrantReadWriteLock类
读写锁代码演示:
public class TestDemo4 { public static void main(String[] args) { //创建一个读写锁 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //分离出读锁 ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); //分离出写锁 ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); //声明线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000)); //任务1.读锁 threadPoolExecutor.execute(new Runnable() { @Override public void run() { //加锁 readLock.lock(); try{ //业务逻辑处理 System.out.println(Thread.currentThread().getName()+ "执行了读锁操作"+new Date()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }finally{ readLock.unlock(); //释放锁 } } }); //任务2.读锁 threadPoolExecutor.execute(new Runnable() { @Override public void run() { //加锁 readLock.lock(); try{ //业务逻辑处理 System.out.println(Thread.currentThread().getName()+ "执行了读锁操作"+new Date()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }finally{ readLock.unlock(); //释放锁 } } }); //任务3.写锁 threadPoolExecutor.execute(new Runnable() { @Override public void run() { //加锁 writeLock.lock(); try{ //业务逻辑处理 System.out.println(Thread.currentThread().getName()+ "执行了写锁操作"+new Date()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }finally{ writeLock.unlock(); //释放锁 } } }); //任务4.写锁 threadPoolExecutor.execute(new Runnable() { @Override public void run() { //加锁 writeLock.lock(); try{ //业务逻辑处理 System.out.println(Thread.currentThread().getName()+ "执行了写锁操作"+new Date()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }finally{ writeLock.unlock(); //释放锁 } } }); } }
可以看出,读操作之间没有执行我们设定的时间间隔,说明读锁是共享的;写操作之间间隔了我们设定的时间,表明同一时刻写锁只能被一个线程占有。
注意事项:读写锁中读锁和写锁是互斥的,为了防止同时读写所产生的脏数据。
公平锁的定义:锁的获取顺序与线程方法的先后顺序一致,这就叫公平锁。
非公平锁的定义:锁的获取顺序和线程方法的先后顺序无关,这就叫非公平锁。(java默认锁策略)
优点:公平锁执行是有序的,执行结果是可预期的;非公平锁性能比较高。
显示设置公平锁:
ReentrantLock reenrantLock = new ReentrantLock();
非公平锁:
//不写参数即默认false ReentrantLock reenrantLock = new ReentrantLock(false);
自旋锁:通过死循环一直尝试获取锁
while(true){ if(获取锁的条件){ return ; } }
可重入锁定义:当一个线程获取到锁之后,该锁可以重复进入(比如synchronized与Lock)。
可重入锁代码演示:
public class TestDemo6 { //创建锁 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机制去实现的,而CAS通过Atomic相关类去实现;CAS中含有三个组成部分V(内存值)、A(预期旧值)、B(新值),执行的时候使用A和V进行对别,如果相等(true)表明没有发生并发冲突,则可以直接修改数据,否则不能直接修改。
Java中CAS是通过调用Unsafe类中c++本地方法(compareAndSwapXXX,XXX可以是Object\Integer\Long)去实现的,本地方法通过调用操作系统Atomic::cmpxchg原子指令实现的。
悲观锁
在程序一开始就会进行加锁,比如synchronized在Java中将锁的ID存放在对象头中。对象头中有一个偏向锁ID,线程会将自己的id放到偏向锁ID中,每次有线程去访问的时候,将当前线程id和偏向锁ID进行判断,如果相等表明该线程拥有了这把锁,执行相应代码;否则通过自旋去尝试获取锁。synchronized在Java层面是通过监视器锁实现的,在操作系统层面通过互斥锁mutex实现的。
2.铁子,你了解读写锁吗?
答:将锁的粒度分的更细,分为读锁和写锁;读锁和写锁是互斥的,为了防止脏读现象(修改时候也在读取);读锁可以被多个线程同时拥有因此是共享锁,而写锁在一个时间段内只能被一个线程拥有因此是非共享锁;读写锁在Java中可以通过ReentrantReadWriteLock去创建,可以将创建的对象通过ReentrantReadWriteLock.WriteLock与ReentrantReadWriteLock.ReadLock将读锁与写锁分离出来;
3.什么是自旋锁?为什么要使用自旋锁策略,缺点是什么?
答:
自旋锁的缺点:如果发生了死锁则会一直自旋(循环),做无用功,带来一定的性能开销(CPU资源)。
解决自旋方案:给自旋设置次数,也就是给明确的循环边界值;先将线程放在等待队列中,等有锁了去唤醒线程。
4.synchronized是可重入锁吗?
答:是可重入锁!!!
5.synchronized锁优化(锁消除)过程是什么?
答:从jdk1.6开始出现锁升级的过程:
无锁:没有线程访问时,处于无锁状态。
偏向锁:第一个线程第一次访问时,转换为偏向锁;它将线程ID存储在对象头的偏向锁标识里面。
轻量级锁:通过自旋方式尝试获取锁,自旋一定次数后,还未获取锁,则会升级为重量级锁。
重量级锁:用户内存到系统内存的切换,性能比较低。