1.线程间的通信
使用wait/notify来实现线程间的通信
生产者/消费者模式的实现
方法join的使用
ThreadLocal类的使用
2.wait/notify:
Wait()方法的作用是使当前执行线程的方法进行等待,该方法用来将当前线程置入“预执行队列中”,并且在wait所在的代码处执行停止。直到接到通知或被中断为止。调用wait()方法也是有限制的,就是线程必须获得对象的对象锁之后才能执行这个方法,言下之意就是wait()方法必须在同步方法或者同步代码块中执行。执行wait()方法后,当前线程释放锁。在从wait()方法返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持适当的锁,则抛出异常。
Notify()该方法是用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程处于等待状态,则由线程规划器随机挑选出其中一个呈wait()状态的线程,对其发出通知notify,并使他等待获取该对象的对象锁。和wait()方法一样,notify()方法也是需要在同步方法同步块中执行。即在调用前,线程也必须获得该对象的对象级别的锁。如果调用notify()时没有获得对应的锁会报错。需要注意的是:在执行notify()方法后,当前线程不会马上释放该对象锁。呈wait状态的线程也不会马上获得该对象锁。要等到执行notify方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁。而呈wait状态的所在的线程才能获得对象锁。当第一个获得了该对象锁的wait线程完毕以后,他会释放掉该对象锁。此时如果该对象没有再次使用notify()语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有接到该对象的通知,将会继续阻塞在wait状态,直到这个对象发出notify.
体验等待通知机制:
线程A:
package com.wx.concurrent1;
public class ThreadA extends Thread {
private String lock;
public ThreadA(String _lock)
{
this.lock=_lock;
}
@Override
public void run() {
try{
synchronized (lock)
{
System.out.println("wait()前面");
lock.wait();
System.out.println("wait()后面");
}
}catch (Exception e)
{
e.printStackTrace();
}
}
}
线程B:
package com.wx.concurrent1;
public class ThreadB extends Thread {
private String lock;
public ThreadB(String _locak)
{
this.lock=_locak;
}
@Override
public void run() {
synchronized (lock)
{
System.out.println("notify方法前面");
lock.notify();
System.out.println("notify方法后面");
}
}
}
测试:
package com.wx.concurrent1;
public class Test2 {
public static void main(String[] args)
{
try{
String lock=new String();
ThreadA threadA=new ThreadA(lock);
ThreadB threadB=new ThreadB(lock);
threadA.start();
Thread.sleep(3000);
threadB.start();
}catch (Exception e)
{
e.printStackTrace();
}
}
}
synchronized可以将任何一个Object对象作为一个同步对象来看待,而java为每个Object对象都实现了wait和notify方法。他们必须用在被synchronized同步的Object临界区内。通过调用wait方法可以使临界区进入内的线程等待状态。同时释放被同步的对象锁。而notify可以唤醒一个因调用了wait操作而处于阻塞状态的线程,使其进入就绪状态。被重新唤醒的线程会试图重新获得临界区的控制权,也就是锁。并继续执行临界区内wait之后的代码。如果发出notify操作没有处于阻塞状态中的线程,那么该命令将会被忽略。
Wait方法可使调用该方法的线程释放共享资源的锁,从运行状态退出,进入等待队列,直到再次被唤醒。
Notify方法可以唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入就绪状态。
NitifyAll方法可以使所有正在等待队列中等待同一共享资源的全部线程从等待状态退出。进入可运行状态。同时优先级最高的哪个线程最优先执行,但也有可能随机执行,看JVM的实现。
Runnable(可运行)状态和Running(运行)状态:
一般线程调用start()方法以后系统就会为线程分配CPU资源,这时线程就是可运行状态,如果线程抢到Cpu资源,线程就会进入运行状态。
线程进入可运行状态的5种姿势:
- 调用sleep()方法后经过的时间超过了指定的休眠时间。
- 线程调用的阻塞IO已经返回,阻塞方法方法执行完毕
- 线程成功的获得了试图同步的监视器
- 线程正在等待某个通知,其他线程发出了通知
- 处于挂起状态的线程调用了resume方法
处于阻塞状态的5种姿势:
- 线程调用sleep方法,主动放弃占用的处理器资源。
- 线程调用了阻塞式IO的方法,在该方法返回前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程等待某个通知。
- 程序带哦用了suspend方法将该线程挂起,此方法容易导致死锁,尽量避免使用该方法。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程。阻塞队列存储了被阻塞的线程。一个线程被唤醒后才会进入就绪队列。等待CPU的调度,反之,一个线程被wait后,就会进入阻塞队列等待下一次被唤醒。
如果当线程处于wait状态的时候,调用线程中断的方法interrupt()会出现异常。
锁被释放的几种情况:
执行完同步代码块后就会释放对象的锁,
执行同步代码块遇到异常使线程终止,也会释放对象锁,
执行同步代码块的时候,如果执行了锁所属对象的wait()方法,这个线程会释放对象锁。而此线程对象会进入线程等待池中,等待被唤醒。
Wait(long):带一个参数的Wait(long)方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果没有则自动唤醒。
如果notify通知过早则会打乱程序的正常运行。
基于等待通知模型的生产者消费者模型:
1.操作值
创建一个生产者,生产之前判断一下值是否为空,如果不为空就调用wait()释放当前的锁,让消费者去消费,消费完了再赋值,赋值完唤醒消费者线程:
package com.wx.concurrent3;
public class P {
private String lock;
public P(String _locak)
{
this.lock=_locak;
}
public void setValue()
{
try {
synchronized (lock)
{
if(!ValueObject.value.equals(""))
{
lock.wait();
}
String value= System.currentTimeMillis()+"_"+System.nanoTime();
System.out.println("set的值是:"+value);
ValueObject.value=value;
lock.notify();
}
}catch (Exception e)
{
e.printStackTrace();
}
}
}
消费者线程,如果消费的数据为空就释放当前锁给生产者,如果不为空就消费,消费完毕唤醒生产者线程:
package com.wx.concurrent3;
public class C {
private String lock;
public C (String _locak)
{
this.lock=_locak;
}
public void getValue()
{
try {
synchronized (lock)
{
if(ValueObject.value.equals(""))
{
lock.wait();
}
String value= System.currentTimeMillis()+"_"+System.nanoTime();
System.out.println("get的值是:"+value);
ValueObject.value="";
lock.notify();
}
}catch (Exception e)
{
e.printStackTrace();
}
}
}
package com.wx.concurrent3;
public class ValueObject {
public static String value="";
}
创建两个线程,一个消费者线程,一个生产者线程:
package com.wx.concurrent3;
public class ThreadA extends Thread {
private P p;
public ThreadA(P _p)
{
this.p=_p;
}
@Override
public void run() {
while(true)
{
p.setValue();
}
}
}
package com.wx.concurrent3;
public class ThreadB extends Thread {
private C c ;
public ThreadB(C _c)
{
this.c=_c;
}
@Override
public void run() {
while(true)
{
c.getValue();
}
}
}
测试:
package com.wx.concurrent3;
public class Test1 {
public static void main(String[] args)
{
String lock=new String("");
P p=new P(lock);
C c=new C(lock);
ThreadA threadA=new ThreadA(p);
ThreadB threadB=new ThreadB(c);
threadA.start();
threadB.start();
}
}
如果有多个生产者和多个消费者,则可能会出现假死的现象,即所有的线程都进入了等待状态,程序永远不可能执行完了。为什么会出现这样的情况呢?因为可能是生产者唤醒了生产者,消费者唤醒了消费者,最后大家都会进入wait等待状态了。
那么如何解决假死呢?将notify方法换成notifyAll方法,这样就就可以通知到所有处于等待的状态的线程。
2.操作栈:生产者向list集合中添加数据,然后消费者取,保证容器的最大容量为1
第一种情况,一个生产者,一个消费者
创建一个栈类:
package com.wx.concurrent4;
import java.util.ArrayList;
import java.util.List;
public class MyStack {
private List list=new ArrayList();
//压入栈的方法
synchronized public void push()
{
try {
if(list.size()==1)
{
this.wait();
}
list.add("anyString"+Math.random());
this.notify();
System.out.println("push="+list.size());
}catch (Exception e)
{
e.printStackTrace();
}
}
//弹出栈的方法
synchronized public void pop()
{
try{
if (list.size()==0)
{
System.out.println("pop的操作中线程"+Thread.currentThread().getName()+"呈wait()状态");
this.wait();
}
list.remove(0);
this.notify();
System.out.println(" pop="+ list.size());
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
生产者和消费者类:
package com.wx.concurrent4;
public class P {
private MyStack myStack;
public P(MyStack myStack)
{
this.myStack=myStack;
}
public void push()
{
myStack.push();
}
}
package com.wx.concurrent4;
public class C {
private MyStack myStack;
public C(MyStack myStack)
{
this.myStack=myStack;
}
public void pop()
{
myStack.pop();
}
}
生产者和消费者线程:
package com.wx.concurrent4;
public class ThreadA extends Thread {
private P p;
public ThreadA(P _p)
{
this.p=_p;
}
@Override
public void run() {
while(true)
{
p.push();
}
}
}
package com.wx.concurrent4;
public class ThreadB extends Thread {
private C c ;
public ThreadB(C _c)
{
this.c=_c;
}
@Override
public void run() {
while(true)
{
c.pop();
}
}
}
测试(最初的目的实现了,容器的大小始终在0和1之间交替):
package com.wx.concurrent4;
public class Test1 {
public static void main(String[] args)
{
MyStack myStack=new MyStack();
P p=new P(myStack);
C c=new C(myStack);
ThreadA threadA=new ThreadA(p);
ThreadB threadB=new ThreadB(c);
threadA.start();
threadB.start();
}
}
第二种情况,生产者还是一个,但是消费者有多个,还是保证容器最大容量为1
运行报错:
从运行的结果分析原因,首先生产者线程启动,消费者线程也启动,生产者生产完了以后,通知消费者消费,消费完了以后,通知生产者,然后在生产者被唤醒试图争夺临界区控制权的时候,其他阻塞在同步方法外的消费者已经进入同步方法,挨着调用了wait()方法进入等待状态,之后锁被释放,然后生产者得到锁,生产并唤醒消费者。随机一个消费者被唤醒后进行消费。消费完了以后通知处于wait()状态的线程,重点来了,通知的一定是处于wait的生产者吗?当然不是,可能通知的是处于wait()状态的消费者,并且几率很大,另一个消费者再次被唤醒接着执行wait()之后的语句list.remove(0);这就是报错的原因。
得知原因后如何解决这个问题呢?显然会想到上面用过的使用notifyAll()方法通知所有处于等待状态的线程,但是他依然会唤醒消费者线程,依然会执行list.remove(0)操作,依然会报错。那么怎么办呢?如果处于等待状态的线程被唤醒后再次执行一下判断条件就好了。这里就需要知道if条件和while条件的区别了?他们的区别在这里起了很大的作用,if条件中线程被唤醒后直接执行wait()后面的语句,不再执行if的判断语句,但是如果使用while的话,从wait下面继续执行,还会返回执行while的条件判断.所以if判断改为while判断问题解决。为了避免假死需要使用notifyAll()。
第三种情况,还是操作值栈,多生产与一个消费者,上面的问题解决了这个就不会有问题了。
第四种情况:多生产多消费,也没有问题了。