Java常用锁
1. 并发竞争概述
竟态条件:多线程在临界区执行,由于代码执行序列不可预知而导致无法预测结果
解决方式:
(1)阻塞式:sync, Lock(ReentrantLock)
(2)非阻塞式:Cas方式(自旋式)
2. Synchronized
2.1 使用方法
(1)实例方法加锁:锁住实例对象
public class MyClass {
private int count;
public synchronized void increment() {
count++;
}
}
在实例化对象上进行锁的判断
(2)静态方法加锁:锁住类对象
public class MyClass {
private static int count;
public static synchronized void increment() {
count++;
}
}
在静态类MyClass上进行锁的判断
(3)加锁在代码块上:
public class MyClass {
private int count;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
我们创建了一个名为 lock 的对象,并在每个方法中使用 synchronized 关键字来获取这个对象的锁。当一个线程进入 synchronized 代码块时,它会获取 lock 对象的锁,然后执行代码块中的代码。其他线程如果想要进入这个代码块,必须等待当前线程释放锁之后才能获取锁并执行代码块中的代码。
2.2 synchronized锁竞争
![](https://img.haomeiwen.com/i24340055/4728923d5609e94d.png)
2.3 synchronized加锁原理
(1)早期实现:sync为JVM内置锁,(JVM层面)基于Monitor(监视器)机制实现,(Linux层面)依赖底层操作系统的Mutux(互斥量)实现,由于进行了内核态及用户态的切换,性能较低
(2)优化后:通过锁升级实现加锁过程:偏向锁,自旋锁,轻量级,重量级
(3)monitor(监视器)原理:
同步方法由Au_SYNCHRONIZED标志实现, 同步代码块由monitorenter与monitorexit实现;
是管理共享变量以及对共享变量操作的过程,让它们支持并发;
synchronized中wait(), notify(), notifyAll()等方法有monitor实现;
补充:
java线程等待方法:sleep, wait, join, unpark
名称 | 作用 | 特点 |
---|---|---|
sleep | 让当前线程暂停执行一段时间,再继续执行 | Thread 类的静态方法线程不会释放锁,其他线程无法访问被锁定的资源 |
wait | 让当前线程暂停执行,直到其他线程调用该对象的 notify() 或 notifyAll() 方法唤醒它 | 在调用 wait() 方法时,线程会释放锁,其他线程可以访问被锁定的资源。wait() 方法必须在 synchronized 块中调用,否则会抛出 IllegalMonitorStateException 异常 |
join | 当前线程会暂停执行,直到另一个线程执行完毕 | Thread 类的实例方法,可以让当前线程等待另一个线程执行完毕。在调用 join() 方法时,当前线程会暂停执行,直到另一个线程执行完毕。 |
LockSupport.park() | LockSupport 类的静态方法,可以让当前线程暂停执行,直到其他线程调用该线程的 unpark() 方法唤醒它 | LockSupport.park() 方法可以用于线程的阻塞和唤醒,例如等待某个条件的满足后再继续执行 |
注:sleep() 方法和 wait() 方法可以在任何地方调用,而 join() 方法和 LockSupport.park() 方法必须在线程内部调用。sleep() 方法和 wait() 方法可以让线程等待一段时间后继续执行,而 join() 方法和 LockSupport.park() 方法可以让线程等待其他线程的执行或唤醒。
(4)sync加锁解锁流程图:
(5)对象内存布局
![](https://img.haomeiwen.com/i24340055/b7a6b7f097de0ab0.png)
2.4 synchronized锁升级
2.4.1 偏向锁:
(1)流程:程序启动时,将对象的标记设置为偏向锁,表示该对象目前没有竞争,可以被当前线程独占;
当其他线程访问该对象时,会检查该对象的标记,如果是偏向锁,则会判断当前线程是否是偏向锁的拥有者,如果是,则直接获取锁,否则会升级为轻量级锁或重量级锁;
优点:减少锁竞争
(2)撤销条件:
obj.wait() 偏向锁撤销,升级为重量级锁
notify(), notifyAll() 升级为轻量级锁
2.4.2 锁升级过程:
(1)在偏向锁的基础上,有线程P2加入竞争,会检查该对象的标记,如果当前线程不是偏向锁的拥有者,则会升级为轻量级锁,轻量级锁(又称自旋锁)的功能是让P2线程不断自旋(while + cas)
注:若线程P1此时sleep,无竞争,则仍为偏向锁
(2)线程自旋达到一定次数失败,进行线程阻塞(切换到内核态),此时为重量级锁;
重量级锁是一种基于操作系统的锁,它的作用是在获取锁时,将当前线程挂起,等待锁的拥有者释放锁。重量级锁的效率比较低,因为它需要进行线程的上下文切换和内核态和用户态之间的切换。
3. ReentrantLock原理
3.1 操作系统中的并发处理--管程
![](https://img.haomeiwen.com/i24340055/0ac7f8828f14ceff.png)
3.2 JVM层面对管程的实现
synchronized:
objectMonitor & cxq(cas owner)-> 等效于同步等待队列
waitset -> 等效于条件等待队列
3.3 java中管程实现
(1)抽象层面:
同步等待队列实现:CAS机制, volatile int state,等待队列
条件等待队列实现:conditional await, signal(), signalAll(), 出队入队条件
(2)工具层面:
抽象队列同步器AQS
3.4 AQS原理
java.util.concurrent包中年基础能力组件,常用于实现依赖状态同步器
3.4.1 特性
(1)阻塞等待队列:由条件队列,同步队列共同实现
(2)共享/独占锁:共享锁:semaphore(信号量), CountDownLatch
独占锁:ReentrantLock
(3)公平/非公平锁
(4)是否可重入:state表示状态
(5)是否允许中断:设置中断标志位
3.4.2 两种等待队列
(1)同步等待队列:维护获取资源(锁)失败后的线程
(2)条件等待队列:
调用await()时释放锁,线程加入条件队列
signal()时唤醒条件队列中的线程,加入同步等待队列中,等待获取
![](https://img.haomeiwen.com/i24340055/aaa6ee3e1dfcd9b7.png)
3.4.3 ReentrantLock应用
解决库存超卖问题
public string reduceStock() {
int stock = Integer.parseInt(StringRedisTemplate.OPsForValue().get("stock"));
if (stock > 0) {
stock--;
StringRedisTemplate.opsForValue().set("stock", stock);
}
return "end";
}
在多线程下会造成stock超卖
3.4.4 小总结
解决并发问题
(1)非阻塞式:cas + 自旋锁
(2)阻塞式:synchronized, ReentrantLock
公平锁/非公平锁区别:
(1)非公平锁效率更高(对CPU的使用效率高);不用经历从等待队列唤醒线程的步骤,有新的任务尝试获取锁资源即可成功
(2)公平锁保证线程不会长时间陷于饥饿状态
3.4.5 条件队列的使用场景
boolean volatile hashcig;
public static Conditional cond = lock.newCondition();
public static cigratte() {
lock.lock();
try {
while (!hashcig) {
try {
log("no cigurate, wait");
cond.await();
} catch(Exception e) e.print();
}
log("begin work);
finally {
lock.unlock();
}
}
}
唤醒逻辑
new Thread(() -> {
lock.lock();
try {
hashcig = true;
cond.signal("上烟了");
} ...
}
3.4.6 流程图
4. AQS框架扩展
4.1 semaphore信号量
(1)作用:实现互斥锁,通过同时只能有一个线程能获取信息量;用于实现限流功能;
(2)工作原理:设置窗口值,当未达到该窗口值时,工作线程正常工作;当窗口值为0时,不支持新的线程执行任务,新的线程阻塞,进入阻塞队列;当上一批线程释放资源后,到等待队列中唤醒等待线程,重新执行以上流程;
注:即根据窗口值一个批次一个批次执行多线程任务,上个批次执行时窗口值为0,则后面的线程阻塞;
![](https://img.haomeiwen.com/i24340055/99fa65e59b97fc71.png)
(3)举例
semphore windows = new semphore(3); // 设置窗口值
for (;;) {
new Thread(new Runnable() {
public void run() {
try {
windows.acquire(); // 占用窗口, windows - 1
Thread.sleep();
} finally {
window.release(); // 释放
}
}
}
}
4.2 CountDownLatch
(1)作用:是一个同步协作类,允许一个或多个线程等待,直到其他线程完成操作集;使用给定的count初始化,await后阻塞直到当前计数值;由于countDown方法调用达到0,count为0后所有等待线程均释放;
注:将所有线程阻塞,直到countDown减到0值,调用unpark唤醒阻塞线程
(2)实现原理:每次countDownLatch都执行release(1)减1,当减到0时调用unpark唤醒阻塞线程;即:state != 0,线程阻塞;state = 0,线程继续执行;
(3)使用场景图:
![](https://img.haomeiwen.com/i24340055/82d02cb9e282255c.png)
4.3 CyclicBarrier 循环屏障
4.3.1 使多个线程在cyclicBarrier上阻塞,直到满足某条件放开
![](https://img.haomeiwen.com/i24340055/0515554eefaa7c8e.png)
4.3.2 使用实例:
CyclicBarrier cyc = new CyclicBarrier(3);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.current().getName());
cyc.await(); // 未达到三个线程,则进行线程阻塞;总计达到三个线程时,继续向下执行
System.out.println(Thread.current().getName());
Thread.sleep(5000);
System.out.println(Thread.current().getName());
} catch (...) { }
}
}
4.3.3 使用场景:
实现人满发车场景,线程池用于线程复用
AutoicInteger con = new AutoicInteger();
ThreadPoolExecutor thread = new ThreadPoolExecutor(5, 5, 1000, Timeunit.Seconds, new ArrayBlockingQueue<>(100), (r)->new Thread(r, con.addAndGet(1)), new ThreadPoolExecutor.Abortpolicy());
CyclicBarrier cyc = new CyclicBarrier(5, () -> System.out.print("start"));
for (int i = 0; i < 10; i++) {
ThreadPoolExecutor.submit(new Runner(cyc));
}
4.3.4 实现流程:
(1)首先加独占锁, 锁住int state
(2)进入条件队列,阻塞线程,并释放锁;由于finally块中的释放锁lock.unlock();需重新获取锁
(3)由于仅有同步队列逻辑实现中有唤醒线程,并重新获取锁的实现,这里进行对进入同步队列的复用
(4)唤醒同步队列绑定的线程节点
4.3.5 两种重要的队列:
(1)同步等待队列:主要用于维护获取锁失败时入队的线程
(2)条件等待队列:调用await()方法时会释放锁,线程会加入条件队列;
调用signal()唤醒时将条件队列中线程节点移动到同步队列中,等待再次获取锁
4.4 ReentrantReadWriteLock
针对读多写少场景,该工具特性为:读读可并发;写写、写读互斥;提高读写场景并发量
4.4.1 总结,实现一把锁有哪些设计?
(1)cas + 自旋尝试
(2)管程方式:synchronized
(3)AQS方式:Cas + 同步队列
(4)实现tryAcquired()方法 -> ReentrantLock等
4.4.2 如何设计并实现一把读写分离锁?
答:采用高地位打标设计
高16位不为0:有读锁,每多一位表示多一个线程持有读锁,最高为65535个
低16位不为0:有写锁,没多一位表示多一次重入