本文共 8724 字,大约阅读时间需要 29 分钟。
目录
一、常见锁策略
1.乐观锁
定义
乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正
式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
执行流程
实现
Atomic*家族AtomicInteger count = new AtomicInteger(0);//int count = 0;count.getAndIncrement();//i++count.incrementAndGet();//++iSystem.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
问题
并不总是能处理所有问题,所以会引入一定的系统复杂度
问题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.乐观锁->CAS->Atomic*,CAS是由V、A、B组成,然后执行的时候使用V==A对比,结果为true表明没有冲突,可以直接修改否则不可以修改。CAS是通过调用C++实现的UnSafe中的本地方法(ComparaAndSwap)来实现,C++是通过调用操作系统Atomic::cmpxchg(原子指令)来实现 2.悲观锁->synchronized在java中是将锁的ID存放到对象
- 是否了解什么读写锁么? 读写锁(readers-writer lock),将一个锁分成两个,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。 读锁和写锁互斥:防止读脏数据。 例如:ReentrantReadWriteLock
- 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么? 通过死循环一直尝试获取锁,如果发生死锁则会一直自旋,所以会带来一定的额外开销
- synchronized 是可重入锁么? 是
二、CAS
定义
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比 较)
- 如果比较相等,将 B 写入V。(交换)
- 返回操作是否成功。
多线程同时对某个资源进行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 背后的原理
面试题:
- 什么是偏向锁? 对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁, 降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁 的,只有当其他线程尝试竞争偏向锁才会被释放。
- java 的 synchronized 是怎么实现的,有了解过么? 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。 偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。 重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
3.synchronized锁优化(重点)
JDK1.6锁升级的过程:无锁->偏向锁(第一个线程第一次访问,将线程ID存储在对象投中的偏行锁标识)->轻量级锁(自旋)->重量级锁
转载地址:https://blog.csdn.net/ZhangHahaaha/article/details/117335588 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!