多线程之间的数据通讯方法

多线程之间的数据通讯方法

生产者消费者模式

生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括生产者、消费者、仓库和产品。他们之间的关系如下:

  • 生产者仅仅在仓储未满时候生产,仓满则停止生产。
  • 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
  • 当消费者发现仓库没产品可消费时候会通知生产者生产。
  • 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

编码实现

仓库类,产品就是仓库的属性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()唤醒线程时会继续上次阻塞的位置继续执行,默认会自动重新获取锁(注意和阻塞时获取锁的次数一致)

猜你喜欢

转载自blog.csdn.net/qq_43480434/article/details/114153429