Thread-0: 0
Thread-1: 1
Thread-2: 2
Thread-0: 3
Thread-1: 4
Thread-2: 5
....
Thread-1: 100
这个问题主要考察多线程的线程安全和通信机制,常见的处理方式有notify/synchorized和condition/ reentrantlock。但是往往有同学只注意线程安全,而忽略了通信机制,常见的错误写法如下:
public class Test {
private static int i = 1;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Print(i)).start();
}
}
private static class Print implements Runnable {
private final int index;
public Print(int index) {
this.index = index;
}
@Override
public void run() {
while(true) {
synchronized (Print.class) {
if (i >= 101) {
return;
}
System.out.println("Thread-" + index + " " + i++);
}
}
}
}
}
这样写固然能通过锁来保证循环打印了1-100,但是却不能保证线程是按照顺序打印的,这个时候就需要用到线程的通信机制。
我们可以结合Sync和Object#notifyAll来完成,如下所示
public class SortTest {
private static final Object LOCK = new Object();
private static volatile int count = 0;
private static final int MAX = 100;
public static void main(String[] args) {
Thread thread = new Thread(new Seq(0));
Thread thread1 = new Thread(new Seq(1));
Thread thread2 = new Thread(new Seq(2));
thread.start();
thread1.start();
thread2.start();
}
static class Seq implements Runnable {
private final int index;
public Seq(int index) {
this.index = index;
}
@Override
public void run() {
while (count < MAX) {
synchronized (LOCK) {
try {
while (count % 3 != index) {
LOCK.wait();
}
if(count <=MAX){
System.out.println("Thread-" + index + ": " + count);
}
count++;
LOCK.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
我们可以使用ReentrantLock和Condition尝试解决这种问题。大概的解决思路就是先通过lock对资源加锁,然后通过condition指定的唤醒下一个线程。相信大家都已经发现,这种方式比Synchronized的优点就是sync只能唤醒一个线程或者全部唤醒来让大家竞争,但是通过condition我们可以唤醒指定线程,避免资源浪费
public class Test {
private static final int WORKER_COUNT = 3;
private static int countIndex = 0;
private static final ReentrantLock LOCK = new ReentrantLock();
public static void main(String[] args){
final List<Condition> conditions = new ArrayList<>();
for(int i=0; i< WORKER_COUNT; i++){
// 为每一个线程分配一个condition
Condition condition = LOCK.newCondition();
conditions.add(condition);
Worker worker = new Worker(i, conditions);
worker.start();
}
}
static class Worker extends Thread{
int index;
List<Condition> conditions;
public Worker(int index, List<Condition> conditions){
super("Thread-"+index);
this.index = index;
this.conditions = conditions;
}
private void signalNext(){
int nextIndex = (index + 1) % conditions.size();
conditions.get(nextIndex).signal();
}
@Override
public void run(){
while(true) {
//锁住 保证操作间同时只有一个线程
LOCK.lock();
try {
// 如果当前线程不满足打印条件,则等待
if (countIndex % 3 != index) {
conditions.get(index).await();
}
if (countIndex > 100) {
// 唤醒下一个线程,保证程序正常退出
signalNext();
// 退出循环 线程运行结束
return;
}
System.out.println((this.getName() + " " + countIndex));
// 计数器+1
countIndex ++;
// 通知下一个干活
signalNext();
}catch (Exception e){
e.printStackTrace();
}finally {
LOCK.unlock();
}
}
}
}
}
注意,对于Worker里面的逻辑,不能为了图省事,用下面的写法:
public void run(){
while(true) {
//锁住 保证操作间同时只有一个线程
LOCK.lock();
try {
if (countIndex > 100) {
// 唤醒下一个线程,保证程序正常退出
signalNext();
// 退出循环 线程运行结束
return;
}
System.out.println((this.getName() + " " + countIndex));
// 计数器+1
countIndex ++;
// 通知下一个干活
signalNext();
conditions.get(index).await();
}catch (Exception e){
e.printStackTrace();
}finally {
LOCK.unlock();
}
}
}
这种写法是错误的,原因是如果刚开始不让所有线程都等待,就有可能会导致线程竞争,举个例子:
step1:thread0执行逻辑
step2:thread2被锁阻塞
step3:thread0执行完成,唤醒thread1
step4:thread2和thread1竞争锁,thread2竞争成功,就会导致thread0执行完成后,直接由thread2执行
除了线程之间的通信之外,我们还可以使用一种取巧的方式,就是通过指定线程打印某些值,如Thread0打印0,3,9等值。
核心思想是通过yield自旋的方式,如果当前的值不需要被当前线程打印,那么就让出该线程。如下所示:
private static volatile int count = 0;
private static final int MAX = 100;
static class OtherWorker implements Runnable {
private final int index;
public OtherWorker(int index) {
this.index = index;
}
@Override
public void run() {
while (count < MAX) {
while (count % 3 != index) {
Thread.yield();
}
if (count > MAX) {
return;
}
System.out.println("Thread-" + index + " " + count);
count++;
}
}
}
这种方式需要线程不停的竞争和自旋,性能显然比不过前两种方法。同时,因为yield并不能保证立刻让出CPU,所以这种方法是有风险的
这种问题还有很多变种,如三个线程顺序打印ABC,三个线程按照要求顺序打印ABC等。
这些问题都是上线问题的变体,譬如把1-100换成ABC,常见的1-100转为ABC的代码转换如下所示:
for (int i = 0; i < 100; i ++) {
char ascii = (char)(65 + i%3);
}