Java-并发-线程通信详解和案例演示
线程通信
为什么要线程通信?
多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
狭义上来说:线程通信的目标是使线程间能够互相发送信号(通知),另一方面,线程通信使线程能够等待其他线程的信号(通知),也称为线程间的等待/通知机制,或者生产消费模式!广义上说:能够协调线程调度运行的方法都属于线程通信的应用,不应是一个线程主动通知另外一个线程,这个通知还可能是一个公共信号。
线程通信的方式
- synchronized 线程通信
JDK1.5之前synchronized + wait + notify + notifyAll - 使用lock+Condition控制线程通信
JDK1.5开始,Lock可以代替synchronized 同步方法或同步代码块,Condition替代同步监视器的功能:lock + condition + await + signal + signalAll - 使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue接口主要作为线程同步的工具。当生产者试图向BlockingQueue中放入元素,如果队列已满,则线程被阻塞;当消费者试图向BlockingQueue中取出元素时,若该队列已空,则线程被阻塞。这里通过共享一个队列的信息,实现生产者和消费者。 - 使用管道流PipedWriter/PipedReader
- 使用JDK1.5提供的信号量Semaphore、CountDownLatch、CyclicBarrier等工具类
- volatile
volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。以此来实现线程通信,但是volatile不能保证操作原子性,是一种弱的同步机制。
线程通信的案例
synchronized实现生产消费
下面的案例使用synchronized实现了多生产者多消费者的案例
public class ProductionAndConsumption {
public static void main(String[] args) {
Resource resource = new Resource();
Thread thread1 = new Thread(new Producer(resource), "生产者1");
Thread thread2 = new Thread(new Producer(resource), "生产者2");
Thread thread3 = new Thread(new Consumer(resource), "消费者1");
Thread thread4 = new Thread(new Consumer(resource), "消费者2");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
/**
* 表示产品资源
*/
class Resource {
//标号
private int count;
//名字
private String name;
//标志位,false表示没有产品,trur表示生产出了产品
private boolean flag;
/**
* 生产产品
* @param name
*/
void set(String name) {
//使用同步块
synchronized (this) {
//判断false是否为true,如果是true说明有产品了,那么生产者线程应该等待
if (flag) {
try {
System.out.println("有产品了--" + Thread.currentThread().getName() + "生产等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//走到这一步,说明没有产品,可以生产
this.name = name;
System.out.println(Thread.currentThread().getName() + "--" + this.name + (++count) + "生产");
//设置产品标志为true,表示有产品了,可以消费了
flag = true;
//这里唤醒所有线程,有可能还会唤醒生产者
this.notifyAll();
}
}
/**
* 消费产品
*/
void get() {
synchronized (this) {
//判断flag是否为false,如果是fasle说明有产品了,那么消费者线程应该等待
if (!flag) {
try {
System.out.println("没产品了--" + Thread.currentThread().getName() + "消费等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//走到这一步,说明有产品,可以消费
System.out.println(Thread.currentThread().getName() + "--" + this.name + count + "消费");
//设置产品标志为false,表示没有产品了,可以生产了
flag = false;
//这里唤醒所有线程,有可能还会唤醒消费者
this.notifyAll();
}
}
}
/**
* 生产者线程
*/
class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用生产方法
resource.set("面包");
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用消费方法
resource.get();
}
}
}
Lock实现生产消费
public class LockPC {
public static void main(String[] args) {
Resource resource = new Resource();
Producer producer = new Producer(resource);
Consumer consumer = new Consumer(resource);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.shutdown();
}
/**
* 产品资源
*/
static class Resource {
private String name;
private int count;
boolean flag;
//获取lock锁,lock锁的获取和释放需要代码手动操作
ReentrantLock lock = new ReentrantLock();
//从lock锁获取一个condition,用于生产者线程在此等待和唤醒
Condition producer = lock.newCondition();
//从lock锁获取一个condition,用于消费者线程在此等待和唤醒
Condition consumer = lock.newCondition();
void set(String name) {
//获得锁
lock.lock();
try {
while (flag) {
try {
System.out.println("有产品了--" + Thread.currentThread().getName() + "生产等待");
//该生产者线程,在producer上等待
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
++count;
this.name = name;
System.out.println(Thread.currentThread().getName() + "生产了" + this.name + +count);
flag = !flag;
//唤醒在consumer上等待的消费者线程,这样不会唤醒等待的生产者
consumer.signalAll();
} finally {
//释放锁
lock.unlock();
}
}
void get() {
lock.lock();
try {
while (!flag) {
try {
System.out.println("没产品了--" + Thread.currentThread().getName() + "消费等待");
//该消费者线程,在consumer上等待
consumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费了" + this.name + count);
flag = !flag;
//唤醒在producer监视器上等待的生产者线程,这样不会唤醒等待的消费者
producer.signalAll();
} finally {
lock.unlock();
}
}
}
/**
* 消费者
*/
static class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用消费方法
resource.get();
}
}
}
/**
* 生产者
*/
static class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用生产方法
resource.set("面包");
}
}
}
}
Lock实现生产消费和产品仓库的功能
在上一个案例中,生产者线程生产的产品必须马上被消费,在下面的案例中,生产的产品可以被累积,使用Lock实现了多生产者多消费者的案例,同时实现了商品仓库的功能,使得生产者可以连续生产,消费者可以连续消费。
public class LockPCWhithStorage {
public static void main(String[] args) {
Resource resource = new Resource();
Consumer c = new Consumer(resource);
Producer p = new Producer(resource);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
threadPoolExecutor.execute(p);
threadPoolExecutor.execute(p);
threadPoolExecutor.execute(c);
threadPoolExecutor.execute(c);
threadPoolExecutor.shutdown();
}
static class Resource {
// 获得锁对象
final Lock lock = new ReentrantLock();
// 获得生产监视器
final Condition notFull = lock.newCondition();
// 获得消费监视器
final Condition notEmpty = lock.newCondition();
// 定义一个数组,当作仓库,用来存放商品
final Object[] items = new Object[100];
/*
* 取消了falg标志,取而代之的是用仓库的数量来判断是否应该阻塞或者唤醒对应的线程
* putpur:生产者使用的下标索引;
* takeptr:消费者下标索引;
* count:用计数器,记录商品个数
*/
int putptr, takeptr, count;
// 生产方法
public void put(Object x) {
// 获得锁
lock.lock();
try {
// 如果商品个数等于数组的长度,商品满了将生产将等待消费者消费
while (count == items.length) {
try {
System.out.println("仓库满了--" + Thread.currentThread().getName() + "生产等待");
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产索引对应的商品,放在仓库中
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
items[putptr] = x;
// 如果下标索引加一等于数组长度,将索引重置为0,重新开始
if (++putptr == items.length) {
putptr = 0;
}
// 商品数加1
++count;
System.out.println(Thread.currentThread().getName() + "生产了" + x + "共有" + count + "个");
// 唤醒消费线程
notEmpty.signal();
} finally {
// 释放锁
lock.unlock();
}
}
// 消费方法
public Object take() {
//获得锁
lock.lock();
try {
//如果商品个数为0.消费等待
while (count == 0) {
try {
System.out.println("仓库空了--" + Thread.currentThread().getName() + "消费等待");
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//获得对应索引的商品,表示消费了
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object x = items[takeptr];
//如果索引加一等于数组长度,表示取走了最后一个商品,消费完毕
if (++takeptr == items.length)
//消费索引归零,重新开始消费
{
takeptr = 0;
}
//商品数减一
--count;
System.out.println(Thread.currentThread().getName() + "消费了" + x + "还剩" + count + "个");
//唤醒生产线程
notFull.signal();
//返回消费的商品
return x;
} finally {
//释放锁
lock.unlock();
}
}
}
static class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
resource.put("面包");
}
}
}
static class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
resource.take();
}
}
}
}
输出ABCABC
编写一个程序,开启3 个线程,这三个线程的name分别为A、B、C,每个线程将自己的名字 在屏幕上打印10 遍,要求输出的结果必须按名称顺序显示。
public class PrintABC {
ReentrantLock lock = new ReentrantLock();
Condition A = lock.newCondition();
Condition B = lock.newCondition();
Condition C = lock.newCondition();
private int flag = 1;
public void printA(int i) {
lock.lock();
try {
while (flag != 1) {
try {
A.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
flag = 2;
B.signal();
} finally {
lock.unlock();
}
}
public void printB(int i) {
lock.lock();
try {
while (flag != 2) {
try {
B.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
flag = 3;
C.signal();
} finally {
lock.unlock();
}
}
public void printC(int i) {
lock.lock();
try {
while (flag != 3) {
try {
C.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
System.out.println("---------------------");
flag = 1;
A.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
PrintABC testABC = new PrintABC();
Thread A = new Thread(new A(testABC), "A");
Thread B = new Thread(new B(testABC), "B");
Thread C = new Thread(new C(testABC), "C");
A.start();
B.start();
C.start();
}
static class A implements Runnable {
private PrintABC testABC;
public A(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printA(i + 1);
}
}
}
static class B implements Runnable {
private PrintABC testABC;
public B(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printB(i + 1);
}
}
}
static class C implements Runnable {
private PrintABC testABC;
public C(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printC(i + 1);
}
}
}
}
使用高级阻塞队列实现生产消费
前面的案例都是使用的比较原始的方法,适合初学者,这里使用高级阻塞队列实现通知等待/生产消费。
public class BlockingQueuePC {
//定义一个阻塞队列
static LinkedBlockingQueue<Object> objects = new LinkedBlockingQueue<>();
public static void main(String[] args) {
Resource resource = new Resource("面包");
Consumer consumer = new Consumer();
Producer producer = new Producer(resource);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//启动多个生产者\消费者线程
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.shutdown();
}
/**
* 消费者
*/
static class Consumer implements Runnable {
public Object take() throws InterruptedException {
return objects.take();
}
@Override
public void run() {
while (true) {
try {
Object take = take();
System.out.println(Thread.currentThread().getName() + "消费了" + take + ",还剩" + objects.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
static class Producer implements Runnable {
Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
public void put(Object o) throws InterruptedException {
objects.put(o);
}
@Override
public void run() {
while (true) {
try {
put(resource);
System.out.println(Thread.currentThread().getName() + "生产了" + resource + ",还剩" + objects.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 产品/资源
*/
static class Resource {
String name;
public Resource(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
}
我们的代码非常简单,并没有使用任何同步,那么如果做到线程安全和通信的呢,实际上这些活都被阻塞队列帮我们做了,对比上一个手动实现的生产消费+仓库案例,这个是不是简单得多,效率也更高呢?这些都是属于JUC包中的内容,想要学好Java并发,JUC是所有人绕不过去的坎!
使用Semaphore信号量
Semaphore是JDK1.5出现的类,翻译成字面意思为“信号量”,属于JUC,是synchronized的加强版,可以用来控制线程的并发数量。
Semaphore可以控制同时访问共享资源的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。它通过协调各个线程,以保证合理的使用公共资源。相比synchronized和lock锁一次只能允许一个线程访问资源,功能更加强大。
案例:若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:
public class Worker extends Thread {
private int num;
private Semaphore semaphore;
public Worker(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
public static void main(String[] args) {
//工人数
int N = 8;
//机器数目 许可=5
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < N; i++)
new Worker(i, semaphore).start();
}
@Override
public void run() {
try {
//获取permits个许可,若无许可能够获得,则会一直等待,直到获得许可。
semaphore.acquire();
System.out.println("工人" + this.num + "占用一个机器在生产...");
Thread.sleep(2000);
System.out.println("工人" + this.num + "释放出机器");
//释放许可。注意,在释放许可之前,必须先获获得许可。
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用管道流
对于Piped类型的流,必须先要进行绑定,即调用connect方法,如果没有绑定,那么将会抛出异常。管倒流通信类似于聊天室。用的比较少
public class PipeTest {
public static void main(String[] args) throws IOException {
PipedWriter pipedWriter = new PipedWriter();
PipedReader pipedReader = new PipedReader();
pipedWriter.connect(pipedReader);
Thread printThread = new Thread(new Print(pipedReader), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
pipedWriter.write(receive);
}
} finally {
pipedWriter.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException e) {
}
}
}
}
参考:
《实战Java高并发程序设计(第2版) 》
《Java并发编程的艺术》
《Java并发编程之美》
上一篇: 经典笔试题:3个线程连续打印XYZ
下一篇: lhgcalendar时间范围选择
推荐阅读