Java中常用锁
Java中常用锁
1 各种锁概念及使用
1.1 synchronized
非公平锁
JDK早期 重量级锁,向OS申请系统锁
锁升级改进概念:syschronized锁开始只记录线程ID(偏向锁,偏向锁只记录线程的ID,实际不是真正的加锁 只记录状态),如果线程争抢,升级为自旋锁(自旋锁占用CPU不经过内核态,自旋锁适用于执行时间短,线程少),10次自旋之后升级为重量级锁-向OS申请系统锁(进入等待队列)。
错误的锁对象
synchronized不能用string常量(常量拼接后默认使用Stringbuild.toString创建新对象,导致锁的不是同一个对象)、Integer(在-128~127范围使用栈赋值方式声明变量,超过该范围会创建一个新对象) 、Long(与Integer范围相同)。
为了防止锁对象发生改变,通常需要在锁定的值上面加final修饰。
1.2 volatile
- 保证线程可见
volatile可见性是针对引用,引用内部发生了改变对其他线程依旧不可见。
MESI CPU缓存一致性
public class ThreadVolatile {
private volatile static boolean running = true;
public static void m1() {
System.out.println("T1 start!!");
while (running) {
// System.out.println("hello");
}
System.out.println("T1 end!!");
}
public static void main(String[] args) {
try {
new Thread(ThreadVolatile::m1).start();
Thread.sleep(500);
running = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 输出:
* T1 start!!
* 结论:
* 1)当m1方法中System.out.println被注释掉,且running没有被
volatile修饰时,
* 程序出现死锁.running=false线程间不可见;
* 2)当m1方法中System.out.println输出后,且running没有被
volatile修饰时,
* 线程正常执行完成.原因是println方法从使用了synchronized修饰;
* 3)当running被volatile修饰时,程序正常执行
*/
}
- 禁止指令重排(CPU)
指令重排即CPU和编译器为了提高执行效率会进行指令重排。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条内存屏障则会告诉编译器和CPU,不管什么指令都不能和这条指令重排,也就是说通过插入内存屏障,就能禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
volatile禁止的是JVM级别的指令重排,不是CPU级别的。
public class VolatileDemo {
private /*volatile*/ boolean statu = true;
void m(){
System.out.println("m==start");
while (statu){
}
System.out.println("m==end");
}
public static void main(String[] args) {
ValatileDemo t1 = new ValatileDemo();
new Thread(()->{
t1.m();
},"t2").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.statu = false;
}
}
/**
* 1)没有加volatile
* 执行结果: m==start
* 程序一直卡住,没有按我们期望t1.statu = false 后停止
*
* 2)加了volatile后程序按期望停止
*
*/
- volatile不保证原子性
volatile不能保证多个线程共同修改变量时所带来的不一致问题
public class VolatileDemo2 {
volatile int count = 0;
// int count = 0;
private /*synchronized*/ void c() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
VolatileDemo2 demo = new VolatileDemo2();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
demo.c();
}).start();
}
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(demo.count);
}
}
/**
* 测试结果:每次运算结果都不是我们期望的10万
* 原因:volatile保证了可见性但是没有保证原子性
* 解决方案:去掉volatile 给累加方法添加synchronize
*/
1.3 CAS
无锁优化 自旋 乐观锁
java.util.concurrent.atomic.Atomic…
类都是通过CAS实现来保证线程安全,cas(期望值,原来值,更新值)。
CAS底层:CompareAndSet
(比较并交换)。CAS是CPU原支持,执行判断过程是不能不打乱。
CAS在极端情况下可能会出现ABA问题
什么是ABA问题?
考虑如下操作:
并发1:获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功
并发2:将数据修改成B
并发3:将数据修改回A
并发1:CAS乐观锁,检测发现初始值还是A,进行数据修改
上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。
ABA问题的优化
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。
优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。
常见实践:“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。
1.4 Lock
可中断锁 公平锁/非公平锁
Lock需手动开启和释放锁,可以使用tryLock尝试获取锁,不管锁定与否,方法都将继续执行,可以通过tryLock返回值判断是否锁定,也可以根据tryLock的时间判断是否成功获取锁。使用synchronize遇到异常JVM会自动释放锁,底层也是CAS实现,当进入等待队列后也会存在锁升级使用LockSupport。
Lock的锁定后可以允许被打断
Lock默认非公平锁,可以设置未公平锁
// 非公平锁
Lock unFairLock = new ReentrantLock();
// 公平锁
Lock fairLock = new ReentrantLock(true);
1.5 CountDownLatch与CyclicBarrier
- CountDownLatch
减计数方式,计算为0时释放所有等待的线程,计数为0时无法重置。调用counDown()
方法计数减一,调用await()
方法只进行阻塞,对计数没有任何影响。不可重复利用。 - CyclicBarrier
加计数方式,计数达到指定值时释放所有等待线程,计划达到指定值时,计算置为0重新开始。调用await()
方法计数加1,若加1后的值不等于构造方法指定的值,则线程阻塞。可重复利用。
1.6 ReadWriteLock
读写锁 写锁为排他锁,读锁为共享锁
读写锁只有读操作的时候不会上锁,当有写操作的时候读操作会被上锁。可以提升读的操作。如果用Re
public class ReadWriteLockDeme {
private int value;
// 读
public void read(Lock lock){
try {
lock.lock();
Thread.sleep(1000);
System.out.println("Read over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
//写
public void write(Lock lock,int v){
try {
lock.lock();
this.value = v;
Thread.sleep(1000);
System.out.println("write over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
//独占锁
// Lock reentrantLock = new ReentrantLock();
//读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
ReadWriteLockDeme deme = new ReadWriteLockDeme();
for (int i = 0; i < 2; i++) {
int value = i;
new Thread(()->{
// deme.write(reentrantLock, value);
deme.write(writeLock, value);
}).start();
}
for (int i = 0; i < 20; i++) {
new Thread(()->{
// deme.read(reentrantLock);
deme.read(readLock);
}).start();
}
}
}
/**
* 运行结果:
* 1) 当读写使用ReentrantLock时,无论线程读或写其他线程都将进入等待状态;
* 2) 当使用ReadWriteLock时,当有一个写线程执行写操作时其他所有线程等待,
* 只有读操作时,所有读线程可以共享读,读效率非常高.
*/
1.7 Semaphore
凭证、信号灯。Semaphore创建时会指定凭证数量,当线程获取到凭证时开始执行,未获取凭证的线程进入等待状态。
可用于限流场景。
//默认非公平,传true为公平锁
Semaphore semaphore = new Semaphore(1,false);
new Thread(()->{
try {
//未获取凭证会进入阻塞状态
semaphore.acquire();
System.out.println("T1 Start");
System.out.println("T1 End");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}).start();
new Thread(()->{
try {
//未获取凭证会进入阻塞状态
semaphore.acquire();
System.out.println("T2 Start");
System.out.println("T2 End");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}).start();
/**
* 执行结果:
* T1 Start
* T1 End
* T2 Start
* T2 End
*/
1.8 Exchanger
2个线程之间数据交换。当线程执行到exchange时,都会进入阻塞队列。
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
String value = "T1";
try {
value = exchanger.exchange(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + value);
}, "t1").start();
new Thread(() -> {
String value = "T2";
try {
value = exchanger.exchange(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + value);
},
/**
* 执行结果:
* t2 T1
* t1 T2
*/
1.9 LockSupport
用于创建锁和其他同步类的基本线程阻塞原语。
这个类与使用它的每个线程关联一个许可证(类似信号)。如果许可证可用,将执行,否则阻塞。许可证可以提前获取。但与Semaphore信号灯不同,许可证不会累积,最多有一个。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.print(i+" ");
if(i==3){
//阻塞
LockSupport.park();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
/**
* 输出结果:0 1 2 3 4
* 结论1:
* 提前颁发凭证,t1线程在遇到LockSupport.park()时不会停止.
*/
LockSupport.unpark(t1);
//2秒后唤醒t1线程继续执行
// Thread.sleep(2000);
// System.out.print(" 5秒后 ");
// LockSupport.unpark(t1);
/**
* 输出结果:0 1 5秒后 2 3 4
* 结论2:
* LockSupport.park()线程进入阻塞状态,
* 当遇到LockSupport.unpark(t1)后,t1获取了凭证继续唤醒执行
*/
}
1.10 AQS
Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
ReentrantLock跟常用的Synchronized进行比较
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。主要原理图如下:
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。