在上一篇博客主要是对象的并发访问,这篇博客主要是讲解线程之间的通信,线程之间的通信能极大地增强系统的交互性。
在这里我们使用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();
}
}
仔细看完代码,就不解释代码了。看结果:
好了线程之间的通信比较简单。到此为止