5.1 显式锁
Java内置锁的功能相对单一,不具备一些比较高级的锁功能,比如:
- 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃,不至于无限等下去。
- 可中断抢锁:在抢锁时,外部线程给抢锁线程发一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
多个等待队列:为锁维持多个等待队列,以便提高锁的效率。比如在生产者-消费者模式实现中,生产者和消费者共用一把锁,该锁上维持两个等待队列,即一个生产者队列和一个消费者队列。
5.1.1 显式锁Lock接口
5.1.2 可重入锁ReentrantLock
可重入的含义: 表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。比如,同一线程在外层函数获得锁后,在内层函数能再次获取该锁,甚至多次抢占到同一把锁。
下面是一段对可重入锁进行两次抢占和释放的伪代码,具体如下:
lock.lock(); // 第一次获取锁
lock.lock(); // 第二次获取锁,重新进入
try {
// 临界区代码块
} finally {
lock.unlock(); // 释放锁
lock.unlock(); // 第二次释放锁
}
- 独占的含义:在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。
一个简单地使用ReentrantLock进行同步累加的演示案例如下:
package com.crazymakercircle.demo.lock;
// 省略import
public class LockTest
{
@org.junit.Test
public void testReentrantLock()
{
// 每个线程的执行轮数
final int TURNS = 1000;
// 线程数
final int THREADS = 10;
//线程池,用于多线程模拟测试
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
//创建一个可重入、独占的锁对象
Lock lock = new ReentrantLock();
// 倒数闩
CountDownLatch countDownLatch = new CountDownLatch(THREADS);
long start = System.currentTimeMillis();
//10个线程并发执行
for (int i = 0; i < THREADS; i++)
{
pool.submit(() ->
{
try
{
//累加 1000 次
for (int j = 0; j < TURNS; j++)
{
//传入锁,执行一次累加
IncrementData.lockAndFastIncrease(lock);
}
Print.tco("本线程累加完成");
} catch (Exception e)
{
e.printStackTrace();
}
//线程执行完成,倒数闩减少一次
countDownLatch.countDown();
});
}
出于“分离变与不变”的设计原则,这里将临界区使用锁的代码进行了抽取和封装,形成一个可以复用的独立类——IncrementData累加类,具体代码如下:
package com.crazymakercircle.demo.lock;
// 省略import
//封装锁的使用代码
public class IncrementData
{
public static int sum = 0;
public static void lockAndFastIncrease(Lock lock)
{
lock.lock(); //step1:抢占锁
try
{
//step2:执行临界区代码
sum++;
} finally
{
lock.unlock(); //step3:释放锁
}
}
// 省略其他代码
}
5.1.3 使用显式锁的模板代码
- 使用lock()方法抢锁的模板代码
```java
//创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock
Lock lock = new SomeLock();
lock.lock(); //step1:抢占锁
try {
} finally {//step2:抢锁成功,执行临界区代码
}lock.unlock(); //step3:释放锁
2. 调用tryLock()方法非阻塞抢锁的模板代码
调用tryLock()方法非阻塞抢占锁,大致的模板代码如下:
```java
//创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock
Lock lock = new SomeLock();
if (lock.tryLock()) { //step1:尝试抢占锁
try {
//step2:抢锁成功,执行临界区代码
} finally {
lock.unlock(); //step3:释放锁
}
}
else
{
//step4:抢锁失败,执行后备动作
}
调用tryLock(long time,TimeUnit unit)方法抢锁的模板代码 ```java //创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock Lock lock = new SomeLock(); //抢锁时阻塞一段时间,如1秒 if (lock.tryLock(1, TimeUnit.SECONDS)) { //step1:限时阻塞抢占
try {
//step2:抢锁成功,执行临界区代码
} finally {
lock.unlock(); //step3:释放锁
}
} else {
//限时抢锁失败,执行后备操作
}
<a name="JhvDl"></a>
## 5.1.4 基于显式锁进行“等待-通知”方式的线程间通信
与Object对象的wait、notify两类方法相类似,基于Lock显式锁,JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition
1. Condition接口的主要方法
```java
public interface Condition
{
//方法1:等待。此方法在功能上与 Object.wait()语义等效
//使当前线程加入 await() 等待队列中,并释放当前锁
//当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁
void await() throws InterruptedException;
//方法2:通知。此方法在功能上与Object.notify()语义等效
// 唤醒一个在 await()等待队列中的线程
void signal();
//方法3:通知全部。唤醒 await()等待队列中所有的线程
//此方法与object.notifyAll()语义上等效
void signalAll();
//方法4:限时等待。此方法与await()语义等效
//不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待
//线程等待超时返回false,其他情况返回true
boolean await(long time, TimeUnit unit) throws InterruptedException;
}
显式锁Condition演示案例 ```java package com.crazymakercircle.demo.lock; // 省略import public class ReentrantCommunicationTest {
// 创建一个显式锁
static Lock lock = new ReentrantLock();
// 获取一个显式锁绑定的Condition对象
static private Condition condition = lock.newCondition();
// 等待线程的异步目标任务
static class WaitTarget implements Runnable
{
public void run()
{
lock.lock(); // ①抢锁
try
{
Print.tcfo("我是等待方");
condition.await(); // ② 开始等待,并且释放锁
Print.tco("收到通知,等待方继续执行");
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
lock.unlock();//释放锁
}
}
}
//通知线程的异步目标任务
static class NotifyTarget implements Runnable
{
public void run()
{
lock.lock(); //③抢锁
try
{
Print.tcfo("我是通知方");
condition.signal(); // ④发送通知
Print.tco("发出通知了,但是线程还没有立马释放锁");
} finally
{
lock.unlock(); //⑤释放锁之后,等待线程才能获得锁
}
}
}
public static void main(String[] args) throws InterruptedException
{
//创建等待线程
Thread waitThread = new Thread(new WaitTarget(), "WaitThread");
//启动等待线程
waitThread.start();
sleepSeconds(1); //稍等一下
//创建通知线程
Thread notifyThread = new Thread(new NotifyTarget(), "NotifyThread");
//启动通知线程
notifyThread.start();
}
}
<a name="pILQn"></a>
## 5.1.5 LockSupport
LockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法。
1. LockSupport的常用方法
```java
// 无限期阻塞当前线程
public static void park();
// 唤醒某个被阻塞的线程
public static void unpark(Thread thread);
// 阻塞当前线程,有超时时间的限制
public static void parkNanos(long nanos);
// 阻塞当前线程,直到某个时间
public static void parkUntil(long deadline);
// 无限期阻塞当前线程,带blocker对象,用于给诊断工具确定线程受阻塞的原因
public static void park(Object blocker);
// 限时阻塞当前线程,带blocker对象
public static void parkNanos(Object blocker, long nanos);
// 获取被阻塞线程的blocker对象,用于分析阻塞的原因
public static Object getBlocker(Thread t);
LockSupport的演示实例 ```java package com.crazymakercircle.demo.lock; // 省略import public class LockSupportDemo {
public static class ChangeObjectThread extends Thread
{
public ChangeObjectThread(String name)
{
super(name);
}
@Override
public void run()
{
Print.tco("即将进入无限时阻塞");
//阻塞当前线程
LockSupport.park();
if (Thread.currentThread().isInterrupted())
{
Print.tco("被中断了,但仍然会继续执行");
} else
{
Print.tco("被重新唤醒了");
}
}
}
//LockSupport 测试用例
@org.junit.Test
public void testLockSupport()
{
ChangeObjectThread t1 = new ChangeObjectThread("线程一");
ChangeObjectThread t2 = new ChangeObjectThread("线程二");
//启动线程一
t1.start();
sleepSeconds(1);
//启动线程二
t2.start();
sleepSeconds(1);
//中断线程一
t1.interrupt();
//唤醒线程二
LockSupport.unpark(t2);
}
}
3. LockSupport.park()和Thread.sleep()的区别
3. LockSupport.park()与Object.wait()的区别
下面的演示代码演示在LockSupport.park()执行之前,通过执行LockSupport.unPark()唤醒一个线程,具体如下:
```java
package com.crazymakercircle.demo.lock;
// 省略import
public class LockSupportDemo
{
@org.junit.Test
public void testLockSupport2()
{
Thread t1 = new Thread(() ->
{
try
{
Thread.sleep(1000); //使sleep阻塞当前线程,时长为1秒
} catch (InterruptedException e)
{
e.printStackTrace();
}
Print.tco("即将进入无限时阻塞");
//使用LockSupport.park()阻塞当前线程
LockSupport.park();
Print.tco("被重新唤醒了");
}, "演示线程"); //通过匿名对象创建一个线程
t1.start();
//唤醒一次没有使用 LockSupport.park()阻塞的线程
LockSupport.unpark(t1);
//再唤醒一次没有调用 LockSupport.park()阻塞的线程
LockSupport.unpark(t1);
sleepSeconds(2);
//中断线程一
//第三唤醒调用 LockSupport.park()阻塞的线程
LockSupport.unpark(t1);
}
// 省略其他
}
5.1.6 显式锁的分类
显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。
- 可重入锁和不可重入锁
- 悲观锁和乐观锁
- 公平锁和非公平锁
- 可中断锁和不可中断锁
-
5.2 悲观锁和乐观锁
独占锁其实就是一种悲观锁,Java的synchronized是悲观锁。悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区。虽然悲观锁的逻辑非常简单,但是存在不少问题。
5.2.1 悲观锁存在的问题
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。
5.2.2 通过CAS实现乐观锁
5.2.3 不可重入的自旋锁
作为演示,这里先实现一个简单版本的自旋锁——不可重入的自旋锁,具体的代码如下:
package com.crazymakercircle.demo.lock.custom;
// 省略import
public class SpinLock implements Lock
{
/**当前锁的拥有者
* 使用Thread 作为同步状态
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 抢占锁
*/
@Override
public void lock()
{
Thread t = Thread.currentThread();
//自旋
while (owner.compareAndSet(null, t))
{
// DO nothing
Thread.yield();//让出当前剩余的CPU时间片
}
}
/**
* 释放锁
*/
@Override
public void unlock()
{
Thread t = Thread.currentThread();
//只有拥有者才能释放锁
if (t == owner.get())
{
// 设置拥有者为空,这里不需要 compareAndSet操作
// 因为已经通过owner做过线程检查
owner.set(null);
}
}
// 省略其他代码
}
5.2.4 可重入的自旋锁
为了实现可重入锁,这里引入一个计数器,用来记录一个线程获取锁的次数。一个简单的可重入的自旋锁的代码大致如下:
package com.crazymakercircle.demo.lock.custom;
// 省略import
public class ReentrantSpinLock implements Lock
{
/**当前锁的拥有者
* 使用拥有者 Thread 作为同步状态,而不是使用一个简单的整数作为同步状态
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 记录一个线程重复获取锁的次数
* 此变量为同一个线程在操作,没有必要加上volatile保障可见性和有序性
*/
private int count = 0;
/**
* 抢占锁
*/
@Override
public void lock()
{
Thread t = Thread.currentThread();
// 如果是重入,增加重入次数后返回
if (t == owner.get())
{
++count;
return;
}
//自旋
while (owner.compareAndSet(null, t))
{
// DO nothing
Thread.yield();//让出当前剩余的CPU时间片
}
}
/**
* 释放锁
*/
@Override
public void unlock()
{
Thread t = Thread.currentThread();
//只有拥有者才能释放锁
if (t == owner.get())
{
if (count > 0)
{
// 如果重入的次数大于0, 减少重入次数后返回
--count;
} else
{
// 设置拥有者为空
// 这里不需要 compareAndSet, 因为已经通过owner做过线程检查
owner.set(null);
}
}
}
// 省略其他代码
}
自旋锁的问题:
在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。
5.2.5 CAS可能导致“总线风暴”
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final class Unsafe {
//Unsafe中的CAS操作
public final native boolean compareAndSwapInt(
Object o, //操作对象
long offset, //字段偏移
int expected, //预期值
int x); //待更新的值
// 省略不相干代码
}
5.2.6 CLH自旋锁
实现CLH锁的一个学习版本 ```java package com.crazymakercircle.demo.lock.custom; // 省略import public class CLHLock implements Lock {
/**
* 当前节点的线程本地变量
*/
private static ThreadLocal<Node> curNodeLocal = new ThreadLocal();
/**
* CLHLock队列的尾部指针,使用AtomicReference,方便进行CAS操作
*/
private AtomicReference<Node> tail = new AtomicReference<>(null);
public CLHLock()
{
//设置尾部节点
tail.getAndSet(Node.EMPTY);
}
//加锁操作:将节点添加到等待队列的尾部
@Override
public void lock()
{
Node curNode = new Node(true, null);
Node preNode = tail.get();
//CAS自旋:将当前节点插入队列的尾部
while (!tail.compareAndSet(preNode, curNode))
{
preNode = tail.get();
}
//设置前驱节点
curNode.setPrevNode(preNode);
// 自旋,监听前驱节点的locked变量,直到其值为false
// 若前驱节点的locked状态为true,则表示前一个线程还在抢占或者占有锁
while (curNode.getPrevNode().isLocked())
{
//让出CPU时间片,提高性能
Thread.yield();
}
// 能执行到这里,说明当前线程获取到了锁
// Print.tcfo("获取到了锁!!!");
//将当前节点缓存在线程本地变量中,释放锁会用到
curNodeLocal.set(curNode);
}
//释放锁
@Override
public void unlock()
{
Node curNode = curNodeLocal.get();
curNode.setLocked(false);
curNode.setPrevNode(null); //help for GC
curNodeLocal.set(null); //方便下一次抢锁
}
//虚拟等待队列的节点
@Data
static class Node
{
public Node(boolean locked, Node prevNode)
{
this.locked = locked;
this.prevNode = prevNode;
}
// true:当前线程正在抢占锁,或者已经占有锁
// false:当前线程已经释放锁,下一个线程可以占有锁了
volatile boolean locked;
// 前一个节点,需要监听其locked字段
Node prevNode;
// 空节点
public static final Node EMPTY = new Node(false, null);
}
// 省略其他代码
}
3. CLH锁的原理分析
3. 举例说明:CLH锁的抢占过程
3. 举例说明:CLH锁的释放过程
3. CLH锁的优缺点
<a name="esjQ7"></a>
# 5.3 公平锁与非公平锁
<a name="nhlQE"></a>
## 5.3.1 非公平锁实战
使用ReentrantLock锁作为非公平锁的实战用例,具体代码如下:
```java
package com.crazymakercircle.basic.demo.lock;
// 省略import
public class LockTest
{
/**
* 非公平锁测试用例
*/
@org.junit.Test
public void testNotFairLock() throws InterruptedException
{
//创建可重入锁,默认的非公平锁
Lock lock = new ReentrantLock(false);
//创建Runnable可执行实例
Runnable r = () -> IncrementData.lockAndIncrease(lock);
//创建4个线程
Thread[] tArray = new Thread[4];
for (int i = 0; i < 4; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动4个线程
for (int i = 0; i < 4; i++)
{
tArray[i].start();
}
Thread.sleep(Integer.MAX_VALUE);
}
// 省略其他代码
}
5.3.2 公平锁实战
什么是公平锁呢?公平锁是指多个线程按照申请锁的顺序来获取锁,抢锁成功的次序体现为FIFO(先进先出)顺序。虽然ReentrantLock锁默认是非公平锁,但可以通过构造器指定该锁为公平锁,具体的代码如下:
//可重入、公平锁对象
Lock lock = new ReentrantLock(true);
下面是一个简单的公平锁实战案例。此实战案例并没有使用ReentrantLock锁,而是使用前面自定义的CLHLock锁进行演示,具体的实战代码如下:
package com.crazymakercircle.basic.demo.lock;
// 省略import
public class LockTest
{
/**
* 公平锁测试用例
*/
@org.junit.Test
public void testFairLock() throws InterruptedException
{
//创建为公平锁的类型
Lock lock = new CLHLock();
//创建Runnable可执行实例
Runnable r = () -> IncrementData.lockAndIncrease(lock);
//创建4个线程
Thread[] tArray = new Thread[4];
for (int i = 0; i < 4; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动4个线程
for (int i = 0; i < 4; i++)
{
tArray[i].start();
}
Thread.sleep(Integer.MAX_VALUE);
}
// 省略其他代码
}
5.4 可中断锁与不可中断锁
可中断锁是指抢占过程可以被中断的锁,JUC的显式锁(如ReentrantLock)是一个可中断锁。不可中断锁是指抢占过程不可以被中断的锁,如Java的synchronized内置锁就是一个不可中断锁。
5.4.1 锁的可中断抢占
- lockInterruptibly()
- tryLock(long timeout,TimeUnit unit)
```java
package com.crazymakercircle.demo.lock;
// 省略import
public class IncrementData
{
}public static int sum = 0;
//演示方法:可中断抢锁
public static void lockInterruptiblyAndIncrease(Lock lock)
{
Print.synTco(" 开始抢占锁");
try
{
lock.lockInterruptibly();
} catch (InterruptedException e)
{
Print.synTco("抢占被中断,抢锁失败");
// e.printStackTrace();
return;
}
try
{
Print.synTco("抢到了锁,同步执行1秒");
sleepMilliSeconds(1000);
sum++;
if (Thread.currentThread().isInterrupted())
{
Print.synTco("同步执行被中断");
}
} catch (Exception e)
{
e.printStackTrace();
} finally
{
lock.unlock();
}
}
// 省略其他代码
以上代码的测试用例具体如下:
```java
package com.crazymakercircle.basic.demo.lock;
// 省略import
public class LockTest
{
//测试用例:抢锁过程可中断
@org.junit.Test
public void testInterruptLock() throws InterruptedException
{
//创建可重入锁,默认的非公平锁
Lock lock = new ReentrantLock();
//创建Runnable可执行任务实例
Runnable r = () -> IncrementData.lockInterruptiblyAndIncrease(lock);
Thread t1 = new Thread(r, "thread-1"); //创建第1条线程
Thread t2 = new Thread(r, "thread-2"); //创建第2条线程
t1.start(); //启动第1个线程
t2.start(); //启动第2个线程
sleepMilliSeconds(100);
Print.synTco( "等待100毫秒,中断两个线程");
t1.interrupt(); //启动第1个线程
t2.interrupt(); //启动第2个线程
Thread.sleep(Integer.MAX_VALUE);
}
// 省略其他代码
}
5.4.2 死锁的监测与中断
JDK 8中包含的ThreadMXBean接口提供了多种监视线程的方法,其中包括两个死锁监测的方法,具体如下:
- findDeadlockedThreads
- findMonitorDeadlockedThreads
ThreadMXBean的实例可以通过JVM管理工厂ManagementFactory去获取,具体的获取代码如下:
//获取ThreadMXBean的实例
public static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
在这里举一个死锁监测与中断的案例。首先定义一段需要抢占两把锁才能进入的临界区代码,具体如下:
package com.crazymakercircle.demo.lock;
// 省略import
public class TwoLockDemo
{
//演示代码:使用两把锁,通过可以中断的方式抢锁
public static void useTowlockInterruptiblyLock(
Lock lock1, Lock lock2) {
String lock1Name =
lock1.toString().replace("java.util.concurrent.locks.", "");
String lock2Name =
lock2.toString().replace("java.util.concurrent.locks.", "");
Print.synTco(" 开始抢第一把锁, 为:" + lock1Name);
try
{
lock1.lockInterruptibly();
} catch (InterruptedException e)
{
Print.synTco(" 被中断,抢第一把锁失败, 为:" + lock1Name);
//e.printStackTrace();
return;
}
try
{
Print.synTco(" 抢到了第一把锁, 为:" + lock1Name);
Print.synTco(" 开始抢第二把锁, 为:" + lock2Name);
try
{
lock2.lockInterruptibly();
} catch (InterruptedException e)
{
Print.synTco(" 被中断,抢第二把锁失败,为:" + lock2Name);
//e.printStackTrace();
return;
}
try
{
Print.synTco(" 抢到了第二把锁:" + lock2Name);
Print.synTco("do something ");
//等待1000ms
sleepMilliSeconds(1000);
} catch (Exception e)
{
e.printStackTrace();
} finally
{
lock2.unlock();
Print.synTco(" 释放了第二把锁, 为:" + lock2Name);
}
} catch (Exception e)
{
e.printStackTrace();
} finally
{
lock1.unlock();
Print.synTco(" 释放了第一把锁, 锁为:" + lock1Name);
}
}
以上代码的测试用例如下:
package com.crazymakercircle.basic.demo.lock;
// 省略import
public class LockTest
{
//获取ThreadMXBean
public static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
//测试用例:抢占两把锁,造成死锁,然后进行死锁监测和部分中断
@org.junit.Test
public void testDeadLock() throws InterruptedException
{
//创建可重入锁,默认的非公平锁
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
//Runnable异步执行目标实例1: 先抢占lock1,再抢占lock2
Runnable r1 = () ->
TwoLockDemo.useTowlockInterruptiblyLock(lock1, lock2);
//Runnable异步执行目标实例2: 先抢占lock2,再抢占 lock1
Runnable r2 = () ->
TwoLockDemo.useTowlockInterruptiblyLock(lock2, lock1);
Thread t1 = new Thread(r1, "thread-1"); //创建第1个线程
Thread t2 = new Thread(r2, "thread-2"); //创建第2个线程
t1.start(); //启动第1个线程
t2.start(); //启动第2个线程
//等待一段时间再执行死锁监测
Thread.sleep(2000);
Print.tcfo("等待2秒,开始死锁监测和处理");
//获取到所有死锁线程的id
long[] deadlockedThreads = mbean.findDeadlockedThreads();
if (deadlockedThreads.length > 0)
{
Print.tcfo("发生了死锁,输出死锁线程的信息");
//遍历数组获取所有的死锁线程id
for (long pid : deadlockedThreads)
{
//此方法用于获取不带有堆栈跟踪信息的线程数据
//hreadInfo threadInfo = mbean.getThreadInfo(pid);
//此方法用于获取带有堆栈跟踪信息的线程数据
ThreadInfo threadInfo = mbean.getThreadInfo(
pid, Integer.MAX_VALUE);
Print.tcfo(threadInfo);
}
Print.tcfo("中断一个死锁线程,这里是线程:" + t1.getName());
t1.interrupt(); //中断一个死锁线程
}
}
// 省略其他代码
}
5.5 共享锁与独占锁
5.5.1 独占锁
5.5.2 共享锁Semaphore
共享锁使用示例
package com.crazymakercircle.demo.lock;
// 省略import
public class SemaphoreTest
{
@org.junit.Test
public void testShareLock() throws InterruptedException
{
// 排队总人数(请求总数)
final int USER_TOTAL = 10;
// 可同时受理业务的窗口数量(同时并发执行的线程数)
final int PERMIT_TOTAL = 2;
// 线程池,用于多线程模拟测试
final CountDownLatch countDownLatch =
new CountDownLatch(USER_TOTAL);
// 创建信号量,含有两个许可
final Semaphore semaphore = new Semaphore(PERMIT_TOTAL);
AtomicInteger index = new AtomicInteger(0);
// 创建Runnable可执行实例
Runnable r = () ->
{
try
{
//阻塞开始获取许可
semaphore.acquire(1);
//获取了一个许可
Print.tco( DateUtil.getNowTime()
+ ", 受理处理中...,服务号: " + index.incrementAndGet());
//模拟业务操作: 处理排队业务
Thread.sleep(1000);
//释放一个信号
semaphore.release(1);
} catch (Exception e)
{
e.printStackTrace();
}
countDownLatch.countDown();
};
//创建10个线程
Thread[] tArray = new Thread[USER_TOTAL];
for (int i = 0; i < USER_TOTAL; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动10个线程
for (int i = 0; i < USER_TOTAL; i++)
{
tArray[i].start();
}
countDownLatch.await();
}
}
5.5.3 共享锁CountDownLatch
package com.crazymakercircle.visiable;
// 省略import
class Driver
{
private static final int N = 100; // 乘客数
public static void main(String[] args) throws InterruptedException
{ //step1:创建倒数闩,设置倒数的总数
CountDownLatch doneSignal = new CountDownLatch(N);
//取得CPU密集型线程池
Executor e = ThreadUtil.getCpuIntenseTargetThreadPool();
for (int i = 1; i <= N; ++i) // 启动报数任务
e.execute(new Person(doneSignal, i));
doneSignal.await(); //step2:等待报数完成,倒数闩计数值为0
Print.tcfo("人到齐,开车"); }
static class Person implements Runnable
{
private final CountDownLatch doneSignal;
private final int i;
Person(CountDownLatch doneSignal, int i)
{
this.doneSignal = doneSignal;
this.i = i;
}
public void run()
{
try
{
//报数
Print.tcfo("第" + i + "个人已到");
doneSignal.countDown(); //step3:倒数闩减少1
} catch (Exception ex)
{
}
}
}
}
5.6 读写锁
JUC包中的读写锁接口为ReadWriteLock,主要有两个方法,具体如下:
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
5.6.1 读写锁ReentrantReadWriteLock
接着进行代码演示,读锁是共享锁,写锁是排他锁:
package com.crazymakercircle.demo.lock;
// 省略import
public class ReadWriteLockTest
{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个读写锁
final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
//获取读锁
final static Lock READ_LOCK = LOCK.readLock();
//获取写锁
final static Lock WRITE_LOCK = LOCK.writeLock();
//对共享数据的写操作
public static Object put(String key, String value)
{
WRITE_LOCK.lock(); //抢写锁
try
{
Print.tco(DateUtil.getNowTime()
5.6.2 锁的升级与降级
锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。具体的演示代码如下:
package com.crazymakercircle.demo.lock;
// 省略import
public class ReadWriteLockTest2
{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个读写锁
final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
//获取读锁
final static Lock READ_LOCK = LOCK.readLock();
//获取写锁
final static Lock WRITE_LOCK = LOCK.writeLock();
//对共享数据的写操作
public static Object put(String key, String value)
{
WRITE_LOCK.lock();
try
{
Print.tco(DateUtil.getNowTime()
+ " 抢占了WRITE_LOCK,开始执行write操作");
Thread.sleep(1000);
String put = MAP.put(key, value);
Print.tco( "尝试降级写锁为读锁");
//写锁降级为读锁(成功)
READ_LOCK.lock();
Print.tco( "写锁降级为读锁成功");
return put;
} catch (Exception e)
{
e.printStackTrace();
} finally
{
READ_LOCK.unlock();
WRITE_LOCK.unlock();
}
return null;
}
//对共享数据的读操作
public static Object get(String key)
{
READ_LOCK.lock();
try
{
Print.tco(DateUtil.getNowTime()
+ " 抢占了READ_LOCK,开始执行read操作");
Thread.sleep(1000);
String value = MAP.get(key);
Print.tco( "尝试升级读锁为写锁");
//读锁升级为写锁(失败)
WRITE_LOCK.lock();
Print.tco("读锁升级为写锁成功");
return value;
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
WRITE_LOCK.unlock();
READ_LOCK.unlock();
}
return null;
}
public static void main(String[] args)
{
//创建Runnable可执行实例
Runnable writeTarget = () -> put("key", "value");
Runnable readTarget = () -> get("key");
//创建1个写线程,并启动
new Thread(writeTarget, "写线程").start();
//创建1个读线程
new Thread(readTarget, "读线程").start();
}
}
5.6.3 StampedLock
StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。
- 悲观读锁的获取与释放 ```java //获取普通读锁(悲观读锁),返回long类型的印戳值 public long readLock()
//释放普通读锁(悲观读锁),以取锁时的印戳值作为参数 public void unlockRead(long stamp)
2. 写锁的获取与释放
```java
//获取写锁,返回long类型的印戳值
public long writeLock()
//释放写锁,以获取写锁时的印戳值作为参数
public void unlockWrite(long stamp)
- 乐观读的印戳获取与有效性判断 ```java //获取乐观读,返回long类型的印戳值,返回0表示当前锁处于写锁模式,不能乐观读 public long tryOptimisticRead()
//判断乐观读的印戳值是否有效,以tryOptimisticRead返回的印戳值作为参数 public long tryOptimisticRead(long stamp)
StampedLock的演示案例
```java
package com.crazymakercircle.demo.lock;
// 省略import
public class StampedLockTest
{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个印戳锁
final static StampedLock STAMPED_LOCK = new StampedLock();
//对共享数据的写操作
public static Object put(String key, String value)
{
long stamp = STAMPED_LOCK.writeLock(); //尝试获取写锁的印戳
try
{
Print.tco(getNowTime() + " 抢占了WRITE_LOCK,开始执行write操作");
Thread.sleep(1000);
String put = MAP.put(key, value);
return put;
} catch (Exception e)
{
e.printStackTrace();
} finally
{
Print.tco(getNowTime() + " 释放了WRITE_LOCK");
STAMPED_LOCK.unlockWrite(stamp); //释放写锁
}
return null;
}
//对共享数据的悲观读操作
public static Object pessimisticRead(String key)
{
Print.tco(getNowTime() + "LOCK进入过写模式,只能悲观读");
//进入了写锁模式,只能获取悲观读锁
long stamp = STAMPED_LOCK.readLock(); //尝试获取读锁的印戳
try
{
//成功获取到读锁,并重新获取最新的变量值
Print.tco(getNowTime() + " 抢占了READ_LOCK");
String value = MAP.get(key);
return value;
} finally
{
Print.tco(getNowTime() + " 释放了READ_LOCK");
STAMPED_LOCK.unlockRead(stamp); //释放读锁
}
}
//对共享数据的乐观读操作
public static Object optimisticRead(String key)
{
String value = null;
//尝试进行乐观读
long stamp = STAMPED_LOCK.tryOptimisticRead();
if (0 != stamp)
{
Print.tco(getNowTime() + "乐观读的印戳值,获取成功");
sleepSeconds(1); //模拟耗费时间1秒
value = MAP.get(key);
} else // 0 == stamp 表示当前为写锁模式
{
Print.tco(getNowTime() + "乐观读的印戳值,获取失败");
//LOCK已经进入写模式,使用悲观读方法
return pessimisticRead(key);
}
//乐观读操作已经间隔了一段时间,期间可能发生写入
//所以,需要验证乐观读的印戳值是否有效,即判断LOCK是否进入过写模式
if (!STAMPED_LOCK.validate(stamp))
{
//乐观读的印戳值无效,表明写锁被占用过