多线程之间的数据通讯方法
生产者消费者模式
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括生产者、消费者、仓库和产品。他们之间的关系如下:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产。
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
- 当消费者发现仓库没产品可消费时候会通知生产者生产。
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
编码实现
仓库类,产品就是仓库的属性data
//临界资源
public class Basket {
private volatile Object data;
//生产者向仓库中存放数据
public synchronized void product(Object data) {
//如果仓库中有数据,则生产者进入阻塞等待,直到其它线程唤醒
while(this.data!=null)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果没有数据则进行生产操作
this.data=data;
System.out.println(Thread.currentThread().getName()+"生产了一个日期"+this.data);
this.notifyAll();//唤醒在当前对象上处于wait的所有线程
}
//消费者从仓库中消费数据
public synchronized void consume() {
//如果仓库中没有数据,则消费者进入阻塞等待,直到其它线程唤醒
while(this.data==null)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果有数据data!=null,则执行消费操作
System.out.println(Thread.currentThread().getName()+"消费了一个数据"+this.data);
this.data=null;
this.notifyAll(); //唤醒在当前对象上处于wait的所有线程
}
}
生产者线程负责生产产品,并和消费者共享仓库
public class Producer extends Thread {
private Basket basket;
//通过构造器传入对应的basket对象
public Producer(Basket basket) {
this.basket=basket;
}
@Override
public void run() {
//生产20次日期对象
for(int i=0;i<20;i++) {
Object data=new Date(); //生产者生产的具体产品
basket.product(data);
}
}
}
消费者线程负责消费产品,并和生产者共享仓库
public class Consumer extends Thread {
private Basket basket;
public Consumer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
// 消费20次日期对象
for (int i = 0; i < 20; i++) {
basket.consume();
}
}
}
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
为什么要使用生产者/消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
生产者/消费者模型优点
1、解耦。因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这一点很容易想到,这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为了生产者和缓冲区/消费者和缓冲区之间的弱耦合
2、通过平衡生产者和消费者的处理能力来提高整体处理数据的速度,这是生产者/消费者模型最重要的一个优点。如果消费者直接从生产者这里拿数据,如果生产者生产的速度很慢,但消费者消费的速度很快,那消费者就得占用CPU的时间片白白等在那边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡
生产者/消费者模式的作用
- 支持并发
- 解耦
- 支持忙闲不均
调用wait/notify之类的方法要求必须在当前线程对象内部,例如synchronized方法中
单例模式Singleton
保证在VM中只有一个实例
- 饿汉模式
- 私有构造器
- 私有的静态属性 private static Singleton instance=new Singleton();
- 共有的静态方法
- 懒汉模式
- 私有构造器
- 私有的静态属性,不直接创建对象
- 共有的静态方法
public class Singleton {
private Singleton() {
}
private static Singleton instance;
public static Singleton getInstance() {
if(instance==null)
instance=new Singleton(); //当第一次使用对象时才进行创建
return instance;
}
}
有可能会有对象的多次创建,如何解决
public class Singleton {
private Singleton() {
}
private static Singleton instance;
public synchronized static Singleton getInstance() {
if(instance==null)
instance=new Singleton(); //当第一次使用对象时才进行创建
return instance;
}
}
一般不建议使用颗粒度较大的锁处理机制,并发性会受到影响
双检测的懒汉模式
-
按需创建对象,避免没有用处的创建操作
-
线程安全
public class Singleton{
private Singleton(){
}
private static Singleton instance=null;
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null)
instance=new Singleton();
}
}
return instance;
}
}
Lock的使用
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口,可以通过显式定义同步锁对象来实现同步,能够提供比synchronized更广泛的锁定操作,并支持多个相关的Condition对象
- void lock();尝试获取锁,获取成功则返回,否则阻塞当前线程
void lockInterruptibly() throws InterruptedException;尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
boolean tryLock();尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock(long time, TimeUnit unit)尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
- void unlock();释放锁
- Condition newCondition();返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
private final ReentrantLock lock=new ReentrantLock();
在具体方法中lock.lock() try{}finally{lock.unlock}
Lock lock=new ReentrantLock();//构建锁对象
try{
lock.lock();//申请锁,如果可以获取锁则立即返回,如果锁已经被占用则阻塞等待
System.out.println(lock);//执行处理逻辑
} finally{
lock.unlock();//释放锁,其它线程可以获取锁
}
样例1:启动4个线程,对一个int数字进行各50次加减操作,要求2个加,2个减,保证输出的线程安全
public class OperNum {
private int num=0;
private final static Lock lock=new ReentrantLock(); //构建锁对象
public void add() {
try {
lock.lock(); //申请加锁操作,如果能加上则立即返回,否则阻塞当前线程
num++;
System.out.println(Thread.currentThread().getName()+"add..."+num);
} finally {
lock.unlock(); //具体实现采用的是重入锁,所以持有锁的线程可以多次申请同一个锁,但是申请加锁次数必须和释放锁的次数一致
}
}
public void sub() {
try {
lock.lock();
num--;
System.out.println(Thread.currentThread().getName()+"sub..."+num);
} finally {
lock.unlock();
}
}
}
Condition接口
Condition是j.u.c包下提供的一个接口。可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。
void await() throws InterruptedException;让线程进入等待,如果其他线程调用同一Condition对象的notify/notifyAll,那么等待的线程可能被唤醒。释放掉锁
void signal();唤醒等待的线程
void signalAll();唤醒所有线程
使用Condition的特殊点:
- 当调用condition.await()阻塞线程时会自动释放锁,不管调用了多少次lock.lock(),这时阻塞在lock.lock()方法上线程则可以获取锁
- 当调用condition.signal()唤醒线程时会继续上次阻塞的位置继续执行,默认会自动重新获取锁(注意和阻塞时获取锁的次数一致)