• 线程之间的通信方式:wait/notify


    1.什么是线程间的通信

    通信,顾名思义就是一种通知交通的方式,在多线程的环境下,如果各个线程之间可以互相通信的话,可以很好地提高工作效率,提高CPU的利用率。

    Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意如图3-1所示 

    从图3-1来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
    1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
    2)线程B到主内存中去读取线程A之前已更新过的共享变量 

    2.线程间常用的通信方式

    多线程间的通信一般采取等待/通知机制进行实现,即Object类中的wait()和notify()方法实现的,一个是等待,一个是通知。其实就像我们平时去营业厅办理业务一样,我们要先取号,然后就开始等待,等到听到叫我们号的时候,我们再过去办理业务。

    1. Java中等待/通知机制的实现
      1. 如上面所说的,wait()和notify()这两个方法都是Object类中的方法,之所以是超类的方法,其实是因为之前我们说过任何对象都可以作为锁,而这两个方法都是由锁调用的,所以很自然地就可以理解为什么这两个方法是属于超类的。
      2. wait方法:
        1. 作用是使当前执行代码的线程进行等待,该方法会将该线程放入”等待队列“中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
        2. 在调用 wait() 之前,线程必须获得该对象级别锁,即只能在同步方法或同步块中调用 wait() 方法。
        3. wait() 是释放锁的,即在执行到 wait() 方法之后,当前线程会释放锁,当从 wait() 方法返回前,线程与其他线程竞争重新获得锁
        4. 此外,还有带一个参数的wait(long),表示在等待一段时间内,如果没有唤醒线程,则会自动唤醒。当然,在这段时间内,也可以由其他线程唤醒。
      3. notify方法:
        1. 和 wait() 方法一样, notify() 方法也要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。
        2. 该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个是wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。即notify方法一次只随机唤醒一个wait状态的线程。
        3. 这里需要注意的是,执行notify方法之后,当前线程不会立即释放其拥有的该对象锁,而是执行完之后才会释放该对象锁,被通知的线程也不会立即获得对象锁,而是等待notify方法执行完之后,释放了该对象锁,才可以获得该对象锁。
        4.  notifyAll() 通知所有等待同一共享资源的全部线程从等待状态退出,进入可运行状态,重新竞争获得对象锁。即notifyAll方法可以唤醒所有wait状态的线程。
      4. wait()/notify()总结:

        用一句话来说就是:wait使线程停止运行,notify使停止的线程继续运行 。

        每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒之后,才会进入就绪队列,等待CPU的调度;反之,一个线程调用wait方法后,就会进入阻塞队列,等待下一次被唤醒。 

        1. 要结合synchronized关键字一起使用,因为他们都需要首先获取该对象的对象锁;
        2. wait方法是释放锁,notify方法是不释放锁的;
        3. 线程的四种状态如下图:
    2. wait/notify线程间通信示例代码
      1. Mylist代码:
        public class MyList {
            private static List list = new ArrayList();
        
            public static void add() {
                list.add("我是元素");
            }
        
            public static int size() {
                return list.size();
            }
        }
      2. 线程A:
        public class ThreadA extends Thread {
            private Object lock;
        
            public ThreadA(Object lock) {
                super();
                this.lock = lock;
            }
        
            @Override
            public void run() {
                try {
                    synchronized (lock) {
                        if (MyList.size() != 5) {
                            System.out.println("wait begin " +
                                System.currentTimeMillis());
                            lock.wait();
                            System.out.println("wait end " +
                                System.currentTimeMillis());
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
      3. 线程B:
        public class ThreadB extends Thread {
            private Object lock;
        
            public ThreadB(Object lock) {
                super();
                this.lock = lock;
            }
        
            @Override
            public void run() {
                try {
                    synchronized (lock) {
                        for (int i = 0; i < 10; i++) {
                            MyList.add();
        
                            if (MyList.size() == 5) {
                                lock.notify();
                                System.out.println("已发出通知!");
                            }
        
                            System.out.println("添加了" + (i + 1) + "个元素!");
                            Thread.sleep(1000);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
      4. 测试代码:
        public class Run {
            public static void main(String[] args) {
                try {
                    Object lock = new Object();
                    ThreadA a = new ThreadA(lock);
                    a.start();
                    Thread.sleep(50);
        
                    ThreadB b = new ThreadB(lock);
                    b.start();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
      5. 运行结果:
         
        wait begin 1507634541467
        添加了1个元素!
        添加了2个元素!
        添加了3个元素!
        添加了4个元素!
        已发出通知!
        添加了5个元素!
        添加了6个元素!
        添加了7个元素!
        添加了8个元素!
        添加了9个元素!
        添加了10个元素!
        wait end 1507634551563

        由上面可以看出,虽然线程B在第五个元素的时候发出通知,而线程A实现线程B执行完之后才获得对象锁,这也可以明,wait方法是释放锁的而notify方法是不释放锁的。因为如果notify方法会释放锁的话,那么应该在打印通知之前就执行线程A中的打印wait end。

    3. 使用wait/notify模拟BlockingQueue阻塞队列

      1. BlockingQueue是阻塞队列,我们需要实现的是阻塞的放入和得到数据,设计思路如下:
        (1)初始化队列最大长度为5;
        (2)需要新加入的时候,判断是否长度为5,如果是5则等待插入;
        (3)需要消费元素的时候,判断是否为0,如果是0则等待消费;

      2. 实现代码如下:

        public class MyQueue {
        	//1、需要一个承装元素的集合
        	private final LinkedList<Object> list = new LinkedList<>();
        	//2、需要一个计数器
        	private final AtomicInteger count = new AtomicInteger(0);
        	//3、需要指定上限和下限
        	private final int maxSize = 5;
        	private final int minSize = 0;
        	//5、初始化锁对象
        	private final Object lock = new Object();
        	/**
        * put方法
        */
        	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();
        			//通知另外一个阻塞的线程方法
        		}
        	}
        	/**
        * get方法
        */
        	public Object get() {
        		Object temp;
        		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;
        	}
        	private int size() {
        		return count.get();
        	}
        	public static void main(String[] args) throws Exception {
        		final MyQueue myQueue = new MyQueue();
        		initMyQueue(myQueue);
        		Thread t1 = new Thread(() -> {
        			myQueue.put("h");
        			myQueue.put("i");
        		}, "t1");
        		Thread t2 = new Thread(() -> {
        			try {
        				Thread.sleep(2000);
        				myQueue.get();
        				Thread.sleep(2000);
        				myQueue.get();
        			}
        			catch (InterruptedException e) {
        				e.printStackTrace();
        			}
        		}, "t2");
        		t1.start();
        		Thread.sleep(1000);
        		t2.start();
        	}
        	private static void initMyQueue(MyQueue myQueue) {
        		myQueue.put("a");
        		myQueue.put("b");
        		myQueue.put("c");
        		myQueue.put("d");
        		myQueue.put("e");
        		System.out.println("当前元素个数:" + myQueue.size());
        	}
        }
      3. 执行结果:

        元素 a 被添加
        元素 b 被添加
        元素 c 被添加
        元素 d 被添加
        元素 e 被添加
        当前元素个数:5
        元素 a 被消费
        元素 h 被添加
        元素 b 被消费
        元素 i 被添加

        注意:在数据结构中,队列是可以无长度限制的,就是可以无限扩展,但是对于阻塞队列,他之所以称之为阻塞队列就是因为其有长度限制,也是上述实例中的maxSize,这也是常见的笔试面试题中比较容易忽略的一个地方,想当然的认为只要是队列他就是无长度限制的,看到这里你应该知道了Java中提供的阻塞队列的类是有长度限制的!

        1. 当调用wait方法的时候,wait方法所在的代码块停止执行,直到被notify唤醒才开始执行。所以这里的get和put中都有wait和notify方法,可以理解为相互制约和唤醒。

    4. 注意事项:

      1. wait()和notify()方法要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。

      2. wait方法是释放锁,notify方法是不释放锁的;

      3. notify每次唤醒wait等待状态的线程都是随机的,且每次只唤醒一个;

      4. notifAll每次唤醒wait等待状态的线程使之重新竞争获取对象锁,优先级最高的那个线程会最先执行;

      5. 当线程处于wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常;
         

    3.其他通信方式

    (1)进程间的通信方式:
    管道(pipe)、有名管道(named pipe)、信号量(semophore)、消息队列(message
    queue)、信号(signal)、共享内存(shared memory)、套接字(socket);
    (2)线程程间的通信方式:
    1、锁机制:
    1.1 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
    1.2 读写锁:允许多个线程同时读共享数据,而对写操作互斥。
    1.3 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。
    对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
    2、信号量机制:包括无名线程信号量与有名线程信号量
    3、信号机制:类似于进程间的信号处理。
    线程间通信的主要目的是用于线程同步,所以线程没有像进程通信中用于数据交换的
    通信机制。

    徐刘根大佬的多线程专栏:https://blog.csdn.net/column/details/17790.html

  • 相关阅读:
    关于机器学习系统的方向
    图像去雾
    中国九十年代摇滚
    matlab 将数字矩阵转换成图像
    vue-router路由守卫ie浏览器下报错问题解决
    vue:index.html、main.js、app.vue
    vue关于for循环
    element-ui表单校验
    记录vue组件引入components两次的后果
    vue组件通信
  • 原文地址:https://www.cnblogs.com/baichendongyang/p/13235523.html
Copyright © 2020-2023  润新知