带你深入理解多线程 --- 锁策略篇
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是可重入锁
总结锁策略的问题
你是怎么理解乐观锁和悲观锁的,具体怎么实现?
- 它认为一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突,则直接修改,如果有冲突,则返回失败。
乐观锁 -》 CAS -》 Atomic*,CAS组成是由V(内存值)A(预期值)B(新值)组成,然后执行的时候是使用V==A对比,如果结果为true则表明没有冲突,则可以直接修改,否则不能修改。
CAS是通过调用C++提供的UnSafe中的本地方法(CompareAndSwapXXX)来实现的,C++通过调用操作系统的Atomic::cmpxchg(原子指令)来实现的 - 它认为通常情况下会出现并发冲突,所以它一开始就会加锁
悲观锁 - 》 synchronized 在Java 中是将锁的ID存放到对象头来实现的放到;
synchronized 在jvm层面是通过监视器来实现的;
synchronized在操作系统层面是通过互斥锁mutex实现的
有了解什么是读写锁吗?
- 读写锁就是将锁的粒度分的更细,分为读锁和写锁,并且读锁和写锁之间是互斥的,互斥的原因是为了防止脏数据的诞生(在修改的时候也在读取)
- 读锁是可以多个线程同时拥有的,所以它是共享锁,而写锁是互斥的,它是独占锁,在一个线程操作的时候其他线程不能操作
- 读写锁在java里面可以使用ReentrantReaderWriterLock来去创建,通过.readLock 和 .writeLock来分别得到读锁和写锁
什么是自旋锁,为什么要使用自旋锁策略呢,其缺点是什么?
自旋锁就是通过自己的循环来尝试获取锁,如果获取到了锁他才会停止
初衷是在短期间内进行轻量级的锁定。
如果是死锁,或者锁执行周期过长,那么就会一直尝试获取锁
自旋锁的缺点:如果发生死锁则会一直自旋(循环),所以会带来一定的额外开销
java对于自旋锁的策略:使其有最大执行次数,比如超过64次,则将锁放入队列中进行排队,下次再进行自旋
synchronized 锁优化(锁消除)
jdk1.6锁升级的过程:
- 无锁
- 偏向锁(第一个线程第一次访问)将线程id存储在对象头中的偏向锁标识
- 轻量级锁(自旋)
- 重量级锁
以上是关于带你深入理解多线程 --- 锁策略篇的主要内容,如果未能解决你的问题,请参考以下文章