一、常见锁策略
1.乐观锁
定义
乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正
式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
执行流程
实现
Atomic*家族
AtomicInteger count = new AtomicInteger(0);//int count = 0;
count.getAndIncrement();//i++
count.incrementAndGet();//++i
System.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 <MAXSIZE ; i++) {
//count++;
count.getAndIncrement();
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <MAXSIZE ; i++) {
//count--;
count.getAndDecrement();
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果"+count);
问题
并不总是能处理所有问题,所以会引入一定的系统复杂度
问题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存储在对象投中的偏行锁标识)->轻量级锁(自旋)->重量级锁