效果:
线程1:1
线程1:2
线程1:3
线程2:4
线程2:5
线程2:6
线程3:7
线程3:8
线程3:9
线程1:10
线程1:11
线程1:12
…
线程3:34
线程3:35
线程3:36
实现 思路:基于 synchronized 的 wait, notifyAll
- 同时开启三个线程,分别为这三个线程设置name,1 2 3.。
- 定义一个共享的
Runnable
实现类。 - 定义一把
共享锁
对象,或者使用this
锁对象。(必须是三个线程共享的锁) white
循环,自增数num
小于36。- 业务代码执行前,
synchronized
获取锁。 - 获取锁成功的线程,判断
num
是否大于等于36。(避免线程是从阻塞状态唤醒,获取锁,而num
>= 36的情况)。 - 判断线程名称是否是可以执行业务的线程。顺序是 线程1执行完再执行线程2再执行线程3,然后又执行线程1,保证顺序输出。
- 如果当前线程不能执行业务,
wait
,释放锁,且暂停当前线程。 - 如果是可执行业务线程,循环三次输出
num
++,将可执行线程name++,列如当前线程name = 1,则下一个可执行线程name = 2。 - 业务执行完毕 唤醒阻塞线程。
注意:
阻塞线程被唤醒,必须还要判断当前线程是否是可执行线程。且还要判断num
是否大于等于36。
实现代码
public class Test {
// num 相加后的最大值
private final int MAX_VALUE = 36;
// num 自增次数
private final int INCREMENT_COUNT = 3;
// 线程相加的变量 num 保证可见性
private volatile int num;
// 当前可以进行操作的线程 name 保证可见性
private volatile int currThread = 1;
/**
* i++ 线程的 run方法
*/
public class MyRun implements Runnable {
@Override
public void run() {
while (num < MAX_VALUE) { // num < MAX_VALUE 循环
// 使用this锁
synchronized (this) {
// 此处有可能是阻塞线程被唤醒 可能 num 已经达到 MAX_VALUE 了
if (num >= MAX_VALUE) {
return;
}
// 获取线程名称
String name = Thread.currentThread().getName();
// 死循环
for (; ;) { // 该循环体 控制被唤醒的线程 继续判断当前线程是否是可执行线程
if (Integer.parseInt(name) != currThread) { // 当前线程name 不是可执行操作的线程 进入休眠
try {
this.wait();
// 此处是被唤醒了 才会执行
if (num >= MAX_VALUE) { // 被唤醒后判断 num 是否已经达到 MAX_VALUE了
return;
}
// num < MAX_VALUE
// 再次进入循环体 目的就是判断当前线程是否可执行
} catch (InterruptedException e) {
}
} else { // 是可执行的线程 跳出循环体
break;
}
}
int j = num + INCREMENT_COUNT; // num + 3
while (num < j) { // 循环三次 输出 i++
System.out.println("当前线程" + name + ": " + ++num);
}
currThread++; // 将可执行线程的 name加一
if (currThread > INCREMENT_COUNT) {
currThread = 1; // 重置可执行线程为 初始
}
this.notifyAll(); // 唤醒阻塞线程
}
}
}
}
/**
* 启动三个线程
*/
public void run() {
// 每个线程共用此 Runnable 实例
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
t1.setName("1");
Thread t2 = new Thread(myRun);
t2.setName("2");
Thread t3 = new Thread(myRun);
t3.setName("3");
t1.start();
t2.start();
t3.start();
}
public static void main(String[] args) {
new Test().run();
}
}
优点
- 代码简单,通俗易懂,不依赖其他工具包。
缺点
- 唤醒线程使用
notifyAll
,唤醒所有线程,但是只能有一个线程执行。 - 被唤醒的线程,在死循环体中,判断是否可执行,如果不能执行,又暂停,从而导致某个线程会(启动 -> 暂停 -> 唤醒 -> 暂停 -> 唤醒),线程的唤醒与暂停对CPU来说开销还是有点大的。
其他解决方案:
从题目出发:
- 同时开启三个线程。
- 每个线程输出
num
++,且是连续的。 - 线程执行必须是顺序性的:
线程1 -> 线程2 -> 线程3 -> 线程1
以此类推。
以上内容看出,线程的执行是串行的,那就可以这样构思,线程1执行完去唤醒线程2,线程2执行完去唤醒线程3,线程3执行完去唤醒线程1,始终都只有一个线程在执行,这样还降低了线程安全问题。
实现 思路。 基于 LockSupport 唤醒/暂停线程
- 用数组或集合,维护需要运行的线程。可保证线程的运行顺序。
- 首先启动索引0的线程。
- 所有业务逻辑代码在
while (num < 36)
循环体中。 - 执行完业务逻辑,唤醒/启动下一个索引的线程。
- 计算出当前线程是否是最后一次执行,如果是:业务执行完毕。如果不是,线程暂停,等待唤醒。
实现代码
import java.util.concurrent.locks.LockSupport;
public class TestMain {
/** num 数自增后最大数值 */
private final int MAX_VALUE = 36;
/** 每次每个线程 对 num 自增多少次 */
private final int INCREMENT_COUNT = 3;
/** 启动多少个线程 */
private final int THREAD_NUM = 3;
/** 自增 num 保证线程可见性 */
private volatile int num;
/** 启动自增 num 线程的数组 */
private Thread[] threads = new Thread[THREAD_NUM];
/* 启动 */
public static void main(String[] args) {
new TestMain().run();
}
/** 执行 */
public void run() {
// 每个线程共用此 Runnable 实例
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < THREAD_NUM; i++) {
// 创建线程
Thread thread = new Thread(myRunnable);
// 设置线程名称
thread.setName(String.valueOf(i + 1));
// 缓存线程
threads[i] = thread;
}
// 先启动第一个线程 线程 name = 1
threads[0].start();
}
/**
* 运行下一个线程
*/
public void runNextThread() {
// 计算下一个执行线程的 数组索引
int index = num / 3 % 3;
// 下一个执行的线程
Thread nextThread = threads[index];
if (nextThread.getState() == Thread.State.NEW)
// 线程从未启动过 启动线程
nextThread.start();
else
// 唤醒下一个线程
LockSupport.unpark(nextThread);
}
/**
* 线程的 Runnable
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
while (num < MAX_VALUE) {
int j = num + INCREMENT_COUNT; // num + 3
while (num < j) { // 循环三次 输出 ++num
System.out.println("当前线程" + Thread.currentThread().getName() + ": " + ++num);
}
// 运行下一个线程
runNextThread();
// MAX_VALUE - THREAD_NUM * INCREMENT_COUNT
// 36 - 3 * 3 = 27
if (num > MAX_VALUE - THREAD_NUM * INCREMENT_COUNT)
// 计算 num 是否到了线程的最后一次执行自增 是的话退出循环
return;
else
// 暂停线程
LockSupport.park(Thread.currentThread());
}
}
}
}
优点
- 代码简洁,易懂,run方法里不需要对数值进行双重判断。
- 不需要加锁。
- 每次只唤醒一个线程执行,避免了唤醒多个线程的开销。
缺点
- 未知
总结
实现方法有很多种,其实选择什么实现方法,思路都差不多。最主要的是要理解多线程编程的思维。多线程最重要的就是思维和想象力,可以去力扣官网
刷题提升自己。
- 应充分的去想象多线程同时在执行的效果。
- 应如何把多线程的
并行
控制成串行
。 - 应如何把多线程的
并行
控制成顺序串行
。