并发编程(3)线程之间的通信

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SDDDLLL/article/details/87868291

在上一篇博客主要是对象的并发访问,这篇博客主要是讲解线程之间的通信,线程之间的通信能极大地增强系统的交互性。

在这里我们使用wait/notify方法来实现线程之间的通信。这个两个方法都是object的类的方法,也就是说java为所有的对象都提供了这两个方法。有两点需要注意

  • wait和notify必须配合synchronized关键字使用。
  • wait方法释放锁,notify方法不释放锁

下面上例子,看看线程之间如何实现通信

一、不使用等待/通知机制实现线程间通信

下面这个例子要实现的功能是:线程一通过for循环,使得list每次添加一条数据,线程二不断去检测list的大小,当list的size等于5的时候抛出一个异常。

public class ListAdd1 {
	private volatile static List<String> list = new ArrayList<String>();	
	public void add(){
		list.add("bjsxt");
	}
	public int size(){
		return list.size();
	}
	
	public static void main(String[] args) {
		
		final ListAdd1 list1 = new ListAdd1();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					for(int i = 0; i <10; i++){
						list1.add();
						System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
						Thread.sleep(500);
					}	
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				while(true){
					if(list1.size() == 5){
						System.out.println("当前线程收到通知:" + Thread.currentThread().getName() + " list size = 5 线程停止..");
						throw new RuntimeException();
					}
				}
			}
		}, "t2");		
		
		t1.start();
		t2.start();
	}
}

看一下结果:

从上面的结果我们可以看到,线程一添加了5条数据之后,线程二抛出了一个异常。但是这种实现方式有一个弊端。此时线程二不断地去查询list的size,这样会浪费掉CPU资源,这时候我们就可以使用等待/通知机制来实现。

二、使用wait/notify方式来实现

下面这个例子功能跟上面的一样,但是有一些代码有区别。

public class ListAdd2 {
	private volatile static List list = new ArrayList();	
	
	public void add(){
		list.add("bjsxt");
	}
	public int size(){
		return list.size();
	}
	
	public static void main(String[] args) {
		
		final ListAdd2 list2 = new ListAdd2();
		final Object lock = new Object();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					synchronized (lock) {
						System.out.println("t1启动..");
						for(int i = 0; i <10; i++){
							list2.add();
							System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
							Thread.sleep(500);
							if(list2.size() == 5){
								System.out.println("已经发出通知..");
								lock.notify();
							}
						}						
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (lock) {
					System.out.println("t2启动..");
					if(list2.size() != 5){
						try {
							lock.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.println("当前线程:" + Thread.currentThread().getName() + "收到通知线程停止..");
					throw new RuntimeException();
				}
			}
		}, "t2");	
		//一定要t2先执行,这是因为如果t1先执行的话,由于有同步锁的关系,size会一直增加到10
		//但是t2此时才刚刚启动,她启动之后获得的size是10,永远不是5
		t2.start();
		t1.start();		
	}
}

我们可以看到,在这里我们使用的是同步方式,然后线程一在list的size等于5的时候,发送一个通知notify。线程二不断地去等待。此时wait方法释放锁,这是因为线程一的notify能够通知到线程二。需要注意的事项在一开始已经说明,

先看一看结果,跟上面的一样。

好了,从结果可以看出,这个结果其实是有一点问题的,在线程一发送完通知之后,线程二并没有立即抛出异常,一直等到线程一执行完毕之后才抛出,那么有什么办法能够立即抛出异常呢?

三、使用CountDownLatch实时实现通信

这个CountDownLatch的作用就是,在线程二获得通知之后,能够立即抛出异常,但此时就不需要同步代码锁了。

public class ListAdd3 {
	private volatile static List<String> list = new ArrayList<String>();	
	public void add(){
		list.add("aaaa");
	}
	public int size(){
		return list.size();
	}
	
