Три потока T1, T2, T3, как заставить их выполняться по порядку?
Это кодовый вопрос для параллельного программирования, который часто тестируется на собеседованиях.
- Три потока T1, T2, T3 по очереди печатают ABC, печатая n раз, например ABCABCABCABC......
- Два потока поочередно печатают нечетные и четные числа от 1 до 100
- Печать цикла N потоков 1-100
- ......
На самом деле, этот тип проблемы по сути является проблемой взаимодействия потоков.Идея в основном состоит в том, что после выполнения потока он блокирует поток, пробуждает другие потоки и выполняет следующий поток в последовательности. Давайте сначала рассмотрим самое простое, как последовательно выполнить три потока.
synchronized+wait/notify
Основная идея состоит в том, что поток A, поток B и поток C запускают три потока одновременно, потому что переменнаяnum
Начальное значение0
, поэтому после того, как поток B или поток C получит блокировку, введитеwhile()
цикл, затем выполнитьwait()
метод, поток блокирует поток, освобождает блокировку. Только после того, как поток A получит блокировку, он не входитwhile()
петля, выполнитьnum++
, печатать символыA
и, наконец, разбудить поток B и поток C. В настоящее времяnum
значение1
, только после того, как поток B получит блокировку, он не блокируется и выполняетсяnum++
, печатать символыB
, и, наконец, разбудить поток A и поток C и так далее.
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替while,想不起来可以看公众号上一篇文章
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(Thread.currentThread().getName());
LOCK.notifyAll();
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC(0);
}, "A").start();
new Thread(() -> {
wait_notify_acb.printABC(1);
}, "B").start();
new Thread(() -> {
wait_notify_acb.printABC(2);
}, "C").start();
}
}
Введите результат:
ABC
Process finished with exit code 0
Теперь рассмотрим первый вопрос: три потока T1, T2 и T3 по очереди печатают ABC n раз. На самом деле вам нужно всего лишь добавить цикл к приведенному выше коду, предполагая, что здесь n=10.
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum) {
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(Thread.currentThread().getName());
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC(0);
}, "A").start();
new Thread(() -> {
wait_notify_acb.printABC(1);
}, "B").start();
new Thread(() -> {
wait_notify_acb.printABC(2);
}, "C").start();
}
}
Выходной результат:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
Рассмотрим второй вопрос: два потока поочередно выводят четные и нечетные числа от 1 до 100. Чтобы уменьшить место, занимаемое вводом, здесь 100 заменено на 10. Основная идея аналогична приведенной выше, нечетный поток первым получает блокировку - печатает номер - пробуждает четный поток - блокирует нечетный поток и повторяет это.
class Wait_Notify_Odd_Even{
private Object monitor = new Object();
private volatile int count;
Wait_Notify_Odd_Even(int initCount) {
this.count = initCount;
}
private void printOddEven() {
synchronized (monitor) {
while (count < 10) {
try {
System.out.print( Thread.currentThread().getName() + ":");
System.out.println(++count);
monitor.notifyAll();
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
monitor.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
new Thread(waitNotifyOddEven::printOddEven, "odd").start();
Thread.sleep(10); //为了保证线程odd先拿到锁
new Thread(waitNotifyOddEven::printOddEven, "even").start();
}
}
результат операции:
odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10
Посмотрите на третий вопрос, N потоков циклически печатают от 1 до 100. На самом деле, хорошо подумайте об этом, а три потока циклически печатают ABC, и нет существенной разницы. Вам нужно только добавить утверждение, чтобы определить, является ли максимальное значение напечатанного число достигнуто. . Предполагая, что N=3, чтобы полностью отобразить результаты вывода, напечатайте 1-10, код выглядит следующим образом:
class Wait_Notify_100 {
private int num;
private static final Object LOCK = new Object();
private int maxnum = 10;
private void printABC(int targetNum) {
while (true) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
if(num >= maxnum){
break;
}
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(num >= maxnum){
break;
}
num++;
System.out.println(Thread.currentThread().getName() + ": " + num);
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_100 wait_notify_100 = new Wait_Notify_100 ();
new Thread(() -> {
wait_notify_100.printABC(0);
}, "thread1").start();
new Thread(() -> {
wait_notify_100.printABC(1);
}, "thread2").start();
new Thread(() -> {
wait_notify_100.printABC(2);
}, "thread3").start();
}
}
Выходной результат:
thread1: 1
thread2: 2
thread3: 3
thread1: 4
thread2: 5
thread3: 6
thread1: 7
thread2: 8
thread3: 9
thread1: 10
Интервьюер:Все используют этоsynchronized+wait/notify
а можно решить задачу по другому?
Я:хорошо, я тоже воспользуюсьjoin()
метод
Метод, описанный ниже, дает только код первого вопроса, иначе он слишком длинный.Я думаю, каждый может сделать выводы из одного примера.
join()
join()
Метод: когда метод join() потока B вызывается в потоке A, это означает, что поток A может продолжить выполнение только после завершения выполнения потока B. Основываясь на этом принципе, мы заставляем три потока выполняться последовательно, а затем зацикливаемся несколько раз. Независимо от того, какой поток 1, поток 2 или поток 3 выполняется первым, последний порядок выполнения — поток 1 -> поток 2 -> поток 3. код показывает, как показано ниже:
class Join_ABC {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread(new printABC(null),"A");
Thread t2 = new Thread(new printABC(t1),"B");
Thread t3 = new Thread(new printABC(t2),"C");
t0.start();
t1.start();
t2.start();
Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。
}
}
static class printABC implements Runnable{
private Thread beforeThread;
public printABC(Thread beforeThread) {
this.beforeThread = beforeThread;
}
@Override
public void run() {
if(beforeThread!=null) {
try {
beforeThread.join();
System.out.print(Thread.currentThread().getName());
}catch(Exception e){
e.printStackTrace();
}
}else {
System.out.print(Thread.currentThread().getName());
}
}
}
}
Выходной результат:
ABCABCABCABCABCABCABCABCABCABC
Интервьюер:Есть ли другой способ?
Я:Также используйте Lock для решения проблемы.
Lock
Этот метод прост для понимания, независимо от того, какой поток получает блокировку, только те, которые соответствуют условиям, могут печатать. код показывает, как показано ниже:
class Lock_ABC {
private int num; // 当前状态值:保证三个线程之间交替打印
private Lock lock = new ReentrantLock();
private void printABC(int targetNum) {
for (int i = 0; i < 10; ) {
lock.lock();
if (num % 3 == targetNum) {
num++;
i++;
System.out.print(Thread.currentThread().getName());
}
lock.unlock();
}
}
public static void main(String[] args) {
Lock_ABC lockABC = new Lock_ABC();
new Thread(() -> {
lockABC.printABC(0);
}, "A").start();
new Thread(() -> {
lockABC.printABC(1);
}, "B").start();
new Thread(() -> {
lockABC.printABC(2);
}, "C").start();
}
}
Выходной результат:
ABCABCABCABCABCABCABCABCABCABC
Интервьюер:В чем проблема с этим методом и можно ли его дополнительно оптимизировать?
Я:Вы можете использовать Lock+Condition для точного пробуждения потоков, уменьшая бессмысленную конкуренцию за блокировки синхронизации и бесполезную трату ресурсов.
Lock+Condition
Эта идея очень похожа на метод synchronized+wait/notify, синхронизированный соответствует блокировке, а метод await/signal соответствует методу ожидания/уведомления. Следующий код создает несколько объектов Condition, чтобы точно разбудить следующий поток.
class LockConditionABC {
private int num;
private static Lock lock = new ReentrantLock();
private static Condition c1 = lock.newCondition();
private static Condition c2 = lock.newCondition();
private static Condition c3 = lock.newCondition();
private void printABC(int targetNum, Condition currentThread, Condition nextThread) {
for (int i = 0; i < 10; ) {
lock.lock();
try {
while (num % 3 != targetNum) {
currentThread.await(); //阻塞当前线程
}
num++;
i++;
System.out.print(Thread.currentThread().getName());
nextThread.signal(); //唤醒下一个线程,而不是唤醒所有线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
LockConditionABC print = new LockConditionABC();
new Thread(() -> {
print.printABC(0, c1, c2);
}, "A").start();
new Thread(() -> {
print.printABC(1, c2, c3);
}, "B").start();
new Thread(() -> {
print.printABC(2, c3, c1);
}, "C").start();
}
}
Выходной результат:
ABCABCABCABCABCABCABCABCABCABC
Интервьюер:В дополнение к этому методу есть ли способ избежать пробуждения других бессмысленных потоков, чтобы не тратить ресурсы?
Я:Это может быть достигнуто с помощью семафоров.
Semaphore
Семафор: используется для управления количеством операций, одновременно обращающихся к определенному ресурсу, или количеством одновременных выполнений указанной операции. Semaphore внутренне поддерживает счетчик, значение которого является количеством общих ресурсов, к которым можно получить доступ.
Чтобы получить доступ к общему ресурсу, поток сначала используетacquire()
Метод получает семафор.Если значение счетчика семафора больше или равно 1, это означает, что есть общий ресурс, к которому можно получить доступ, вычесть 1 из значения счетчика, а затем получить доступ к общему ресурсу. Если значение счетчика равно 0, поток переходит в спящий режим.
Когда поток завершает использование общего ресурса, используйтеrelease()
Семафор освобождается, а счетчик внутри семафора увеличивается на 1, и поток, который раньше засыпал, проснется и снова попытается получить семафор.
код показывает, как показано ниже:
class SemaphoreABC {
private static Semaphore s1 = new Semaphore(1); //因为先执行线程A,所以这里设s1的计数器为1
private static Semaphore s2 = new Semaphore(0);
private static Semaphore s3 = new Semaphore(0);
private void printABC(Semaphore currentThread, Semaphore nextThread) {
for (int i = 0; i < 10; i++) {
try {
currentThread.acquire(); //阻塞当前线程,即信号量的计数器减1为0
System.out.print(Thread.currentThread().getName());
nextThread.release(); //唤醒下一个线程,即信号量的计数器加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
SemaphoreABC printer = new SemaphoreABC();
new Thread(() -> {
printer.printABC(s1, s2);
}, "A").start();
Thread.sleep(10);
new Thread(() -> {
printer.printABC(s2, s3);
}, "B").start();
Thread.sleep(10);
new Thread(() -> {
printer.printABC(s3, s1);
}, "C").start();
}
}
Выходной результат:
ABCABCABCABCABCABCABCABCABCABC
Интервьюер:Помимо пяти вышеперечисленных методов, есть ли еще какие-нибудь?
Я:Есть также LockSupport, CountDownLatch, AtomicInteger и другие.
Интервьюер:Тогда как реализовать три потока для циклической печати ACB, в которых A печатает дважды, B печатает три раза, а C печатает четыре раза?
Я: ......
Интервьюер:Как я могу печатать числа и символы, чередующиеся с двумя потоками? Например, A1B2C3...Z26.
Я: ......
В реальном процессе собеседования мы точно не позволим вам использовать столько методов для достижения многопоточной поочередной печати вопросов. Просто запомните один-два. Можно подумать над следующими двумя модернизированными вариантами вопросов. Принципы те же. .
Публичный аккаунт поиска WeChatпрохожий Чжан,ОтветитьРуководство по проведению интервью, получите более часто задаваемые вопросы для интервью в формате PDF и другие материалы для интервью.
Рекомендуемое чтение: