洞察探索open banking如何通过小程序容器技术助力金融企业实现数据安全和数字化转型
913
2023-01-14
程序猿必须要掌握的多线程安全问题之锁策略详解
一、常见的锁策略
1.1 乐观锁
乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正 式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如 何去做。乐观锁的性能比较高。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会 上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。 乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。
乐观锁的使用场景:
import java.util.concurrent.atomic.AtomicInteger;
public class happylock {
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 count.getAndIncrement(); } } }); t1.start(); t1.join(); Thread t2= new Thread(new Runnable() { @Override public void run() { for(int j = 0;j count.getAndDecrement(); } } }); t2.start(); t2.join(); System.out.println("结果"+count); } //结果是0,如果不加AtomicInteger,那么线程执行完以后不会是0,存在线程不安全! } 1.2 悲观锁 悲观锁:他认为通常情况下会出现并发冲突,所以它在一开始就加锁; synchronized 就是悲观锁 1.3 读写锁 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁, 就会产生极大的性能损耗。所以读写锁因此而产生。 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。 把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有。读锁是共享锁,而写锁是非公享锁。 读写锁的应用方法: import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Readerlock { //读写锁的具体实现 public static void main(String[] args) { //创建读写锁 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //分离读锁 ReentrantReadWriteLock.ReadLock readLock= ReadWriteLock.ReadLock(); //分离写锁 ReentrantReadWriteLock.WriteLock readLock= ReadWriteLock.WriteLock(); } } 1.4 公平锁与非公平锁 公平锁:锁的获取顺序必须合线程方法的先后顺序是保存一致的,就叫公平锁 优点:执行时顺序的,所以结果是可以预期的 非公平锁:锁的获取方式循序和线程获取锁的顺序无关。优点:性能比较高 1.5 自旋锁(Spin Lock) 按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。 你可以简单的认为自旋锁就是下面的代码 只要没抢到锁,就死等。 自旋锁的缺点: 缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。 1.6 可重入锁 可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数 里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因 可重入锁也叫做递归锁)。 Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重入的。 1.7 相关题目 面试题: 1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢? 乐观锁——> CAS ——> Atomic.(CAS是由v(内存值) A(预期值)B(新值))组成,然后执行的时候是使用V=A对比,如果结果为true,这表明没有并发冲突,则可以直接进行修改,否则返回错误信息。* 2.有了解什么读写锁么? 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。 把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有 3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么? 按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。 你可以简单的认为自旋锁就是下面的代码 只要没抢到锁,就死等。 自旋锁的缺点: 缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。 4.synchronized 是可重入锁么? synchronized 是可重入锁, 代码如下: public class Chonglock { private static Object lock = new Object(); public static void main(String[] args) { //第一次进入锁 synchronized (lock){ System.out.println("第一次进入锁"); synchronized (lock){ System.out.println("第二次进入锁"); } } } } 二、CAS问题 2.1 什么是CAS问题 CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。 1. 比较 A 与 V 是否相等。(比较) 2. 如果比较相等,将 B 写入 V。(交换) 3. 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。 2.2 CAS 是怎么实现的 针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲: java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作; unsafe 的 CAS 依 赖 了 的 是 jvm 针 对 不 同 的 操 作 系 统 实 现 的 Atomic::cmpxchg(一个原子性的指令) /Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。 简而言之,是因为硬件予以了支持,软件层面才能做到。 2.3 CAS 有哪些应用 2.3.1 实现自旋锁 public class SpinLock { private AtomicReference public void lock(){ Thread current = Thread.currentThread(); // 不放弃 CPU,一直在这里旋转判断 while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign.compareAndSet(current, null); } } 用于实现原子类 示例代码: public class AtomicInteger { public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } } public class Unsafe { public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } } 三、ABA问题 3.1 什么是ABA问题 ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。 3.2 实现ABA问题场景 我来举一个例子,如果你向别人转钱,你需要转100元,但是你点击了两次转钱,第一次会成功,但是第二次肯定会失败,但是,在你点击第二次转钱的同一时刻,你的公司给你转了100元工资,那么你就会莫名其妙的把100又转了出去,你丢失了100,别人也没有获得100. 代码演示: 1.正常转钱流程 import java.util.concurrent.atomic.AtomicReference; public class Aba { //ABA问题的演示 private static AtomicReference money = new AtomicReference(100);//转账 public static void main(String[] args) { //转账线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("点击第一次转出100"+result); } }); t1.start(); //转账线程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("点击第二次转出100"+result); if(!result){ System.out.println("余额不足,无法转账!"); } } }); t2.start(); } } 2.错误操作后: import java.util.concurrent.atomic.AtomicReference; public class ABas { private static AtomicReference money = new AtomicReference(100);//转账 public static void main(String[] args) throws InterruptedException { //转账出线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第一次"+result); } }); t1.start(); t1.join(); //转入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100); System.out.println("转账"+result); } }); t3.start(); //转账线程2 t3.join(); Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第二次"+result); } }); t2.start(); } } 解决ABA方法 解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会 出现老的值。 代码实现: import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class Abaack { //private static AtomicReference money = new AtomicReference(100);//转账 private static AtomicStampedReference money = new AtomicStampedReference(100,1); // public static void main(String[] args) throws InterruptedException { //转账出线程1 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(); //转入100 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(); //转账线程2 t3.join(); 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(); //Integer的高速缓存是-128--127(AtomicStampedReference) //如果大于127,那么就开始new对象了 /* * 解决方法,调整边界值*/ } } 四、总结 以上就是今天要讲的内容,本文仅仅简单介绍了锁策略,解决线程安全。
count.getAndIncrement();
}
}
});
t1.start();
t1.join();
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0;j count.getAndDecrement(); } } }); t2.start(); t2.join(); System.out.println("结果"+count); } //结果是0,如果不加AtomicInteger,那么线程执行完以后不会是0,存在线程不安全! } 1.2 悲观锁 悲观锁:他认为通常情况下会出现并发冲突,所以它在一开始就加锁; synchronized 就是悲观锁 1.3 读写锁 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁, 就会产生极大的性能损耗。所以读写锁因此而产生。 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。 把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有。读锁是共享锁,而写锁是非公享锁。 读写锁的应用方法: import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Readerlock { //读写锁的具体实现 public static void main(String[] args) { //创建读写锁 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //分离读锁 ReentrantReadWriteLock.ReadLock readLock= ReadWriteLock.ReadLock(); //分离写锁 ReentrantReadWriteLock.WriteLock readLock= ReadWriteLock.WriteLock(); } } 1.4 公平锁与非公平锁 公平锁:锁的获取顺序必须合线程方法的先后顺序是保存一致的,就叫公平锁 优点:执行时顺序的,所以结果是可以预期的 非公平锁:锁的获取方式循序和线程获取锁的顺序无关。优点:性能比较高 1.5 自旋锁(Spin Lock) 按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。 你可以简单的认为自旋锁就是下面的代码 只要没抢到锁,就死等。 自旋锁的缺点: 缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。 1.6 可重入锁 可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数 里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因 可重入锁也叫做递归锁)。 Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重入的。 1.7 相关题目 面试题: 1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢? 乐观锁——> CAS ——> Atomic.(CAS是由v(内存值) A(预期值)B(新值))组成,然后执行的时候是使用V=A对比,如果结果为true,这表明没有并发冲突,则可以直接进行修改,否则返回错误信息。* 2.有了解什么读写锁么? 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。 把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有 3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么? 按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。 你可以简单的认为自旋锁就是下面的代码 只要没抢到锁,就死等。 自旋锁的缺点: 缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。 4.synchronized 是可重入锁么? synchronized 是可重入锁, 代码如下: public class Chonglock { private static Object lock = new Object(); public static void main(String[] args) { //第一次进入锁 synchronized (lock){ System.out.println("第一次进入锁"); synchronized (lock){ System.out.println("第二次进入锁"); } } } } 二、CAS问题 2.1 什么是CAS问题 CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。 1. 比较 A 与 V 是否相等。(比较) 2. 如果比较相等,将 B 写入 V。(交换) 3. 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。 2.2 CAS 是怎么实现的 针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲: java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作; unsafe 的 CAS 依 赖 了 的 是 jvm 针 对 不 同 的 操 作 系 统 实 现 的 Atomic::cmpxchg(一个原子性的指令) /Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。 简而言之,是因为硬件予以了支持,软件层面才能做到。 2.3 CAS 有哪些应用 2.3.1 实现自旋锁 public class SpinLock { private AtomicReference public void lock(){ Thread current = Thread.currentThread(); // 不放弃 CPU,一直在这里旋转判断 while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign.compareAndSet(current, null); } } 用于实现原子类 示例代码: public class AtomicInteger { public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } } public class Unsafe { public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } } 三、ABA问题 3.1 什么是ABA问题 ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。 3.2 实现ABA问题场景 我来举一个例子,如果你向别人转钱,你需要转100元,但是你点击了两次转钱,第一次会成功,但是第二次肯定会失败,但是,在你点击第二次转钱的同一时刻,你的公司给你转了100元工资,那么你就会莫名其妙的把100又转了出去,你丢失了100,别人也没有获得100. 代码演示: 1.正常转钱流程 import java.util.concurrent.atomic.AtomicReference; public class Aba { //ABA问题的演示 private static AtomicReference money = new AtomicReference(100);//转账 public static void main(String[] args) { //转账线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("点击第一次转出100"+result); } }); t1.start(); //转账线程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("点击第二次转出100"+result); if(!result){ System.out.println("余额不足,无法转账!"); } } }); t2.start(); } } 2.错误操作后: import java.util.concurrent.atomic.AtomicReference; public class ABas { private static AtomicReference money = new AtomicReference(100);//转账 public static void main(String[] args) throws InterruptedException { //转账出线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第一次"+result); } }); t1.start(); t1.join(); //转入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100); System.out.println("转账"+result); } }); t3.start(); //转账线程2 t3.join(); Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第二次"+result); } }); t2.start(); } } 解决ABA方法 解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会 出现老的值。 代码实现: import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class Abaack { //private static AtomicReference money = new AtomicReference(100);//转账 private static AtomicStampedReference money = new AtomicStampedReference(100,1); // public static void main(String[] args) throws InterruptedException { //转账出线程1 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(); //转入100 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(); //转账线程2 t3.join(); 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(); //Integer的高速缓存是-128--127(AtomicStampedReference) //如果大于127,那么就开始new对象了 /* * 解决方法,调整边界值*/ } } 四、总结 以上就是今天要讲的内容,本文仅仅简单介绍了锁策略,解决线程安全。
count.getAndDecrement();
}
}
});
t2.start();
t2.join();
System.out.println("结果"+count);
}
//结果是0,如果不加AtomicInteger,那么线程执行完以后不会是0,存在线程不安全!
}
1.2 悲观锁
悲观锁:他认为通常情况下会出现并发冲突,所以它在一开始就加锁;
synchronized 就是悲观锁
1.3 读写锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,
就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有。读锁是共享锁,而写锁是非公享锁。
读写锁的应用方法:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Readerlock {
//读写锁的具体实现
public static void main(String[] args) {
//创建读写锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//分离读锁
ReentrantReadWriteLock.ReadLock readLock= ReadWriteLock.ReadLock();
//分离写锁
ReentrantReadWriteLock.WriteLock readLock= ReadWriteLock.WriteLock();
}
}
1.4 公平锁与非公平锁
公平锁:锁的获取顺序必须合线程方法的先后顺序是保存一致的,就叫公平锁 优点:执行时顺序的,所以结果是可以预期的
非公平锁:锁的获取方式循序和线程获取锁的顺序无关。优点:性能比较高
1.5 自旋锁(Spin Lock)
按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。
你可以简单的认为自旋锁就是下面的代码
只要没抢到锁,就死等。
自旋锁的缺点:
缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。
1.6 可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数 里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因 可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的。
1.7 相关题目
面试题:
1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
乐观锁——> CAS ——> Atomic.(CAS是由v(内存值) A(预期值)B(新值))组成,然后执行的时候是使用V=A对比,如果结果为true,这表明没有并发冲突,则可以直接进行修改,否则返回错误信息。*
2.有了解什么读写锁么?
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有
3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。
你可以简单的认为自旋锁就是下面的代码
只要没抢到锁,就死等。
自旋锁的缺点:
缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。
4.synchronized 是可重入锁么?
synchronized 是可重入锁,
代码如下:
public class Chonglock {
private static Object lock = new Object();
public static void main(String[] args) {
//第一次进入锁
synchronized (lock){
System.out.println("第一次进入锁");
synchronized (lock){
System.out.println("第二次进入锁");
}
}
}
}
二、CAS问题
2.1 什么是CAS问题
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。 1. 比较 A 与 V 是否相等。(比较) 2. 如果比较相等,将 B 写入 V。(交换) 3. 返回操作是否成功。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。
2.2 CAS 是怎么实现的
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
unsafe 的 CAS 依 赖 了 的 是 jvm 针 对 不 同 的 操 作 系 统 实 现 的 Atomic::cmpxchg(一个原子性的指令)
/Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
2.3 CAS 有哪些应用
2.3.1 实现自旋锁
public class SpinLock {
private AtomicReference
public void lock(){
Thread current = Thread.currentThread();
// 不放弃 CPU,一直在这里旋转判断
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread(); sign.compareAndSet(current, null);
}
}
用于实现原子类
示例代码:
public class AtomicInteger {
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
public class Unsafe {
public final int getAndAddInt(Object var1, long var2, int var4) { int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}
三、ABA问题
3.1 什么是ABA问题
ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。
3.2 实现ABA问题场景
我来举一个例子,如果你向别人转钱,你需要转100元,但是你点击了两次转钱,第一次会成功,但是第二次肯定会失败,但是,在你点击第二次转钱的同一时刻,你的公司给你转了100元工资,那么你就会莫名其妙的把100又转了出去,你丢失了100,别人也没有获得100.
代码演示:
1.正常转钱流程
import java.util.concurrent.atomic.AtomicReference;
public class Aba {
//ABA问题的演示
private static AtomicReference money = new AtomicReference(100);//转账
public static void main(String[] args) {
//转账线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0);
System.out.println("点击第一次转出100"+result);
}
});
t1.start();
//转账线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0);
System.out.println("点击第二次转出100"+result);
if(!result){
System.out.println("余额不足,无法转账!");
}
}
});
t2.start();
}
}
2.错误操作后:
import java.util.concurrent.atomic.AtomicReference;
public class ABas {
private static AtomicReference money = new AtomicReference(100);//转账
public static void main(String[] args) throws InterruptedException {
//转账出线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0);
System.out.println("第一次"+result);
}
});
t1.start();
t1.join();
//转入100
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,100);
System.out.println("转账"+result);
}
});
t3.start();
//转账线程2
t3.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0);
System.out.println("第二次"+result);
}
});
t2.start();
}
}
解决ABA方法
解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会
出现老的值。
代码实现:
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Abaack {
//private static AtomicReference money = new AtomicReference(100);//转账
private static AtomicStampedReference money = new AtomicStampedReference(100,1);
//
public static void main(String[] args) throws InterruptedException {
//转账出线程1
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();
//转入100
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();
//转账线程2
t3.join();
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();
//Integer的高速缓存是-128--127(AtomicStampedReference)
//如果大于127,那么就开始new对象了
/*
* 解决方法,调整边界值*/
}
}
四、总结
以上就是今天要讲的内容,本文仅仅简单介绍了锁策略,解决线程安全。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~