	public static void main(String[] args) {
		final ListAdd3 list2 = new ListAdd3();
		//括号中的1表示发起一次通知,若为2,那么线程一发出2次notify
		final CountDownLatch countDownLatch = new CountDownLatch(1);
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
						System.out.println("t1启动..");
						for(int i = 0; i <10; i++){
							list2.add();
							System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
							Thread.sleep(500);
							if(list2.size() == 5){
								System.out.println("已经发出通知..");
								countDownLatch.countDown();
							}
						}							
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {		
					System.out.println("t2启动..");
					if(list2.size() != 5){
						try {
							countDownLatch.await();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.println("当前线程:" + Thread.currentThread().getName() + "收到通知线程停止..");
					throw new RuntimeException();
				
			}
		}, "t2");	
		//一定要t2先执行,这是因为如果t1先执行的话,由于有同步锁的关系,size会一直增加到10
		//但是t2此时才刚刚启动,她启动之后获得的size是10,永远不是5
		t2.start();
		t1.start();	
	}
}

再来看一下实验结果:

从上面的结果可以看到,没有等待线程一执行完毕,线程二就已经抛出了异常。

四、例子:使用wait/notify模拟Queue

BlockingQueue:也就是一个队列,并且支持阻塞的机制,阻塞的放入和得到数据。现在我们要实现这个队列里面的两个基本操作,put数据和take数据。

  • put:把一个object放到队列里面。如果此时队列已满,则调用此方法的线程阻塞,一直到队列里面有空间才继续
  • take:取走一个元素,若队列为空,那么调用此方法的线程阻塞,一直到队列里面有元素才取走

看看代码实现,下面这个例子的功能是,创建了一个包含5个元素的队列,此时线程一想要继续添加数据,但是需要等到线程二取走才能添加。实现的原理就是通过wait/和notify机制来实现的。

public class MyQueue {
	//需要一个承载元素的集合
	private final LinkedList<Object> list = new LinkedList<Object>();
	//需要一个计数器,这个计数器是原子性的
	private final AtomicInteger count = new AtomicInteger(0);
	//需要指定队列的上限和下限
	private final int maxSize;
	private final int minSize = 0;
	
	private final Object lock = new Object();
	
	public MyQueue (int maxSize){
		this.maxSize = maxSize;
	}

	public void put (Object obj) {
		synchronized(lock){
			while(count.get() == maxSize){
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			list.add(obj);
			count.getAndIncrement();
			System.out.println(" 元素 " + obj + " 被添加 ");
			lock.notify();	
		}
	}
	
	public Object take(){
		Object temp = null;
		synchronized (lock) {
			while(count.get() == minSize){
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			count.getAndDecrement();
			temp = list.removeFirst();
			System.out.println(" 元素 " + temp + " 被消费 ");
			lock.notify();
		}
		return temp;
	}
	
	public int size(){
		return count.get();
	}
	
	
	public static void main(String[] args) throws Exception {
		
		final MyQueue m = new MyQueue(5);
		m.put("a");
		m.put("b");
		m.put("c");
		m.put("d");
		m.put("e");
		System.out.println("当前元素个数:" + m.size());
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				m.put("h");
				m.put("i");
			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(1000);
					Object t1 = m.take();
					System.out.println("被取走的元素为:" + t1);
					Thread.sleep(1000);
					Object t2 = m.take();
					System.out.println("被取走的元素为:" + t2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t2");

		t1.start();
		Thread.sleep(2000);
		t2.start();
	}
}

看看结果:

五、ThreadLocal

概念:线程局部变量,是一种多线程之间并发放稳变量的解决方案,与synchronized等加锁的方法不一样,他完全不提供锁。而是以空间换时间的手段,为每一个线程提供独立的副本,以保障线程安全。

从性能上来讲,Thread不具备绝对的优势,在并发访问不是很高的情况下,加锁的性能更好,但是在高并发或者是竞争激烈的场景,使用threadLocal可以在一定程度上减少锁的竞争。

public class ConnThreadLocal {
	public static ThreadLocal<String> th = new ThreadLocal<String>();
	public void setTh(String value){
		th.set(value);
	}
	public void getTh(){
		System.out.println(Thread.currentThread().getName() + ":" + this.th.get());
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		final ConnThreadLocal ct = new ConnThreadLocal();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				ct.setTh("张三");
				ct.getTh();
			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(1000);
					ct.setTh("李四");
					ct.getTh();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t2");
		
		t1.start();
		t2.start();
	}
}

仔细看完代码,就不解释代码了。看结果:

好了线程之间的通信比较简单。到此为止

猜你喜欢

转载自blog.csdn.net/SDDDLLL/article/details/87868291