第三章:(1)线程间通信—Synchronized实现线程通信&虚假唤醒

一、线程间通信

  线程间通信的模型有两种:共享内存 和 消息传递,以下方式都是基本这两种模型来实现的。

  当调用线程 start() 方法后,是由操作系统来调度的,执行顺序是不固定的。

  如果想让线程按照要求的顺序来执行,这就需要进行线程间通信。

二、多线程编程步骤(中)

  第一步:创建资源类,在资源类创建数据和操作方法;

  第二步:在资源类操作方法
  (1)判断
  (2)干活
  (3)通知

  第三步:创建多个线程,调用资源类的操作方法;

三、示例1

  1、要求

  两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信

  2、代码实现

class Resource {

    //信号量:当等于1 的时候输出输出字符,等于 0 输出数字
    private int flag= 1;

    private int count = 1;

    //打印数字
    public synchronized void printNum() throws InterruptedException {
        //判断
        while (flag != 1) {
            this.wait();
        }

        //干活
        System.out.println(2 * count - 1);
        System.out.println(2 * count);

        //通知
        flag = 0;
        this.notifyAll();
    }

    //打印字符
    public synchronized void printChar() throws InterruptedException {
        //判断
        while (flag != 0) {
            this.wait();
        }

        //干活
        System.out.println((char)(count - 1 + 'A'));
        count++;

        //通知
        flag = 1;
        this.notifyAll();
    }
}

public class Test {

    public static void main(String[] args) {
        Resource resource = new Resource();

        //创建多个线程,操作资源类
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                try {
                    resource.printNum();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                try {
                    resource.printChar();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

四、示例2

  要求:有两个线程,实现对一个初始值是 0 的变量进行操作,一个线程对当前数值加 1,另一个线程对当前数值减 1,这两个线程交替完成效果,要求用线程间通信。

  Synchronized 实现:

//第一步,创建资源类,定义属性和操作方法
class Share {
    //初始值
    private int number = 0;

    //+1
    public synchronized void incr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为0,如果不是0,等待
        if (number != 0) {
            this.wait();
        }

        //2.干活:如果 number 值是0,就 +1 操作
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();

    }

    //-1
    public synchronized void decr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为1,如果不是1,等待
        if (number != 1) {
            this.wait();
        }

        //2.干活:如果 number 值是1,就 -1 操作
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();
    }

}

public class ThreadDemo1 {

    public static void main(String[] args) {
        //第三步  创建多个线程,调用资源类的操作方法;
        Share share = new Share();

        //创建线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

五、虚假唤醒问题

  synchronized 实现多线程通知等待
  对于上面 打印 0 和 1 的案例,如果换成4个线程会怎么样呢?

  1、修改为四个线程

//第一步,创建资源类,定义属性和操作方法
class Share {
    //初始值
    private int number = 0;

    //+1
    public synchronized void incr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为0,如果不是0,等待
        if (number != 0) {
            this.wait();
        }

        //2.干活:如果 number 值是0,就 +1 操作
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();

    }

    //-1
    public synchronized void decr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为1,如果不是1,等待
        if (number != 1) {
            this.wait();
        }

        //2.干活:如果 number 值是1,就 -1 操作
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();
    }

}

public class ThreadDemo1 {

    public static void main(String[] args) {
        //第三步  创建多个线程,调用资源类的操作方法;
        Share share = new Share();

        //创建线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();
    }
}

  2、运行结果

    

    可以发现,这时的运行结果并不正确。
 

  3、分析原因

    换成了4个或多个线程会导致错误,虚假唤醒

    原因:使用了 if 判断,如果 if 判断生效了,就不会出现问题了,为什么 if 判断没有生效?

    看一下 wait() 方法说明:

     虚假唤醒原因:

    ① 当number = 1时,如果是 +1 的AA线程获取执行权,不符合条件,就会阻塞;

    ② 假设是 +1 的线程CC 获取执行权,进行 if 判断,不符合条件,就会堵塞;

    ③ 假设 -1 的线程 BB 获取执行权,执行完 -1,唤醒所有线程;

    ④ 此时 AA 线程被唤醒,不再进行 if 判断,执行 + 1,再唤醒其他线程;

    ⑤ 此时 CC 线程被唤醒,也不再进行 if 判断,执行 +1,此时 number =2;

    主要是在多线程判断时,使用了 if 判断。

    如果一个线程进入到了 if了,突然中断失去了控制权,等再次被唤醒,就不再进行验证,而是直接执行下面的代码(wait 在哪里睡,就在哪里醒),就会出现虚假唤醒情况。

    

    原理图

    正常情况:

    

    四个线程时:

    

    当 number = 1 的时候,加一的线程线程进来之后,判断不符合,就会阻塞在这里;
    假如另一个加一的线程获取执行权,这是判断也不会符合,同时阻塞在这里;
    当有一个减一的线程进来之后,执行完了之后,会把两个 加一 的线程同时唤醒,
    当有一个加一的线程执行完成后,number =1,但此时是 if 判断,不会再次进行判断,
    另外一个 加一的线程 也会 加一,number =2,所以就出现了虚假唤醒情况。

  4、解决办法

    wait() 应该使用在方法体中。
//第一步,创建资源类,定义属性和操作方法
class Share {
    //初始值
    private int number = 0;

    //+1
    public synchronized void incr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为0,如果不是0,等待
        while (number != 0) {
            this.wait();  //在哪里睡,就会在哪里醒
        }

        //2.干活:如果 number 值是0,就 +1 操作
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();

    }

    //-1
    public synchronized void decr() throws InterruptedException {
        //第二步 判断,干活,通知
        //1.判断:number 值是否为1,如果不是1,等待
        while (number != 1) {
            this.wait();
        }

        //2.干活:如果 number 值是1,就 -1 操作
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);

        //3.通知
        this.notifyAll();
    }

}

public class ThreadDemo1 {

    public static void main(String[] args) {
        //第三步  创建多个线程,调用资源类的操作方法;
        Share share = new Share();

        //创建线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();
    }
}
    为了防止虚假唤醒用 while 判断
    注意:一定要注意多线程之间的虚假唤醒

六、多线程编程步骤(下)

  第一步:创建资源类,在资源类创建数据和操作方法;

  第二步:在资源类操作方法
  (1)判断
  (2)干活
  (3)通知

  第三步:创建多个线程,调用资源类的操作方法;

  第四步:为了防止虚假唤醒用 while 判断;

 

 

原文地址:https://www.cnblogs.com/niujifei/p/15820141.html