• JAVA基础知识之多线程——线程通信


    传统的线程通信

    Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。

    • wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
    • notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
    • notifyAll():与上面notify的区别是同时唤醒多个等待线程。

    值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,

    • 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
    • 对于同步代码块,synchronized(obj) { ... },则需要用空号钟的obj来调用。

    生产者-消费者问题模型

    在经典的生产者-消费者问题中,需要使用线程通信来解决。

    假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,

    存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。

    下面是用同步方法结合线程通信的方式来实现的思路,

    • 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
    • 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
    • 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
    • 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
    • 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题

    下面给出实现代码

    在Account类中定义两个同步方法,draw和deposit

     1 package threads.sync;
     2 
     3 public class Account {
     4     private String accountNo;
     5     private double balance;
     6     //if flag = false, means only deposit can be done
     7     private boolean flag = false;
     8     public Account() {}
     9     
    10     
    11     public Account(String accountNo, double balance) {
    12         this.accountNo = accountNo;
    13         this.balance = balance;
    14     }
    15 
    16     public String getAccountNo() {
    17         return accountNo;
    18     }
    19 
    20     public void setAccountNo(String accountNo) {
    21         this.accountNo = accountNo;
    22     }
    23 
    24     public double getBalance() {
    25         return balance;
    26     }
    27 
    28     public void setBalance(double balance) {
    29         this.balance = balance;
    30     }
    31     
    32     
    33     public int hashCode() {
    34         return accountNo.hashCode();
    35     }
    36     
    37     public boolean equals(Object obj) {
    38         if (this == obj) return true;
    39         if (obj != null && obj.getClass() == Account.class) {
    40             Account target = (Account)obj;
    41             return target.getAccountNo().equals(accountNo);
    42         }
    43         return false;
    44     }
    45     
    46     public synchronized void draw(double drawAmount) {
    47         try {
    48             //if flag = false, means only deposit can be done, draw method will be blocked
    49             if (!flag) {
    50                 wait();
    51             } else {
    52                 System.out.println(Thread.currentThread().getName()
    53                         + "        draw money: " + drawAmount);
    54                 balance -= drawAmount;
    55                 System.out.println("            "
    56                         + " balance : " + balance);
    57                 flag = false;
    58                 notifyAll();
    59             } 
    60         } catch (InterruptedException ex) {
    61             ex.printStackTrace();
    62         }
    63     }
    64     
    65     public synchronized void deposit(double depositAmount) {
    66         try {
    67             //if flag = false, means only draw can be done, deposit method will be blocked
    68             if (flag) {
    69                 wait();
    70             } else {
    71                 System.out.println(Thread.currentThread().getName()
    72                         + "        deposit money: " + depositAmount);
    73                 balance += depositAmount;
    74                 System.out.println("            "
    75                         + " balance : " + balance);
    76                 flag = true;
    77                 notifyAll();
    78             } 
    79         } catch (InterruptedException ex) {
    80             ex.printStackTrace();
    81         }    
    82     }
    83     
    84 }

    定义一个存款线程类depositThread

     1 package threads.sync;
     2 
     3 public class DepositThread extends Thread {
     4     private Account account;
     5     private double depositAmount;
     6     public DepositThread(String name, Account account, double depositAmount) {
     7         super(name);
     8         this.setAccount(account);
     9         this.setDepositAmount(depositAmount);
    10     }
    11     public Account getAccount() {
    12         return account;
    13     }
    14     public void setAccount(Account account) {
    15         this.account = account;
    16     }
    17     
    18     public double getDepositAmount() {
    19         return depositAmount;
    20     }
    21     public void setDepositAmount(double depositAmount) {
    22         this.depositAmount = depositAmount;
    23     }
    24     
    25     public void run() {
    26         for(int i=0 ; i<10; i++) {
    27             account.deposit(depositAmount);
    28         }
    29 
    30     }
    31 
    32 }

    定义一个取款线程类depositThread

     1 package threads.sync;
     2 
     3 public class DrawThread extends Thread {
     4     private Account account;
     5     private double drawAmount;
     6     public DrawThread(String name, Account account, double drawAmount) {
     7         super(name);
     8         this.setAccount(account);
     9         this.setDrawAmount(drawAmount);
    10     }
    11     public Account getAccount() {
    12         return account;
    13     }
    14     public void setAccount(Account account) {
    15         this.account = account;
    16     }
    17     public double getDrawAmount() {
    18         return drawAmount;
    19     }
    20     public void setDrawAmount(double drawAmount) {
    21         this.drawAmount = drawAmount;
    22     }
    23     
    24     public void run() {
    25         for(int i=0 ; i<10; i++) {
    26             account.draw(drawAmount);
    27         }
    28     }
    29 }

    下面是测试类,存款线程中会有10次存款,三个取款线程中总共会有30次取款,

     1 package threads.sync;
     2 
     3 public class DrawTest {
     4     public static void main(String[] args) {
     5         Account acc = new Account("123456",1000);
     6         new DrawThread("DrawThread", acc, 800).start();
     7         new DepositThread("DepositThread-A",acc,800).start();
     8         new DepositThread("DepositThread-B",acc,800).start();
     9         new DepositThread("DepositThread-C",acc,800).start();
    10     }
    11 }

    执行结果,

     1 DepositThread-A        deposit money: 800.0
     2                 balance : 1800.0
     3 DrawThread        draw money: 800.0
     4                 balance : 1000.0
     5 DepositThread-B        deposit money: 800.0
     6                 balance : 1800.0
     7 DrawThread        draw money: 800.0
     8                 balance : 1000.0
     9 DepositThread-C        deposit money: 800.0
    10                 balance : 1800.0
    11 DrawThread        draw money: 800.0
    12                 balance : 1000.0
    13 DepositThread-C        deposit money: 800.0
    14                 balance : 1800.0
    15 DrawThread        draw money: 800.0
    16                 balance : 1000.0
    17 DepositThread-C        deposit money: 800.0
    18                 balance : 1800.0
    19 DrawThread        draw money: 800.0
    20                 balance : 1000.0
    21 DepositThread-C        deposit money: 800.0
    22                 balance : 1800.0
    23 DrawThread        draw money: 800.0
    24                 balance : 1000.0
    25 DepositThread-C        deposit money: 800.0
    26                 balance : 1800.0
    27 DrawThread        draw money: 800.0
    28                 balance : 1000.0
    29 DepositThread-A        deposit money: 800.0
    30                 balance : 1800.0

    从执行结果中可以看到,三个取款线程ABC执行顺序随机,但是总是在存款完成后,才会进行取款操作,而且无论存款还是取款,都不会同时进行两次。

    使用condition控制线程通信

    如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。

    在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。

    与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,

    await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。

    signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。

    signalAll:与上面类似,但是是唤醒所有线程。

    下面用condition的方式来实现前面的银行取钱的例子,只需要修改Account类,改用lock同步线程,condition线程通信,

      1 package threads.sync;
      2 
      3 import java.util.concurrent.locks.Condition;
      4 import java.util.concurrent.locks.Lock;
      5 import java.util.concurrent.locks.ReentrantLock;
      6 
      7 public class Account {
      8     private String accountNo;
      9     private double balance;
     10     private boolean flag = false;
     11     //显示定义lock对象
     12     private final Lock lock = new ReentrantLock();
     13     //获取lock对象对应的condition
     14     private final Condition cond = lock.newCondition();
     15     public Account() {}
     16     
     17     
     18     public Account(String accountNo, double balance) {
     19         this.accountNo = accountNo;
     20         this.balance = balance;
     21     }
     22 
     23     public String getAccountNo() {
     24         return accountNo;
     25     }
     26 
     27     public void setAccountNo(String accountNo) {
     28         this.accountNo = accountNo;
     29     }
     30 
     31     public double getBalance() {
     32         return balance;
     33     }
     34 
     35     public void setBalance(double balance) {
     36         this.balance = balance;
     37     }
     38     
     39     
     40     public int hashCode() {
     41         return accountNo.hashCode();
     42     }
     43     
     44     public boolean equals(Object obj) {
     45         if (this == obj) return true;
     46         if (obj != null && obj.getClass() == Account.class) {
     47             Account target = (Account)obj;
     48             return target.getAccountNo().equals(accountNo);
     49         }
     50         return false;
     51     }
     52     
     53     public void draw(double drawAmount) {
     54         lock.lock();
     55         try {
     56             //if flag = false, means only deposit can be done, draw method will be blocked
     57             if (!flag) {
     58                 //this.wait();
     59                 cond.await();
     60             } else {
     61                 System.out.println(Thread.currentThread().getName()
     62                         + "        draw money: " + drawAmount);
     63                 balance -= drawAmount;
     64                 System.out.println("            "
     65                         + "    balance : " + balance);
     66                 flag = false;
     67                 //this.notifyAll();
     68                 cond.signalAll();
     69             } 
     70         } catch (InterruptedException ex) {
     71             ex.printStackTrace();
     72         } finally {
     73             lock.unlock();
     74         }
     75     }
     76     
     77     public void deposit(double depositAmount) {
     78         lock.lock();
     79         try {
     80             //if flag = false, means only draw can be done, deposit method will be blocked
     81             if (flag) {
     82                 //this.wait();
     83                 cond.await();
     84             } else {
     85                 System.out.println(Thread.currentThread().getName()
     86                         + "        deposit money: " + depositAmount);
     87                 balance += depositAmount;
     88                 System.out.println("            "
     89                         + "    balance : " + balance);
     90                 flag = true;
     91                 //this.notifyAll();
     92                 cond.signalAll();
     93             } 
     94         } catch (InterruptedException ex) {
     95             ex.printStackTrace();
     96         } finally {
     97             lock.unlock();
     98         }
     99     }
    100     
    101 }

    对比用synchronized方式同步线程的例子,前面例子中是隐式的同步监视器(this)调用wait和notify来通信,

    而本例是显示同步监视器(lock)的关联对象(condition)调用await和signal来通信,执行结果与前面一样不再给出。

    使用阻塞队列(BlockingQueue)控制线程通信

    BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。

    它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,

    BlockingQueue的特征是,

    当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,

    当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞

    对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,

    但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。

    集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。

    BlockingQueue接口有很多实现类,下面演示最常用的实现类ArrayBlockQueue控制线程通信,

    定义一个生产者线程类Producter

     1 package threads.sync;
     2 
     3 import java.util.concurrent.BlockingQueue;
     4 
     5 public class Producter extends Thread {
     6     private BlockingQueue<String> bq;
     7     
     8     public Producter(BlockingQueue<String> bq) {
     9         this.bq = bq;
    10     }
    11     
    12     public void run() {
    13         String[] strArr = new String[] {
    14                 "Java",
    15                 "Struts",
    16                 "Spring"
    17         };
    18         
    19         for(int i = 0; i<999999; i++) {
    20             System.out.println(getName()+" 生产者准备生产集合元素");
    21             try {
    22                 Thread.sleep(200);
    23                 //如果队列已满,线程将阻塞
    24                 bq.put(strArr[i % 3]);
    25             } catch (InterruptedException e) {
    26                 e.printStackTrace();
    27             }
    28             System.out.println(getName()+" 生产完成: " + bq);
    29         }
    30     }
    31 }

    定义一个消费者类Consumer

     1 package threads.sync;
     2 
     3 import java.util.concurrent.BlockingQueue;
     4 
     5 public class Consumer extends Thread {
     6     private BlockingQueue<String> bq;
     7     
     8     public Consumer(BlockingQueue<String> bq) {
     9         this.bq = bq;
    10     }
    11     
    12     public void run() {
    13         
    14         while (true) {
    15             System.out.println(getName()+" 消费者准备消费集合元素");
    16             try {
    17                 Thread.sleep(200);
    18                 //如果队列已空,线程将阻塞
    19                 bq.take();
    20             } catch (InterruptedException e) {
    21                 e.printStackTrace();
    22             }
    23             System.out.println(getName()+" 消费完成: " + bq);
    24         }
    25     }
    26 }

    在测试类中,定义一个容量为2的阻塞集合,

    启动三个生产者线程, 每个线程都在不停生产商品,存入阻塞队列中,

    启动一个消费者线程,每个线程也在不停从阻塞队列中取出商品,

     1 package threads.sync;
     2 
     3 import java.util.concurrent.ArrayBlockingQueue;
     4 import java.util.concurrent.BlockingQueue;
     5 
     6 public class BlockingQueueTest {
     7     public static void main(String[] args) {
     8         BlockingQueue<String> bq = new ArrayBlockingQueue<String>(2);
     9         new Producter(bq).start();
    10         new Producter(bq).start();
    11         new Producter(bq).start();
    12         new Consumer(bq).start();
    13     }
    14 }

    执行结果,从执行结果中可以看到,只要集合中有元素且集合没有满,那么生产者和消费者线程都有机会得到执行,具体谁有机会要看谁抢到CPU执行片,

    但是当集合空了的时候,例如第7行(Thread-8 消费完成: []),接着又有一个消费者线程执行,但是因此集合为空而阻塞了,此时只有生产者线程能执行,

    当集合满了的时候,例如第11行(Thread-7 生产完成: [Java, Java]),接着又有一个生产者线程执行,但是因为集合已满而阻塞了,此时只有消费者线程能执行。

     1 Thread-5 生产者准备生产集合元素
     2 Thread-6 生产者准备生产集合元素
     3 Thread-7 生产者准备生产集合元素
     4 Thread-8 消费者准备消费集合元素
     5 Thread-5 生产完成: [Java]
     6 Thread-5 生产者准备生产集合元素
     7 Thread-8 消费完成: []
     8 Thread-8 消费者准备消费集合元素
     9 Thread-6 生产完成: [Java]
    10 Thread-6 生产者准备生产集合元素
    11 Thread-7 生产完成: [Java, Java]
    12 Thread-7 生产者准备生产集合元素
    13 Thread-8 消费完成: [Java]
    14 Thread-8 消费者准备消费集合元素
    15 Thread-5 生产完成: [Java, Struts]
    16 Thread-5 生产者准备生产集合元素
    17 Thread-8 消费完成: [Struts]
    18 Thread-8 消费者准备消费集合元素
    19 Thread-6 生产完成: [Struts, Struts]
    20 Thread-6 生产者准备生产集合元素
    21 Thread-8 消费完成: [Struts]
    22 Thread-8 消费者准备消费集合元素
    23 Thread-7 生产完成: [Struts, Struts]
    24 Thread-7 生产者准备生产集合元素
    25 Thread-8 消费完成: [Struts]
    26 Thread-8 消费者准备消费集合元素
    27 Thread-5 生产完成: [Struts, Spring]
    28 Thread-5 生产者准备生产集合元素
    29 Thread-8 消费完成: [Spring]
    30 Thread-8 消费者准备消费集合元素
    31 Thread-6 生产完成: [Spring, Spring]
    32 Thread-6 生产者准备生产集合元素
    33 Thread-8 消费完成: [Spring]
    34 Thread-8 消费者准备消费集合元素

    上面的例子来自李刚的疯狂JAVA, 但个人认为并不是太好,因为无论在生产者还是消费者线程中,打印bq操作前后的两段日志并不是原子操作,这会导致打印的日志不准确,

    例如下面的运行结果,从第5行看到(Thread-6 生产完成: []),刚刚执行完一个生产者线程中的入队操作,但是打印队列却是空的,原因就在于在打印两行日志期间,消费者线做了取元素的操作。

     1 Thread-6 生产者准备生产集合元素
     2 Thread-5 生产者准备生产集合元素
     3 Thread-7 生产者准备生产集合元素
     4 Thread-8 消费者准备消费集合元素
     5 Thread-6 生产完成: []
     6 Thread-6 生产者准备生产集合元素
     7 Thread-5 生产完成: [Java]
     8 Thread-5 生产者准备生产集合元素
     9 Thread-7 生产完成: [Java, Java]
    10 Thread-7 生产者准备生产集合元素
    11 Thread-8 消费完成: []
    12 Thread-8 消费者准备消费集合元素
    13 Thread-7 生产完成: [Java, Struts]
    14 Thread-7 生产者准备生产集合元素
    15 Thread-8 消费完成: [Java, Struts]
    16 Thread-8 消费者准备消费集合元素
    17 Thread-8 消费完成: [Struts]
    18 Thread-5 生产完成: [Struts, Struts]
    19 Thread-5 生产者准备生产集合元素
    20 Thread-8 消费者准备消费集合元素
    21 Thread-8 消费完成: [Struts]
    22 Thread-8 消费者准备消费集合元素
    23 Thread-6 生产完成: [Struts, Struts]
    24 Thread-6 生产者准备生产集合元素
    25 Thread-8 消费完成: [Struts]
    26 Thread-8 消费者准备消费集合元素
    27 Thread-6 生产完成: [Struts, Spring]
    28 Thread-6 生产者准备生产集合元素
    29 Thread-8 消费完成: [Spring]
    30 Thread-8 消费者准备消费集合元素
    31 Thread-5 生产完成: [Spring, Spring]
    32 Thread-5 生产者准备生产集合元素
    33 Thread-8 消费完成: [Spring]
    34 Thread-8 消费者准备消费集合元素
    35 Thread-5 生产完成: [Spring, Java]
    36 Thread-5 生产者准备生产集合元素
    37 Thread-8 消费完成: [Java]
    38 Thread-8 消费者准备消费集合元素
    39 Thread-6 生产完成: [Java, Java]
    40 Thread-6 生产者准备生产集合元素
    41 Thread-8 消费完成: [Java]
    42 Thread-8 消费者准备消费集合元素
    43 Thread-7 生产完成: [Java, Spring]
    44 Thread-7 生产者准备生产集合元素
    45 Thread-8 消费完成: [Spring]
    46 Thread-8 消费者准备消费集合元素
    47 Thread-7 生产完成: [Spring, Java]
    48 Thread-7 生产者准备生产集合元素
    49 Thread-8 消费完成: [Java]
    50 Thread-8 消费者准备消费集合元素
    51 Thread-7 生产完成: [Java, Struts]
    52 Thread-7 生产者准备生产集合元素
    53 Thread-8 消费完成: [Struts]
    54 Thread-8 消费者准备消费集合元素
    55 Thread-7 生产完成: [Struts, Spring]
    56 Thread-7 生产者准备生产集合元素
    57 Thread-8 消费完成: [Spring]
    58 Thread-8 消费者准备消费集合元素
    59 Thread-7 生产完成: [Spring, Java]
    60 Thread-7 生产者准备生产集合元素
    61 Thread-8 消费完成: [Java]
    62 Thread-8 消费者准备消费集合元素
    63 Thread-7 生产完成: [Java, Struts]
    64 Thread-7 生产者准备生产集合元素
    65 Thread-7 生产完成: [Struts, Spring]
    66 Thread-7 生产者准备生产集合元素
    67 Thread-8 消费完成: [Struts, Spring]
    68 Thread-8 消费者准备消费集合元素
    69 Thread-8 消费完成: [Spring]
    70 Thread-8 消费者准备消费集合元素
    71 Thread-5 生产完成: [Spring, Struts]
    72 Thread-5 生产者准备生产集合元素
    73 Thread-8 消费完成: [Struts]
    74 Thread-8 消费者准备消费集合元素
    75 Thread-6 生产完成: [Struts, Struts]
    76 Thread-6 生产者准备生产集合元素
    77 Thread-8 消费完成: [Struts, Spring]
    78 Thread-8 消费者准备消费集合元素
    79 Thread-6 生产完成: [Struts, Spring]
    80 Thread-6 生产者准备生产集合元素
  • 相关阅读:
    加密CMD使电脑溢出也拿不到CMD权限
    全面提升Linux服务器的安全
    ping 源码,详细解释
    伤心一百回
    聊聊我对黑客技术的思考
    一个网管员的真实成长经历
    通过命令限制上网用户的权限
    防范黑客的简单办法
    “黑客”人生
    黑客现身讲话
  • 原文地址:https://www.cnblogs.com/fysola/p/6072883.html
Copyright © 2020-2023  润新知