Java并发协作控制之lock与semaphore
文章目录
1,前言
在前面三种多线程方式:Thread、Executor、Fork-Join,第一种我们通过start调用run就一直运行到底,在Executor、Fork-Join里面我们就根本看不到线程,我们只需要创建任务,然后把任务分配下去,线程就回去执行,实际上线程当中发生了上面事情我们也不知道。
从前面的这三种线程的方式可以看出,这样的线程之间是缺少协作的,也就是说每个线程都是独立的,它从一开始启动,就一直运行到底。这样的粗粒度的线程控制在大部分情况下都是能够胜任大部分的任务要求。
但是在某些情况下,可能会出现一些需要更细粒度的线程控制,希望说,1号线程和2号线程在执行到某个地方时要回合一下,前面三种多线程的方式是无法胜任这样的任务的。
前面学习过一个简单的线程之间的协作:synchronized 同步,限定只有一个线程能进入关键区,这种方法简单粗暴,性能损失有点大。如果说一部分代码,我们运行几个线程能够同时进入,它就做不到,所以就需要更细粒度的线程控制。
2,Lock
2.1 Lock的概念
Lock是synchronized的升级版,它也可以实现同步的效果,并且它能够实现更复杂的协作控制。
-
实现更复杂的临界区(关键区域)结构
-
tryLock方法可以预判锁是否空闲
-
允许读写分离的操作,多个读,一个写。
也就是,对于有些数据,在写的时候是排他的,一次只能允许一个线程去访问它;在读的时候是共享的,允许多个线程同时访问。
-
性能更好
Lock类主要有以下两把锁:
- ReentrantLock类,可重入的互斥锁。最重要的两个方法:lock和unlock。
- ReentrantReadWriteLock类,可重入的读写锁。最重要的两个方法:lock和unlock。
2.2 Lock的使用
例如:
- 有家奶茶店,点单的时候需要排队。
- 假设想买奶茶的人如果看到需要排队,就决定不买。又假设奶茶店有一个老板和多名员工,记单方式比较原始,只有一个订单本。
- 老板负责写订单,员工不断地查看订单本得到信息来制作奶茶,老板写新订单时员工不能看订单。
- 多个员工可同时看订单本,在员工看时老板不能写新订单。
简单实现:
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockExample {
private static final ReentrantLock queueLock = new ReentrantLock(); // 可重入的互斥锁
private static final ReentrantReadWriteLock orderLock = new ReentrantReadWriteLock(); // 可重入的读写锁
public static void main(String[] args) {
buyMilkTea();
//handleOrder();
}
private static void buyMilkTea() {
LockExample lockExample = new LockExample();
int STU_CNT = 10;
Thread[] students = new Thread[STU_CNT];
for (int i = 0; i < STU_CNT; i++) {
students[i] = new Thread(() -> {
try {
long walkingTime = (long) (Math.random() * 1000);
Thread.sleep(walkingTime);
LockExample.tryBuyMilkTea();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
students[i].start();
}
}
private static void handleOrder() {
LockExample lockExample = new LockExample();
Thread boss = new Thread(() -> {
while (true) {
try {
long waitingTime = (long) (Math.random() * 1000);
Thread.sleep(waitingTime);
lockExample.addOrder();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
boss.start();
int WORKER_CNT = 3;
Thread[] workers = new Thread[WORKER_CNT];
for (int i = 0; i < WORKER_CNT; i++) {
workers[i] = new Thread(() -> {
while (true) {
try {
long workingTime = (long) (Math.random() * 1000);
Thread.sleep(workingTime);
lockExample.viewOrder();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
worker[i].start();
}
}
private static void tryBuyMilkTea() throws InterruptedException {
boolean flag = true;
while (flag) {
if (queueLock.tryLock()) {
// queueLock.lock();
/* tryLock 是尝试加锁,如果这个资源被其他线程加锁了,当前线程就会转去做其他的事
* lock 的话,如果这个资源被其他线程加锁了,当前线程就会阻塞住 */
long thinkingTime = (long) (Math.random() * 500);
Thread.sleep(thinkingTime);
System.out.println(Thread.currentThread().getName() + ": 来一杯珍珠奶茶,不要珍珠");
flag = false;
queueLock.unlock(); // 释放资源
} else {
System.out.println(Thread.currentThread().getName() + ": 有人在买奶茶,再等等");
}
if (flag) {
Thread.sleep(1000);
}
}
}
private void addOrder() throws InterruptedException {
// writeLock 写锁,排他的,只能一个线程拥有
orderLock.writeLock().lock();
long writingTime = (long) (Math.random() * 1000);
Thread.sleep(writingTime);
System.out.println("老板添加了一笔订单");
orderLock.writeLock().unlock(); // 释放资源
}
private void viewOrder() throws InterruptedException {
// readLock 读锁,可以多个线程共享
orderLock.readLock().lock();
long readingTime = (long) (Math.random() * 1000);
Thread.sleep(readingTime);
System.out.println(Thread.currentThread().getName() + ": 查看订单");
orderLock.readLock().unlock(); // 释放资源
}
}
执行结果如下图:


从结果1中可以看出,买奶茶时一次只允许一个人去买,也就是当一个线程拿到了某个资源的锁,其他的线程就不能对此资源再次加锁了,进而转做其他事,这就是可重入的互斥锁。
在结果2中,老板负责写订单,此时员工不能读订单;员工负责读订单,此时老板不能写订单,这就实现了读和写的分离。3名员工可以同时查看订单,也就是允许多个线程进入一个关键区。
3,Semaphore
3.1 Semaphore的概念
Semaphore是信号量的意思,本来就是轨道上用用来变换线路的信号灯,在1965年的时候由Dijkstra提出,并应用到计算机系统里面来。
信号量本质上是一个计数器,计数器大于0可以使用,等于0不能使用。比如一个停车场有5个可用车位,而现在有10辆车想要停放,每停放一辆车,可用车位减1,当第六辆车想要停放时,可用车位已经为0了,此时,它必须等待有人开走才能去停车。
计数器可以设置多个并发量,如,一个停车场有5个可用车位,就是限制了同时只能有5个访问。它比Lock更进一步,可以控制多个同时访问关键区。
Semaphore的两个重要方法:
- acquire:获取一个信号量,信号量 -1
- release:释放一个信号量,信号量 +1
3.2 Semaphore的使用
如上述所讲的停车的例子:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final Semaphore placeSemaphore = new Semaphore(5); // 代表有5个车位
public static void main(String[] args) throws InterruptedException {
int tryToPark_CNT = 10; // 表示有10辆车要停放
SemaphoreExample semaphoreExample = new SemaphoreExample();
Thread[] parkers = new Thread[tryToPark_CNT];
for (int i = 0; i < tryToPark_CNT; i++) {
parkers[i] = new Thread(() -> {
try {
long randomTime = (long) (Math.random() * 1000);
Thread.sleep(randomTime);
if (semaphoreExample.parking()) {
long parkingTime = (long) (Math.random() * 1200);
Thread.sleep(parkingTime);
semaphoreExample.leaving();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
parkers[i].start();
}
/**
join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用parkers[i]线程的join方法,则main线程放弃cpu控制权,并返回parkers[i]线程继续执行直到线程parkers[i]执行完毕
所以结果是parkers[i]线程执行完后,才到主线程执行,相当于在main线程中同步parkers[i]线程,parkers[i]执行完了,main线程才有执行的机会
可以试试 30-32 注释与否 34行的输出结果
*/
for (int i = 0; i < tryToPark_CNT; i++) {
parkers[i].join();
}
System.out.println("main is exiting");
}
public boolean parking() {
// 尝试申请信号量
if (placeSemaphore.tryAcquire()) {
System.out.println(Thread.currentThread().getName() + ": 停车成功");
return true;
} else {
System.out.println(Thread.currentThread().getName() + ": 没有车位");
return false;
}
}
public void leaving() {
// 释放信号量
placeSemaphore.release();
System.out.println(Thread.currentThread().getName() + ": 开走");
}
执行结果:

通过上面的例子可以看到Semaphore信号量的使用还是比较简单的。