1. 异常问题(wait问题)
wait中断:在调用处立即中断并且释放锁,该线程转入等待队列。当收到持有同一把锁的线程notify通知后进入锁池队列,再次获得同步锁之后才会继续执行。这里的继续执行是从上次中断处继续向下执行。
package 多线程核心.生产者与消费者问题.一生产多消费操作栈假死与异常;
import java.util.ArrayList;
class MyService{
private ArrayList list = new ArrayList();
synchronized public void push() throws InterruptedException {
if(list.size() == 1){ //改为while即可解决异常问题
System.out.println(Thread.currentThread().getName()+" -> wait");
this.wait();
}
System.out.println(Thread.currentThread().getName()+" -> 生产");
list.add(Math.random());
System.out.println("size = " + list.size());
this.notify();
}
synchronized public void pop() throws InterruptedException {
if(list.size() == 0){ //改为while即可解决异常问题
System.out.println(Thread.currentThread().getName()+" -> wait");
this.wait();
}
System.out.println(Thread.currentThread().getName()+" -> 消费");
list.remove(0);
System.out.println("size = " + list.size());
this.notify();
}
}
class P{
private MyService myService;
public P(MyService myService) {
this.myService = myService;
}
public void pushService(){
try{
myService.push();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class C{
private MyService myService;
public C(MyService myService) {
this.myService = myService;
}
public void popService(){
try{
myService.pop();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadP extends Thread{
private P p;
public ThreadP(P p, String name) {
super(name);
this.p = p;
}
@Override
public void run() {
while(true)
p.pushService();
}
}
class ThreadC extends Thread{
private C c;
public ThreadC(C c, String name) {
super(name);
this.c = c;
}
@Override
public void run() {
while(true)
c.popService();
}
}
public class Demo {
public static void main(String[] args) {
MyService myService = new MyService();
P p1 = new P(myService);
C r1 = new C(myService);
C r2 = new C(myService);
C r3 = new C(myService);
C r4 = new C(myService);
C r5 = new C(myService);
ThreadP p = new ThreadP(p1,"生产者");
p.start();
ThreadC c1 = new ThreadC(r1,"消费者-1");
ThreadC c2 = new ThreadC(r2,"消费者-2");
ThreadC c3 = new ThreadC(r3,"消费者-3");
ThreadC c4 = new ThreadC(r4,"消费者-4");
ThreadC c5 = new ThreadC(r5,"消费者-5");
c1.start();
c2.start();
c3.start();
c4.start();
c5.start();
try{
Thread.sleep(5000);
Thread[] arr = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(arr);
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i].getName() + " -> " + arr[i].getState());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
-
分析:出现异常是因为ArratList删除元素越界了,首先要了解wait中断机制。wait中断是在此处立即中断并且释放锁,转入等待队列。当收到notify通知后进入锁池队列,再次获得同步锁之后才会继续执行。这里的继续执行是从上次中断处继续向下执行。当多个线程都wait之后,突然有个wait线程中断恢复继续向下执行,由于并没有什么条件判断或者限制导致size = 0时依旧将第1个(下标为0)的元素移除,此时ArrayList中已经为空,所以导致数组越界。
-
由于中断恢复回来之后,中断前的状态 和 中断恢复后的状态是无法预测的,谁也不知道恢复后是个什么样子,中断时size=0才中断,中断恢复后并不知道此时size是否依旧为0? 所以在恢复后依旧需要继续判断最初的中断条件是否成立。
-
解决方法:将if 改为 while。这样即使回来之后size = 0依旧会进行一次判断,如果size == 0继续中断,不为0才能移除ArrayList中的元素。
2. 假死问题(notify问题)
notify唤醒:随机唤醒一个持有同一把锁的wait线程,这个线程是随机无法指定的。
notifyAll唤醒:唤醒所有持有同一把锁的wait线程。
将上面代码中的if 改为 while后依旧存在问题,这里存在 “假死”。
import java.util.ArrayList;
class MyService{
private ArrayList list = new ArrayList();
synchronized public void push() throws InterruptedException {
while(list.size() == 1){
System.out.println(Thread.currentThread().getName()+" -> wait");
this.wait();
}
System.out.println(Thread.currentThread().getName()+" -> 生产");
list.add(Math.random());
System.out.println("size = " + list.size());
this.notify(); //改为notifyAll(); 解决假死
}
synchronized public void pop() throws InterruptedException {
while(list.size() == 0){
System.out.println(Thread.currentThread().getName()+" -> wait");
this.wait();
}
System.out.println(Thread.currentThread().getName()+" -> 消费");
list.remove(0);
System.out.println("size = " + list.size());
this.notify(); //改为notifyAll(); 解决假死
}
}
class P{
private MyService myService;
public P(MyService myService) {
this.myService = myService;
}
public void pushService(){
try{
myService.push();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class C{
private MyService myService;
public C(MyService myService) {
this.myService = myService;
}
public void popService(){
try{
myService.pop();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadP extends Thread{
private P p;
public ThreadP(P p, String name) {
super(name);
this.p = p;
}
@Override
public void run() {
while(true)
p.pushService();
}
}
class ThreadC extends Thread{
private C c;
public ThreadC(C c, String name) {
super(name);
this.c = c;
}
@Override
public void run() {
while(true)
c.popService();
}
}
public class Demo {
public static void main(String[] args) {
MyService myService = new MyService();
P p1 = new P(myService);
C r1 = new C(myService);
C r2 = new C(myService);
C r3 = new C(myService);
C r4 = new C(myService);
C r5 = new C(myService);
ThreadP p = new ThreadP(p1,"生产者");
p.start();
ThreadC c1 = new ThreadC(r1,"消费者-1");
ThreadC c2 = new ThreadC(r2,"消费者-2");
ThreadC c3 = new ThreadC(r3,"消费者-3");
ThreadC c4 = new ThreadC(r4,"消费者-4");
ThreadC c5 = new ThreadC(r5,"消费者-5");
c1.start();
c2.start();
c3.start();
c4.start();
c5.start();
try{
Thread.sleep(5000);
Thread[] arr = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(arr);
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i].getName() + " -> " + arr[i].getState());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
1. 假死原因分析
-
生产者 首先获得锁生产一个产品,notify指令失效(没有同锁wait线程)
-
生产者再次快速获得锁由于生产的产品还未消耗 生产者进入wait状态
-
此时消费者2获得锁将产品进行了消费(注意:这里消费者1已经诞生但是并没有获得锁,在锁池队列等待),notify唤醒唯一等待的生产者线程
-
消费者2继续获得锁,此时已经没有产品消费了, 消费者2进入wait状态
-
消费者3拿到锁,此时已经没有产品消费了, 消费者3进入wait状态
-
生产者 获得锁生产一个产品,现在等待的有消费者2 消费者3,notify唤醒哪一个是不确定的。根据图片继续分析才知道
-
生产者 再次获得锁,产品没有消耗, 生产者进入wait状态
-
消费者2获得锁,进行了消费( 这里说明第5步生产者唤醒了消费者2),此时wait的线程有,消费者3和生产者,notify唤醒哪个不知道继续向下分析
-
消费者2再次获锁,没有产品消耗, 消费者2进入wait状态
-
消费者3获锁(这里说明第7步 消费者2唤醒了消费者3),没有产品消耗, 消费者3进入wait状态
-
消费者4获锁,没有产品消耗, 消费者4进入wait状态
-
此时沉默许久的消费者1首次获得锁, 没有产品消耗, 消费者1进入wait状态
-
消费者5获锁,没有产品消耗, 消费者5进入wait状态
-
到此生产者在第6步wait之后再也没起来过,后续由于第7步的消费者2错误唤醒了同样是消费者的线程3导致一蹶不振,至此线程全线wait导致假死。
假死的原因根据上面的分析很容易得出,由于notify随机唤醒,连续唤醒同类(同样是消费者)导致假死
2. 假死解决方案
将所有的notify改为nofityAll(),这样让每个线程都将所有的线程全部唤醒,这样就解决了随机唤醒并且唤醒同类的问题。因为总能保证生产者一直处于锁池队列争抢同步锁进入Runnable状态。
一生产多消费模型 和 多生产一消费模型是一样的,一样需要注意异常和假死问题。