使用wait和notify
关于wait()
方法,并不是与sleep一样用于阻塞的,而是在synchronized
中配合着notify()
或者notifyAll()
来使用的。
它的目的,就是用于线程之间的通讯。
所谓线程之间的通讯,就是两个线程能够对一个共享变量有协作地改变。
一般来说,都是你改你的,我改我的,处于一种混乱的状态,但是它们可以通过代码的控制完成交流。
package thread_communication;
public class IncreaseAndDecrease {
public static void main(String[] args) {
ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.increase();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.decrease();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.increase();
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.decrease();
}
}, "D").start();
}
}
class ConnectionBtwThread {
private int num = 0;
public synchronized void increase() {
while (num != 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + ++num);
notifyAll();
}
public synchronized void decrease() {
while (num == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + --num);
notifyAll();
}
}
资源类是ConnectionBtwThread
,共享变量是num
。
为什么用 while (num != 0)
而非if(num != 0)
?因为wait存在着虚假唤醒。也就是说,你没有notify,人家自己醒来了,这显然不是我们想要的。
这样的话,即使有虚假唤醒,也要通过num != 0
的检测。
我们可以看一下javadoc的描述:
main方法中我开了4条线程来
进行加减。如果将while改成if的话,这是会有问题的(读者可以尝试)。
结果:
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
那么,为什么要使用synchronized
呢?
如果并非持有锁的线程调用wait
方法,异常会抛出。
使用condition
新的lock有着和以前并发一样的功能,lock能像synchronized一样锁,也有wait和notify一样的功能。
package thread_communication;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class IncreaseAndDecrease {
public static void main(String[] args) {
ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.increase();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.decrease();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.increase();
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
connectionBtwThread.decrease();
}
}, "D").start();
}
}
class ConnectionBtwThread {
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increase() {
lock.lock();
try {
while (num != 0) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + " : " + ++num);
condition.signalAll();
} catch (Exception ex) {
ex.getStackTrace();
} finally {
lock.unlock();
}
}
public void decrease() {
lock.lock();
try {
while (num == 0) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + " : " + --num);
condition.signalAll();
} catch (Exception ex) {
ex.getStackTrace();
} finally {
lock.unlock();
}
}
}
如果你懂之前wait和notify的代码,那么这里的代码也一定会懂,因为逻辑是一样的,只是换了api。
如果只是实现同样的功能的话,新的lock锁又为何出现呢?所以,这里的功能会更加强大。
新的需求:
线程A打印5次,然后线程B打印10次,然后线程C打印15次,再循环……
次序不能变。
这种唤醒叫做精准唤醒,因为A打印完成之后,他要唤醒的是B,而不是其他的。
condition能够做到这一点。
package thread_communication;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class IncreaseAndDecrease {
public static void main(String[] args) {
ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
connectionBtwThread.print5();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
connectionBtwThread.print10();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
connectionBtwThread.print15();
}
}, "C").start();
}
}
class ConnectionBtwThread {
private int num = 1;
private ReentrantLock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (num != 1) {
condition1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
num = 2;
condition2.signal();
} catch (Exception ex) {
ex.getStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (num != 2) {
condition2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
num = 3;
condition3.signal();
} catch (Exception ex) {
ex.getStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (num != 3) {
condition3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
num = 1;
condition1.signal();
} catch (Exception ex) {
ex.getStackTrace();
} finally {
lock.unlock();
}
}
}
这里 condition2.signal();
明确唤醒了print10
,即B线程。
结果大概是这样的:
A1
A2
A3
A4
A5
B1
B2
B3
B4
B5
B6
B7
B8
B9
B10
C1
C2
C3
C4
C5
C6
C7
C8
C9
C10
C11
C12
C13
C14
C15
A1
A2
A3
A4
A5
……
使用CountDownLatch
闭锁的countDown()
和await()
方法的组合也可以完成线程的通信。
只有数到0,才能都通过await()
方法执行它下面的代码。
package thread_communication;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
public class HandsOff {
public static void main(String[] args) throws InterruptedException {
//prepare the thread pool
ExecutorService executorService = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch latch = new CountDownLatch(1);
//producer task
Runnable producer = ()->{
Integer producedElement = ThreadLocalRandom
.current()
.nextInt();
sharedState.set(producedElement);
System.out.println("saving an element : " + producedElement + " to the exchange point");
latch.countDown();
};
//consumer task
Runnable consumer = ()->{
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer consumedElement = sharedState.get();
System.out.println("consuming an element : " + consumedElement + " from the exchange point");
};
//execute
executorService.execute(producer);
executorService.execute(consumer);
executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
executorService.shutdown();
assertEquals(latch.getCount(),0);
}
}
这是一个生产者消费者模型,这种模型是线程通信的典型实现。
latch变为0之后才能执行 Integer consumedElement = sharedState.get();
下面,我们使用同步队列完成同样的效果。
使用SynchronousQueue
同步队列中只能放一个元素,因此put之后会阻塞,他在等take,只有take之后,才能继续put。这样也完美的实现了先生产后消费的思路,当然,这就是线程的通信(协作)。
package thread_communication;
import java.util.concurrent.*;
import static org.junit.Assert.assertEquals;
public class HandsOffV2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();
Runnable producer = () -> {
Integer randomInt = ThreadLocalRandom.current().nextInt();
try {
synchronousQueue.put(randomInt);
System.out.println("saving the sharedNum " + randomInt);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable consumer = () -> {
try {
Integer take = synchronousQueue.take();
System.out.println("consuming the sharedNum " + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
executorService.execute(producer);
executorService.execute(consumer);
try {
executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
assertEquals(synchronousQueue.size(), 0);
}
}
生产者消费者模型及应用
上面已经讲到这种模型了,并且分别用了闭锁和同步队列来实现。我们现在用阻塞队列写一个。
package thread_communication;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Setup {
public static void main(String[] args) {
BlockingQueue q = new ArrayBlockingQueue(2);
Producer producer = new Producer(q);
Consumer consumer1 = new Consumer(q);
Consumer consumer2 = new Consumer(q);
new Thread(producer).start();
new Thread(consumer1).start();
new Thread(consumer2).start();
}
}
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) {
queue = q;
}
@Override
public void run() {
try {
while (true) {
queue.put(produce());
}
} catch (InterruptedException e) {
e.getStackTrace();
}
}
private Object produce() {
return null;
}
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) {
queue = q;
}
@Override
public void run() {
try {
while (true) {
consume(queue.take());
}
} catch (InterruptedException e) {
e.getStackTrace();
}
}
private void consume(Object take) {
}
}
这是Oracle官方的例子,是一个生产者消费者的基本骨架。
那么,生产者消费者模型有什么具体的用途呢?
先生产后消费是其特点。
比如,播放器的预先加载,只有加载完成,才能播放。
为什么灰色的进度条有时候加载加载就停了呢,因为阻塞队列满了,只有当蓝色的播放条追上灰色的时候它才会再次预加载。
我们即将实现的应用,就是异步日志。
什么叫异步呢?就是在主线程之外再开一条线程来写日志,这样就不会阻塞主线程。
日志内容会放进阻塞队列中,在工作线程内,会有一个死循环,一直在询问:“有没有日志信息?”如果有就拿走并写入文件,如果没有就阻塞。
Logger.java
package thread_communication;
import java.io.PrintWriter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 一个异步日志的实现
*
* 使用基于阻塞队列的生产消费者模型
*
* author:Ocean
*/
public class Logger {
private BlockingQueue<String> blockingQueue;
private PrintWriter printWriter;
//初始化阻塞队列
public Logger() {
blockingQueue = new LinkedBlockingQueue<>(10000);
}
//打开文件
public void open() {
try {
printWriter = new PrintWriter("log.txt");
//开一条线程去工作(异步)
//不妨碍主线程做自己的事
Thread writeFileThread = new Thread() {
@Override
public void run() {
//不断地往文件中写日志
while (true) {
writeFile();
}
}
};
writeFileThread.start();
} catch (Exception e) {
e.getStackTrace();
}
}
//写日志
private void writeFile() {
try {
//如果阻塞队列中有内容,则取出,否则就阻塞
String content = blockingQueue.take();
printWriter.println(content);
printWriter.flush();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟日志的产生
public void putFile(String log){
try {
//如果队列满了,则阻塞,否则put
blockingQueue.put(log);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//关闭流
public void close() {
if (printWriter != null) {
printWriter.close();
}
}
}
测试:
package thread_communication;
import java.util.concurrent.TimeUnit;
/**
* 测试类
*/
public class TestLogger {
public static void main(String[] args) {
Logger logger = new Logger();
//打开文件,不断地问:“有没有日志?”
logger.open();
//准备日志信息(模拟)
for (int i = 0; i < 1000; i++) {
logger.putFile("I LOVE YOU");
}
//让主线程等待日志的写入完成
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.close();
}
}