锁(Locks)
1 ReentrantLock
应用demo
可重入锁,是一种使用递归无堵塞的同步机制
比 synchronized 更强大、更灵活的锁机制,可以减少死锁发生的概率
默认为非公平锁,可以自定义为公平锁
底层采用 AQS 实现,通过内部 Sync 集成 AQS
简单应用:
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/14
*/
public class Concurrent07 {
private int count;
private final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Concurrent07 c = new Concurrent07();
CountDownLatch count = new CountDownLatch(2);
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> {
// 加锁
c.lock.lock();
try {
for (int i = 0; i < 100000000; i++) {
c.count ++;
}
count.countDown();
} finally {
// 释放锁
c.lock.unlock();
}
});
service.execute(() -> {
c.lock.lock();
try {
for (int i = 0; i < 100000000; i++) {
c.count ++;
}
count.countDown();
} finally {
c.lock.unlock();
}
});
count.await();
service.shutdown();
System.out.println("计算结果:" + c.count);
}
}
运行结果:
如果不对两个线程进行加锁操作,那么最终结果将不是预期的。
公平锁与非公平锁
公平锁的简单应用
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/15
*/
public class Concurrent08 {
/**
* 公平锁,fair参数设置为true
* 非公平锁,fair参数设置为false(默认)
*/
private static final Lock lock = new ReentrantLock(true);
public static void runTest(String name, int time) {
lock.lock();
try {
System.out.println("线程 " + name + "获取了锁");
TimeUnit.SECONDS.sleep(time);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程 " + name + "释放了锁");
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 2; i++) {
executorService.execute(() -> runTest("A", 1));
executorService.execute(() -> runTest("B", 1));
executorService.execute(() -> runTest("C", 1));
}
executorService.shutdown();
}
}
运行结果:
第二次获取锁的顺序和第一次一致,也就是等待锁的时间最长的优先获取锁。
非公平锁的结果可能如下(也可能与公平锁一致,具有不确定性):
响应中断
我们创建两个线程造成死锁,然后使用其中一个线程中断,然后另一个线程就可以正常获取锁了。
案例如下:
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/15
*/
public class Concurrent09 {
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
lock1.lockInterruptibly();
System.out.println("t1 获取了 lock1");
lock2.lockInterruptibly();
System.out.println("t1 获取了 lock2");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
}
});
Thread t2 = new Thread(() -> {
try {
lock2.lockInterruptibly();
System.out.println("t2 获取了 lock2");
lock1.lockInterruptibly();
System.out.println("t2 获取了 lock1");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
lock1.unlock();
}
});
t1.start();
t2.start();
Thread.sleep(2000);
t1.interrupt();
}
}
运行结果:
两个线程都在相互等待对方释放锁导致死锁,此时 t1 线程中断,然后t1线程中的lock1释放锁,t2线程就能成功获取到t1锁了。
限时等待
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/15
*/
public class Concurrent10 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
boolean flag = false;
try {
flag = lock.tryLock(1000, TimeUnit.MILLISECONDS);
if (flag) {
System.out.println("线程A获取锁成功");
TimeUnit.SECONDS.sleep(2);
} else {
System.out.println("线程A获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (flag) {
lock.unlock();
}
}
});
executorService.execute(() -> {
boolean flag = false;
try {
flag = lock.tryLock(1000, TimeUnit.MILLISECONDS);
if (flag) {
System.out.println("线程B获取锁成功");
TimeUnit.SECONDS.sleep(2);
} else {
System.out.println("线程B获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// ReentrantLock的一个方法:isHeldByCurrentThread()
// 可用于判断当前线程是否获取到Lock锁
if (flag) {
lock.unlock();
}
}
});
executorService.shutdown();
}
}
运行结果:
线程B会尝试 1s 后打印获取锁失败信息。
ReentrantLock与Synchronized
synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
相比于synchronized,ReentrantLock 在功能上更加丰富,它具有 可重入、可中断、可限时、公平锁 等特点
2 ReentrantReadWriteLock
上一节我们使用了ReentrantLock锁,该锁效果与synchronized的关键字的功能差不多,如果在写多读少的环境下,毋庸置疑的使用它也是比较合适的,但是在写少读多的环境下进行加锁,势必造成不必要的性能浪费。在只读的情况下并不存在线程安全问题,其他线程也可读取,但是此时加上了互斥锁,那么会大大降低性能,因此我们需要一个能够在读线程的情况下,其他线程也可以获取锁。我们可以使用ReentrantReadWriteLock。
读写锁,两把锁:共享锁-读锁,排它锁:写锁
支持公平性、非公平性、可重入、锁降级
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
读写锁的应用
简单应用如下:
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/16
*/
public class Concurrent11 {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();
private static Random rand = new Random(100);
private static int pc;
public void read() {
readLock.lock();
try {
System.out.println("线程 " + Thread.currentThread() + " 读取数据 ----> " + pc);
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write(String name) {
writeLock.lock();
try {
int s = rand.nextInt(99);
System.out.println("线程" + name + " 写入数据 ---> " + s);
pc = s;
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
Concurrent11 c = new Concurrent11();
ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < 10; i++) {
service.execute(c::read);
}
service.execute(() -> c.write("E"));
service.execute(() -> c.write("D"));
service.shutdown();
}
}
运行结果如下:
运行过程,前面十个线程是几乎同时打印数据,然后等待大约 1s 后面两个线程才开始打印数据。由此可知,读取数据时,其他读取线程也可以获取读锁,而无法获取写锁。写数据时,读写锁都无法获取,即读锁是共享锁,写锁是排它锁。
锁降级
即是由写锁降级为读锁
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/16
*/
public class Concurrent12 {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();
private int num;
public static void main(String[] args) {
Concurrent12 c = new Concurrent12();
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5; i++) {
service.execute(c::read);
}
service.execute(c::write);
for (int i = 0; i < 8; i++) {
service.execute(c::read);
}
service.shutdown();
}
public void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread() + " 读取 ---> " + num);
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
num = 12;
Thread.sleep(600);
} catch (Exception e) {
e.printStackTrace();
}
readLock.lock();
try {
writeLock.unlock();
System.out.println(Thread.currentThread() + " 锁降级 ---> " + num);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}
运行结果如下:
首先获取读锁,此时无法获取写锁,等待锁释放,而后write获取写锁,修改数据后降级为读锁时,其他的读锁也能获取读锁,结果中锁降级与下面的读取结果同时打印。
ReentrantReadWriteLock不支持锁升级。
3 Condition
Lock提供条件Condition,对线程的等待和唤醒更加详细和灵活
内部维护一个Condition队列。当前线程调用await方法后,将会以当前线程构造为一个结点Node,并将该节点放到该队列的尾部
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的方式是lock.newCondition()
我们来看一个使用Lock + Condition多线程顺序打印A B C …的例子
应用demo
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/16
*/
public class Concurrent03 {
private int count;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public static void main(String[] args) {
Concurrent03 c = new Concurrent03();
ExecutorService service = Executors.newCachedThreadPool();
service.execute(c::printA);
service.execute(c::printB);
service.execute(c::printC);
service.shutdown();
}
public void printA() {
lock.lock();
try {
while (true){
if (count % 3 != 0) {
condition.await();
}
Thread.sleep(300);
System.out.print("A ");
count ++;
condition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (true){
if (count % 3 != 1) {
condition.await();
}
Thread.sleep(300);
System.out.print("B ");
count ++;
condition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (true){
if (count % 3 != 2) {
condition.await();
}
Thread.sleep(300);
System.out.print("C ");
count ++;
condition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果如下:
4 LockSupport
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作,LockSupport为构建同步组件的基础工具。 LockSupport定义了一组 以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。 Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开
简单应用
/**
* @author kenewstar
* @version 1.0
* @date 2021/5/16
*/
public class Concurrent13 {
static class LockSupportTest extends Thread {
@Override
public void run() {
System.out.println("thread......");
LockSupport.park();
System.out.println("执行逻辑......");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new LockSupportTest();
thread.start();
Thread.sleep(1000);
LockSupport.unpark(thread);
System.out.println("main end.....");
}
}
运行结果如下:
当调用park()方法时会阻塞当前线程,调用unpark(Thread t)时,会唤醒指定线程。