思考1:关键在于如何确保交替执行的顺序,只要能确保交替执行顺序,加不加锁其实并不重要(线程任务里需要修改共享资源的另说),这个想明白,一切就都水到渠成~
思考2:累加取余判断打印后看起来不像是交替执行,但仔细看下其实是交替执行,有没有办法优化下呢?原理是啥?详见这篇文章的第4节:13行代码实现两个线程交替打印
思考3:使用CountDownLatch实现时,取余判断后面为啥还要跟 countDownLatch.getCount()>0的判断?
思考4:CountDownLatch和CyclicBarrier的区别在哪?
一、递加取余判断(无锁,存在线程安全问题,在此仅作为一种思路分享)
- 首先定义一个整型变量;
- 每次执行时加一;
- 然后对【取余后的余数】做判断,从而实现线程的交替执行;
- 执行代码如下:
public class AlternatePrintOdevity1 {
private static int step = 0;
//【1】通过递加求余判断,确保线程交替执行
public static void task(int max,int remainder){
while(step<=max) {
if(step%2==remainder) {
System.out.println(Thread.currentThread().getName() + step++);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
task(10,0);
},"T1#");
Thread t2 = new Thread(()->{
task(10,1);
},"T2#");
t1.start();t2.start();
}
}
二、环形链表步进(无锁,存在线程安全问题,在此仅作为一种思路分享)
- 首先定义一个有两个节点的环形链表,两个节点互为对方的下一节点;
- 每次执行后步进到next下一节点;
- 判断【当前节点是否是想要的节点】,从而实现线程的交替执行;
- 执行代码如下:
class Node{
Node next;
}
public class AlternatePrintOdevity2 {
private static int step = 0;
private static Node node1 = new Node();
private static Node node2 = new Node();
private static Node curr = node1;
static{
node1.next = node2;
node2.next = node1;
}
public static void task(int max,Node wantedNode){
while(step<=max){
if(wantedNode==curr) {
System.out.println(Thread.currentThread().getName() + step++);
curr = curr.next;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
task(10,node1);
},"T1#");
Thread t2 = new Thread(()->{
task(10,node2);
},"T2#");
t1.start();t2.start();
}
}
三、LockSupport锁支持工具类
- 通过LockSupport.park()先阻塞一个线程t2,确保另一个线程t1先执行;
- t1执行逻辑:先执行任务,再LockSupport.unpark(t2)放行t2,最后LockSupport.park()阻塞自己;
- t2执行逻辑:先LockSupport.park()阻塞自己,得到放行后执行任务,最后LockSupport.unpark(t1)放行t1;
- 执行代码如下:
import java.util.concurrent.locks.LockSupport;
public class AlternatePrintOdevity3
{
static Thread t1,t2=null;
static int step = 0;
public static void task() {
while(step<10) {
if(t1 == Thread.currentThread()) {
System.out.println(t1.getName() + step++);//t1先执行打印
LockSupport.unpark(t2);//打印后放行t2
LockSupport.park();//阻塞t1,直到被t2放行
}else {
LockSupport.park();//阻塞t2,直到被t1放行
//能走到这里说明t2已经被t1放行,且目前t1处于阻塞等待方向的状态,接着轮到t2执行打印
System.out.println(t2.getName() + step++);
LockSupport.unpark(t1);//打印后放行t1
}
}
}
public static void main(String[] args) {
t1 = new Thread(()->{task();},"#T1#");
t2 = new Thread(()->{task();},"#T2#");
t1.start();t2.start();
}
}
四、synchronized关键字+Object.wait+Object.notify
- 思考:synchronized能够实现线程交替执行么?
- 要想实现线程交替执行,就必须有方法能够在合适的时间主动阻塞和放行线程
- synchronized+Object.wait(阻塞)+Object.notify(放行)可以实现;
- 执行代码如下:
public class AlternatePrintOdevity4
{
static Object obj = new Object();
static int step = 0;
public static void task() {
while(step<10) {
synchronized(obj) {
System.out.println(Thread.currentThread().getName() + step++);
obj.notify();
try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
}
}
public static void main(String[] args) {
new Thread(()->{task();},"#T1#").start();
new Thread(()->{task();},"#T2#").start();
}
}
五、ReentrantLock+Condition.await+Condition.signal
- 基本同synchronized+Object.wait(阻塞)+Object.notify(放行)
- 要想实现线程交替执行,就必须有方法能够在合适的时间主动阻塞和放行线程
- ReentrantLock+Condition.await(阻塞)+Condition.signal(放行)可以实现;
- 执行代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
//报错:IllegalMonitorStateException if the current thread is not the owner of this object's monitor.
//原因:Condition唤醒和阻塞使用的是signal、signalAll和await,注意要和Object的notify、nofityAll和wait区分开
public class AlternatePrintOdevity5
{
static ReentrantLock reentrantLock = new ReentrantLock();
static Condition condition = reentrantLock.newCondition();
static int step = 0;
public static void task() {
while(step<10) {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + step++);
condition.signal();
if(step<=10-1)
try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}
reentrantLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->{task();},"#T1#").start();
new Thread(()->{task();},"#T2#").start();
}
}
六、CountDownLatch倒计时计数器门闩(不推荐)
- CountDownLatch本身并不适用于实现线程交替执行这一场景,因为其设计初衷是为了实现:【在执行某个任务之前,必须先执行指定数量的子任务【子任务之间没有顺序,子任务之间不需要相互等待,子任务之间属于并发执行,每个子任务执行完即可自行结束】,待所有子任务都执行完毕,才能进行该任务的执行】【比如:火箭发射任务,在发射火箭之前,需要执行很多子任务:装燃料、检查、点火等】,而非交替执行。
- 硬要实现,也有方法:递减取余判断,对CountDownLatch的计数【即总执行次数】做递减;
- 其逻辑本质上跟【一、递加取余判断】一样
- 思考:取余判断后面为啥还要跟 countDownLatch.getCount()>0这个判断?因为:有两个线程在同时执行while循环这段代码,假设T2已将计数减为0之前,T1已经执行过【1】,而恰好在T2将计数减为0这个时刻,T1执行到了【2】,此时t1就会再获得一次执行机会了。为了避免这多出来的一次执行,所以才加的这个判断。
- 执行代码如下:
import java.util.concurrent.CountDownLatch;
public class AlternatePrintOdevity6
{
static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void task(int remainder) {
while(countDownLatch.getCount()>0) {//【1】
if(countDownLatch.getCount()%2==remainder && countDownLatch.getCount()>0) {//【2】
System.out.println(Thread.currentThread().getName() + (10-countDownLatch.getCount()));
countDownLatch.countDown();
}
}
}
public static void main(String[] args) {
new Thread(()->{task(0);},"#T1#").start();
new Thread(()->{task(1);},"#T2#").start();
}
}
七、CyclicBarrier循环屏障(不推荐)
- CyclicBarrier本身并不适用于实现线程交替执行这一场景,因为其设计初衷是为了实现:【为多个并发执行的线程设置一个屏障点,每个线程到达屏障点即被阻塞,直到所有线程都到达这个屏障点,各个线程才能继续往下执行】【比如:跑步比赛开始前,需要所有选手都到达代表比赛就绪的起点【屏障点】,比赛才能继续】,而非交替执行。
- 硬要实现,也有方法:递加取余判断;
- 其逻辑本质上跟【一、递加取余判断】一样,只是加了多余的 CyclicBarrier的相关实现,其实完全可以不加CyclicBarrier,可以说算是只是为了用而用,俗了,完全不讲道理
- 执行代码如下:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class AlternatePrintOdevity7
{
static CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
static int step = 0;
public static void task(int remainder) {
while(step < 10) {
if(step%2 == remainder) {
try {cyclicBarrier.await();} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName() + step++ );
cyclicBarrier.reset();
}
}
}
public static void main(String[] args) {
new Thread(()->{task(0);},"#T1#").start();
new Thread(()->{task(1);},"#T2#").start();
}
}
八、Semaphore信号量(不推荐)
- Semaphore本身并不适用于实现线程交替执行这一场景,因为其设计初衷是为了协调多个线程并行执行(只要还有许可可用,线程就能不被阻塞在acquire),而非交替执行。
- 硬要实现,也有方法:递加取余判断;
- 其逻辑本质上跟【一、递加取余判断】一样,只是加了多余的 Semaphore的相关实现,其实完全可以不加Semaphore,可以说算是只是为了用而用,俗了,完全不讲道理
- 执行代码如下:
import java.util.concurrent.Semaphore;
public class AlternatePrintOdevity8
{
static Semaphore semaphore = new Semaphore(1);
static int step = 0;
public static void task(int remainder) {
while(step < 10) {
if(step%2 == remainder) {
try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName() + step++ );
semaphore.release();
}
}
}
public static void main(String[] args) {
new Thread(()->{task(0);},"#T1#").start();
new Thread(()->{task(1);},"#T2#").start();
}
}