深入Java多线程锁策略
Posted Putarmor
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入Java多线程锁策略相关的知识,希望对你有一定的参考价值。
乐观锁
乐观锁认为一般情况下不会发生并发冲突,只有在数据进行更新时候,才会去检测并发冲突,如果没有检测到冲突则直接进行数据修改,若有冲突则返回失败。
CAS机制
CAS:全称为Compare and swap,字面意思“比较并交换”,是乐观锁的一种机制。当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程会收到操作失败的信号,因此可以看出,CAS是乐观锁。
CAS组成部分:V:内存中的值
A:预期的旧值
B:新值
实现原理:如果V==A为true,此时没有并发冲突,将B赋值给V,成功修改;当V!=A,产生了并发冲突,进行自旋,将旧值A修改为内存中的值V,然后再进行比较。
乐观锁的实现
Atomic
使用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:新值
ABA问题
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修改。
解决ABA问题
统一解决方案:每次修改之后增加版本号
使用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锁优化(锁消除)过程是什么
以上是关于深入Java多线程锁策略的主要内容,如果未能解决你的问题,请参考以下文章
Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理