zoukankan      html  css  js  c++  java
  • java多线程编程的概述以及案例详解

    引子:  java编程中有时候会要求线程安全(注:多个线程同时访问同一代码的时候,不会产生不同的结果。编写线程安全的代码需要线程同步),这时候就需要进行多线程编程。从而用到线程间通信的技术。那么在java里面,线程间通信是怎么实现的?这篇文章将通过一个案例详细分析。

    文章关键词: Object,wait,notify,notifyAll,锁,同步(synchronized).

     

    详解一个经典的生产者消费者模型,其中用到了 wait和notifyAll方法。

    源码如下:

      1   2 
      3 import java.util.LinkedList;
      4 import java.util.Queue;
      5 
      6 public class MainTest {
      7     public static void main(String[] args) {
      8         test();
      9     }
     10 
     11     private static final long waitTime = 3000;
     12 
     13     private static void test() {
     14         Queue<Integer> queue = new LinkedList<>();// 队列对象,它就是所谓的“锁”
     15         int maxsize = 2;// 队列中的最大元素个数限制
     16 
     17         // 下面4个线程,一瞬间只能有一个线程获得该对象的锁,而进入同步代码块
     18         Producer producer = new Producer(queue, maxsize, "Producer");
     19         Consumer consumer1 = new Consumer(queue, maxsize, "Consumer1");
     20         Consumer consumer2 = new Consumer(queue, maxsize, "Consumer2");
     21         Consumer consumer3 = new Consumer(queue, maxsize, "Consumer3");
     22 
     23         // 其实随便先启动哪个都无所谓,因为只有一个锁,每一次只会有一个线程能持有这个锁,来操作queue
     24         producer.start();
     25         consumer2.start();
     26         consumer1.start();
     27         consumer3.start();
     28     }
     29 
     30     /**
     31      * 生产者线程
     32      */
     33     public static class Producer extends Thread {
     34         Queue<Integer> queue;// queue,对象锁
     35         int maxsize;// 貌似是队列的最大产量
     36 
     37         Producer(Queue<Integer> queue, int maxsize, String name) {
     38             this.queue = queue;
     39             this.maxsize = maxsize;
     40             this.setName(name);
     41         }
     42 
     43         @Override
     44         public void run() {
     45             while (true) {// 无限循环,不停生产元素,直到达到上限,只要达到上限,那就wait等待。
     46                 synchronized (queue) {// 同步代码块,只有持有queue这个锁的对象才能访问这个代码块
     47                     try {
     48                         Thread.sleep(waitTime);
     49                         // sleep和wait的区别,sleep会让当前执行的线程阻塞一段时间,但是不会释放锁,
     50                         // 但是wait,会阻塞,并且会释放锁
     51                     } catch (Exception e) {
     52                     }
     53 
     54                     System.out.println(this.getName() + "获得队列的锁");// 只有你获得了queue对象的锁,你才能执行到这里
     55                     // 条件的判断一定要使用while而不是if
     56                     while (queue.size() == maxsize) {// 判断生产有没有达到上限,如果达到了上限,就让当前线程等待
     57                         System.out.println("队列已满,生产者" + this.getName() + "等待");
     58                         try {
     59                             queue.wait();// 让当前线程等待,直到其他线程调用notifyAll
     60                         } catch (Exception e) {
     61                         }
     62                     }
     63 
     64                     // 下面写的就是生产过程
     65                     int num = (int) (Math.random() * 100);
     66                     queue.offer(num);// 将一个int数字插入到队列中
     67 
     68                     System.out.println(this.getName() + "生产一个元素:" + num);
     69                     // 唤醒其他线程,在这里案例中是 "等待中"的消费者线程
     70                     queue.notifyAll();// (注:notifyAll的作用是
     71                                         // 唤醒所有持有queue对象锁的正在等待的线程)
     72 
     73                     System.out.println(this.getName() + "退出一次生产过程!");
     74                 }
     75             }
     76         }
     77     }
     78 
     79     public static class Consumer extends Thread {
     80         Queue<Integer> queue;
     81         int maxsize;
     82 
     83         Consumer(Queue<Integer> queue, int maxsize, String name) {
     84             this.queue = queue;
     85             this.maxsize = maxsize;
     86             this.setName(name);
     87         }
     88 
     89         @Override
     90         public void run() {
     91             while (true) {
     92                 synchronized (queue) {// 要想进入下面的代码,就必须先获得锁。
     93                     try {
     94                         Thread.sleep(waitTime);// sleep,让当前线程阻塞指定时长,但是并不会释放queue锁
     95                     } catch (Exception e) {
     96                     }
     97 
     98                     System.out.println(this.getName() + "获得队列的锁");// 拿到了锁,才能执行到这里
     99                     // 条件的判断一定要使用while而不是if,
    100                     while (queue.isEmpty()) {// while判断队列是否为空,如果为空,当前消费者线程就必须wait,等生产者先生产元素
    101                         // 这里,消费者有多个(因为有多个consumer线程),每一个消费者如果发现了队列空了,就会wait。
    102                         System.out.println("队列为空,消费者" + this.getName() + "等待");
    103                         try {
    104                             queue.wait();
    105                         } catch (Exception e) {
    106                         }
    107                     }
    108 
    109                     // 如果队列不是空,那么就弹出一个元素
    110                     int num = queue.poll();
    111                     System.out.println(this.getName() + "消费一个元素:" + num);
    112                     queue.notifyAll();// 然后再唤醒所有线程,唤醒不会释放自己的锁
    113 
    114                     System.out.println(this.getName() + "退出一次消费过程!");
    115                 }
    116             }
    117         }
    118     }
    119 }

    案例解析:

    1)此案例模拟的是,生产者线程 生产元素并且插入到Queue中,Queue有一个存储个数的限制。消费者线程,从Queue中拿出元素。两个线程都是无限循环执行的。

    2)在生产者线程的生产过程(随机产生一个int然后插入到queue中)执行之前,首先检查Queue的存储个数有没有到达上限,如果到达了,那就不能生产,代码中调用了queue.wait();来使生产者线程进入等待状态并且释放锁。如果没超过,那就反复执行,直到到达上限。

    3)消费者线程在执行消费过程(从queue中弹出一个元素)执行之前,首先要检查queue是不是空,如果是空,那就不能消费,调用queue.wait()让消费线程进入等待状态并且释放锁。

    4)在生产过程 或 消费过程执行完毕之后,都会有queue.notifyAll();来唤醒等待锁的所有线程。

    5)生产者中,判定queue的元素个数是不是到达上限。以及 消费者中,判定queue是不是空,这种判定queue.wait()的条件 所使用的关键字,并不是if,而是while.

    因为在执行了wait之后,该线程的执行,会暂时停留在这个while循环中,等待被唤醒,一旦被唤醒,while循环会继续执行,从而会再次判断条件是否满足。

    6)代码中能找到Thread.sleep(long);方法,它的作用,是当当前线程阻塞指定时间,但是它并不会释放锁。而wait除了阻塞之外,还会释放锁。

     

     案例执行的结果打印:

    Producer获得队列的锁
    Producer生产一个元素:86
    Producer退出一次生产过程!
    Producer获得队列的锁
    Producer生产一个元素:31
    Producer退出一次生产过程!
    Producer获得队列的锁
    队列已满,生产者Producer等待
    Consumer2获得队列的锁
    Consumer2消费一个元素:86
    Consumer2退出一次消费过程!
    Consumer3获得队列的锁
    Consumer3消费一个元素:31
    Consumer3退出一次消费过程!
    Consumer1获得队列的锁
    队列为空,消费者Consumer1等待
    Consumer3获得队列的锁
    队列为空,消费者Consumer3等待
    Consumer2获得队列的锁
    队列为空,消费者Consumer2等待
    Producer生产一个元素:29
    Producer退出一次生产过程!
    Producer获得队列的锁
    Producer生产一个元素:82
    Producer退出一次生产过程!
    Producer获得队列的锁
    队列已满,生产者Producer等待
    Consumer2消费一个元素:29
    Consumer2退出一次消费过程!
    

    结果分析(请对照日志来看,大神请绕道,下面的描述比较啰嗦):

    由于首先启动的是生产者线程(Producer),所以producer先获得了锁,进行了两次生产。再次尝试生产的时候发现queue满了,于是,生产者进入等待。

    之后,consumer2的得到了锁,于是进行消费,消费执行了一次,锁被consumer3夺走,consumer3执行了一次消费。

    之后,consumer1得到了锁,就当它准备开始消费的时候,发现queue空了,不能消费了,于是代码调用queue.wait().来让consumer1进入等待。

    之后,consumer3和consumer2相继得到锁,但是他们都发现,queue空了,也不能消费,于是同样调用queue.wait()来让consumer3和consumer1进入等待。

    再然后,生产者得到了锁(这里可能很奇怪,生产者不是在等待么?它什么时候被唤醒的,查看Consumer的代码,能发现,在每一次成功消费之后,都会有queue.notifyAll(),也就是说,在之前cunsumer2消费之后,生产者就已经被唤醒了,只是他没有得到锁,所以就没有执行生产过程)。

    生产者得到锁之后,继续while循环,发现queue并没有填满,于是进入生产过程。之后···就是无限循环了。

     

    这种模型在线程安全比较高的场景中,会被经常用到,比如买票系统,同一张票不能被卖两次。所以,这张票,在同一时间只能被一个线程访问。

    -------------------

     

     

    案例解析完毕,但是针对java多线程,也许有人会有其他疑问,下面列举几个比较重要的问题加以说明:

    问:在java中,wait,notify以及notifyAll是用来做线程之间的通信的,但是为什么这3个方法不是在Thread类里面,而是在Object类里面?
    答:  

    这3个方法虽然是用于线程间的通信,但是他们并不是直接就在Thread类里面,而是在Object类。

    这是 因为 调用一个Object的wait,notify,notifyAll 必须保证该段代码对于该Object是同步的, 否则就可能会报异常IllegalMonitorStateException(具体可以进入Object类的源码搜索此异常,注释中有详细说明),通常的写法如下,

    synchronized(obj){//在执行wait,notify,notifyAll时,必须保证这段代码持有obj对象的锁。
      obj.wait();
      ...
      obj.notify();
      ...
      obj.notifyAll();
    }

    如果多个线程都写了上面的代码,那么同一时间,只会有一个线程能获取obj对象锁。

    所以说,这3个方法在Object类里,而不是在Thread类里,其实是java框架的设定,通过Object锁来完成线程间的通信。

     

    问:wait,notify,notifyAll的作用分别是什么?

    答:

    wait-让当前线程进入等待状态,并且释放锁;

    notify -唤醒任意一个正在等待锁的线程,并且让它得到锁。

    notifyAll,唤醒所有等待对象锁的线程,如果有多个线程都被唤醒,那么锁将会被他们争夺,同一时间只会有一个线程得到锁。

    问:notify,notifyAll有啥区别?

    答:

    notify,让任意一个等待对象锁的线程得到锁,并且唤醒他。

    notifyAll,唤醒所有等到对象锁的线程,如果有多个被唤醒的线程,锁将会被争夺,争夺到锁的线程就可以执行.

     

    ===================就写到这里了。上面的是基础知识,在复杂场景中可能会被复杂化千万倍,但是万变不离其宗,了解了原理,就能应对大部分场景了。

     

  • 相关阅读:
    php关联Apache和nginx
    /etc/profile
    snmp配置
    snmpwalk
    redis主从
    redis安装
    计算真实可用内存
    分类样本不均
    pandas apply 添加进度条
    linux 单次定时任务
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/8693278.html
Copyright © 2011-2022 走看看