Java - 用不同的姿势交替打印线程
一. 常规方法:while自旋控制
public volatile boolean flag = true;
@org.junit.Test
public void testNormal() {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
while (!flag) {
}
System.out.println("A");
flag = false;
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; ) {
while (flag) {
}
System.out.println("B");
flag = true;
}
}).start();
}
二. Thread.yield()
Thread.yield()
的作用是让当前线程从执行状态变为可执行态。来看下相关代码:
public class Test {
public volatile boolean flag = true;
public int i = 0;
@org.junit.Test
public void testYield() {
new Thread(() -> {
while (i < 10) {
if (flag) {
System.out.println("A");
flag = false;
i++;
} else {
Thread.yield();
}
}
}).start();
new Thread(() -> {
while (i < 10) {
if (!flag) {
System.out.println("B");
flag = true;
i++;
} else {
Thread.yield();
}
}
}).start();
}
}
三. Synchronized、notify()和wait()
首先,notify()
一般会和synchronized
联合起来使用,并且需要一个对象作为锁,来限制进程的执行。
- 我们
flag
的初始值为true
,我们希望此时能够打印。所以判断条件是if(!flag)
。 - 如果
flag
的值为false
,那么说明当前线程不能进行打印,需要等待,因此此时应该调用wait()
方法进入阻塞。 - 第一个线程成功打印后,应该将
flag
的值改为false
,表示第一个线程不能再打印,并调用notifyAll()
进行唤醒。 - 第二个线程对于
flag
的判断和处理应该和第一个线程相反。
对于以上分析,做出如下代码:
public class Test {
public volatile boolean flag = true;
private Object obj = new Object();
@org.junit.Test
public void testSynchronized(){
new Thread(()->{
for (int j = 0; j < 3; j++) {
// 占锁
synchronized (obj){
// flag为true的时候,
if (!flag) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A");
flag=false;
obj.notifyAll();
}
}
}).start();
new Thread(()->{
for (int j = 0; j < 3; j++) {
synchronized (obj){
if (flag) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
flag=true;
obj.notifyAll();
}
}
}).start();
}
}
注意Tips:
wait()
会使当前线程进入阻塞状态,但是前提是已经获得锁。notify()
/notifyAll()
会唤醒睡眠的线程,但是并不会立即释放锁。
四. Semaphore信号量控制
分析:
- 可以通过构造函数来指定
Semaphore
的初始信号个数,那对于2个线程交替打印而言,我们需要构造两个信号量,一个初始个数为1,一个为0。 - 其中
Semaphore
类有两个重要的方法,一个是acquire()
方法,代码运行到这一行的时候,会进入阻塞状态,直到获取信号量。另一个就是对应的release()
方法,会释放一个信号。
@org.junit.Test
public void testSemaphore(){
// Semaphore信号量必须大于0才可以获得
Semaphore s1 = new Semaphore(1);
Semaphore s2 = new Semaphore(0);
new Thread(()->{
for (int j = 0; j < 3; j++) {
try {
// 调用acquire方法会进入阻塞状态,直到有一个信号可以获取,即permits>1
// 因为s1的信号量只有1个,第一次循环进来,acquire()方法成功获得信号,代码允许通行,此时s1的信号量变成0
// 此时后续for循环中,会再次调用acquire方法,尝试获取信号,
// 但是信号量为0,所以进入阻塞状态,直到另一个线程释放了s1的信号量
s1.acquire();
System.out.println("A");
// s2释放信号量,信号量+1
s2.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int j = 0; j < 3; j++) {
try {
s2.acquire();
System.out.println("B");
// 释放s1信号,让第一个线程的代码由阻塞状态进入执行状态
s1.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
五. ReentrantLock
一般我们尝尝拿ReentrantLock
和Synchronized
做一个对比,这里做一个简单的整理:
首先,两者都是可重入锁,都是只允许线程访问代码的临界区。那么两者有什么区别呢?
比较内容 | ReentrantLock | Synchronized |
---|---|---|
解锁 | 需要我们手动显式地释放锁 | 交给JVM来执行 |
在递归方法上的调用 | 在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样(因为释放锁是我们手动操作的) | 由于交给JVM管理,所以直接加在递归方法上是可行的。 |
给出个案例大家就能明白了,首先先贴出一个正确的用ReentrantLock
来交替打印的代码:
public volatile boolean flag = true;
@org.junit.Test
public void testLock(){
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
for (int i = 0; i < 3;) {
lock.lock();
try {
if (flag) {
System.out.println("A");
flag=false;
i++;
}
}finally {
lock.unlock();
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 3;) {
lock.lock();
try {
if (!flag) {
System.out.println("B");
flag=true;
i++;
}
}finally {
lock.unlock();
}
}
}).start();
}
结果如下:
错误代码,我们将控制循环的i++
放到外面,看看会发生什么:
public volatile boolean flag = true;
@org.junit.Test
public void testLock() {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
lock.lock();
System.out.println("线程A:lock后,重入次数:" + lock.getHoldCount());
try {
if (flag) {
System.out.println("A");
flag = false;
}
} finally {
lock.unlock();
System.out.println("线程A:unlock后,重入次数:" + lock.getHoldCount());
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
lock.lock();
System.out.println("线程B:lock后,重入次数:" + lock.getHoldCount());
try {
if (!flag) {
System.out.println("B");
flag = true;
}
} finally {
lock.unlock();
System.out.println("线程B:unlock后,重入次数:" + lock.getHoldCount());
}
}
}).start();
}
结果如下:
我们可以看到,只交替打印了一次A和B,而出现这样结果的原因是在于:
for
循环并不受锁的限制,对于程序而言只是单单的循环执行了临界区代码。- 这个临界区代码虽然有着加锁解锁的动作,但是整个过程下来所消耗的时间我们假设忽略不计。
- 那么在启动线程B之前,这段
for
循环其实已经执行完毕,而第一次进入循环的时候,改变了flag,所以根据条件判断,只会打印一次。
因此,我们需要把for
循环的控制放到临界区里面,假设我们for
循环有3次,那么必定只有在临界区代码跑了3次(满足flag
条件)的情况下,for
循环才会终止,那么最终结果的输出必定有3次。
因此在使用ReentrantLock的情况下涉及到循环的时候,我们都需要留个心眼去注意这个问题。
六. LockSupport
LockSupport
类有两个核心的静态方法:
park()
:类似于Semaphore
的acquire()
方法,执行后会让代码进入阻塞,直到能够拿到一个permit
。unpark()
:用于唤醒指定线程,传入一个Thread
对象。
public volatile boolean flag = true;
public static Thread t1 = null;
public static Thread t2 = null;
@org.junit.Test
public void testLockSupport() {
t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
if (!flag) {
LockSupport.park();
}
System.out.println("A");
flag = false;
LockSupport.unpark(t2);
}
});
t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
if (flag) {
LockSupport.park();
}
System.out.println("B");
flag = true;
LockSupport.unpark(t1);
}
});
t1.start();
t2.start();
}
LockSupport
和Semaphore
不同的是,前者对于信号量的控制只能为0或者1。
七. CyclicBarrier
public volatile boolean flag = true;
@org.junit.Test
public void testCyclicBarrier() {
// 屏障的限制个数为2,传入一个参数n,那么需要有n个线程都执行await方法,才会进入下一轮
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
new Thread(() -> {
for (int i = 0; i < 3;) {
if (flag) {
System.out.println("A");
i++;
}
flag = false;
try {
// 进入阻塞,直到线程B也执行await方法
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 3; ) {
if (!flag) {
System.out.println("B");
i++;
}
flag = true;
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}
八. 阻塞队列BlockingQueue
LinkedBlockingQueue
的take()
方法,若调用,会从队列头部去获取元素,若队列中的元素为空,则会进入阻塞状态,直到队列中有元素可以获取。
private BlockingQueue<Integer> q1 = new LinkedBlockingQueue<Integer>() {{
add(0);
}};
private BlockingQueue<Integer> q2 = new LinkedBlockingQueue<>();
@org.junit.Test
public void testQueue(){
new Thread(()->{
for (int i = 0; i < 3; i++) {
try {
q1.take();
System.out.println("A");
q2.add(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 3; i++) {
try {
q2.take();
System.out.println("B");
q1.add(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
分析:
q1
队列中初始化的时候就有一个元素,那么线程A调用q1.take()
的时候,会打印A,同时队列q1
元素数量变为0。- 线程B调用
q2.take()
的时候,由于队列元素为空,则进入阻塞状态,直到线程A调用q2.add(0)
。 - 线程A进入下一次循环时,同样因为队列元素为空,进入阻塞,直到线程B调用
q1.add(0)
,以此循环交替打印。