Java多线程的同步控制记录
一、重入锁
重入锁完全可以代替 synchronized 关键字。在JDK 1.5 早期版本,重入锁的性能优于 synchronized。JDK 1.6 开始,对于 synchronized 做了大量优化,使得两者性能差距不大。
代码示例:
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock(); // 加锁
try {
// doSomething
} finally {
lock.unlock(); // 释放锁
}
}
PS:因为重入锁需要我们手动加锁/释放锁,比较好实现对于逻辑的控制。
注意事项:
- 加锁后记得释放锁,否则其他线程就没有机会访问(比如,临界区资源)。
- 一个线程多次获得锁(其实是同一把锁)。但加了多少次锁,就要释放多少次锁。同时,lock()方法会一直堵塞线程直到释放锁。
- 中断响应 : lockInterruptibly()方法是一个可以对中断进行响应的锁申请动作(在等待锁的过程中,可以响应中断)。
- 锁申请等待限时 : tryLock()方法给定一个等待时间,让线程自动放弃等待,避免死锁(PS:当前线程会尝试获得锁,如果锁未被其他线程占用,则申请成功返回true;否则返回false)。
- 公平锁:
- 通过ReentrantLock(true)的构造函数来启用公平锁。
- 按申请时间顺序来获得锁,不会产生饥饿现象。
- 系统需要维护一个有序队列,实现成本较高且性能相对低下(一般情况下使用非公平锁)。
ReentrantLock的几个重要方法
- lock():获得锁,如果锁已经被占用,则等待。
- lockInterruptibly():获得锁,但优先响应中断。
- tryLock():立刻/给定时间内尝试获得锁(可以开启公平锁)。
- unlock():释放锁。
二、Condition条件
Condition 与重入锁相关联。利用 Condition 可以让线程在合适的时间内等待,或者在某一个特定的时刻得到通知,继续执行。
调用代码如下:
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
常用的方法:
- await() 当前线程等待,同时释放锁。其他线程可以使用 signal() 或 signalAll() 方法使线程获得锁并继续执行。或者线程中断时,也能跳出等待。
- awaitUninterruptibly() 方法与 await() 方法基本相同,,但等待过程中不会响应中断。
- signal() 方法唤醒一个在等待中的线程;signalAll() 方法唤醒所有在等待中的线程。
用一个实例来理解 Condition 对象
代码如下:
public class MyThread implements Runnable {
private static ReentrantLock lock = new ReentrantLock(); //定义重入锁
private static Condition condition = lock.newCondition(); //定义Condition @Override
public void run() {
try {
lock.lock(); //调用 await() 需要先获得锁
condition.await();
System.out.println("这条线程继续执行!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
t1.start();
Thread.sleep(2000);
//通知 t1 线程继续执行
lock.lock(); //调用 signal() 需要先获得锁
condition.signal();
lock.unlock(); //记得释放锁,不然线程没有继续执行
}
}
首先主线程开启副线程(t1)后,休眠2000毫秒。t1线程执行 run() 方法:获得锁后,调用 await() 释放锁并进入等待状态。
主线程被唤醒后,同样需要是获得锁,调用 signal() 唤醒线程但没有释放锁,所以要主动释放锁将其谦让给被唤醒的线程。
一旦线程被唤醒了,它会尝试着获取与之绑定的重入锁,成功获取后就可以继续执行。
在JDK内部,重入锁和 Condition 对象被广泛地使用。
三、允许多个线程同时访问:信号量(Semaphore)
信号量是对锁的扩展,可以指定多个线程同时访问某个资源
- 在构造信号量对象时,必须指定信号量的准入数。
- 申请信号量使用 acquire() 操作,离开时必须使用 release() 释放信号量(PS:若申请了但没有释放,会使可以进入临界区的线程数量越来越少,直到所有的线程均不可访问)。
四、倒计时器 CountDownLatch
这个工具用来控制线程等待,可以让某个线程等待直到倒计时结束,再执行。(PS:就像王者农药一样,需要所有玩家都准备好了才能开始游戏)
代码示例(来源于《实战Java高并发程序设计》,为了便于理解做了一点修改):
public class MyThread implements Runnable {
private static final CountDownLatch end = new CountDownLatch(5);
private static final MyThread MY_THREAD = new MyThread();
private static int count = 1; @Override
public void run() {
try {
Thread.sleep(new Random().nextInt(5) * 1000);
System.out.println("第 " + count + "条线程结束");
count++;
//倒计时减一
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i=0; i<5; i++){
//线程池加入线程
executorService.submit(MY_THREAD);
}
System.out.println("主线程开始执行");
//主线程等待倒计时结束
end.await();
System.out.println("主线程重新执行");
//关闭线程池
executorService.shutdown();
}
}
注意注意:
1. 这里的ExecutorService
是线程池,在后面的学习中是非常重要的工具(有关其用法以后会学到,这里提前了解一下)。
2. 这里的 count 其实是线程不安全的,但因为并发量不高影响不大,而且 count 变量只是为了展示而已,其意义也不大。
输出结果如下:
主线程开始执行
第 1条线程结束
第 2条线程结束
第 3条线程结束
第 4条线程结束
第 5条线程结束
主线程重新执行
五、线程阻塞工具:LockSupport
LockSupport 是用来创建锁和其他同步类的基本线程阻塞,其特点:
- 可以在线程内任意位置堵塞线程。
- 不需要想获得对象的锁就可以执行;不会抛出 InterruptedException 异常。
- park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程(PS:不会引起 suspengd() 与 resume() 造成的死锁问题)。
- park() 方法支持中断,但不会抛出异常。所以需要我们使用 Thread.interrupted() 等方法来获得中断的标记。
为什么 park() 和 unpark() 不会引起 suspengd() 与 resume() 造成的死锁问题?
答:
首先,LockSupport 采用了类似于信号量的许可证机制。它为每一个线程准备了一个许可证(有且只有一个)。若 park() 可以获得许可证,会立刻返回,否则将处于堵塞状态。而 unpark() 会使一个许可证变为可用状态。所以说,即使 unpark() 发生在 park() 之前,也可以保证下一个 park() 操作立刻返回。
其次,处于 park() 挂起状态的线程,会明确地给出一个WAITING
状态,还会标记是 park() 引起的。
六、其他
- ReadWriteLock 读写锁:就是读读之间不阻塞(如果读操作次数远远大于写操作,读写锁就可以发挥最大功效,提升系统性能)。
- CyclicBarrier 循环栅栏:是 CountDownLatch 的升级版,可以接收一个参数 barrierAction,代表着一次计数完成后系统会执行的动作。