【java多线程编程核心技术】3.线程间通信 -笔记总结

来源:互联网 发布:淘宝账期延长十五天 编辑:程序博客网 时间:2024/06/07 14:00

等待/通知机制

线程与线程之间不是独立的个体,他们彼此之间可以互相通信和协作。

不使用等待/通知机制实现线程通信

可以通过不停地while语句轮询机制来检测某一个条件,但这样特别耗费CPU资源。(轮询间隔时间小,更浪费CPU资源,如果间隔时间大,有可能获取不到想要的数据)

什么是等待/通知机制

餐厅里厨师与服务员之间的交互模式就属于等待/通知机制。
服务员去到菜的时间取决于厨师,所以服务员就有“等待(wait)”状态;
厨师做好菜放在“菜品传递台”上,其实就相当于一种“通知(notify)”;

等待/通知机制的实现

wait 使线程停止运行,notify 使停止的线程继续运行。

关键字 synchronized 可以将任何一个 Object 对象作为同步对象看待,而 Java 为每个 Object 都实现了 wait() 和 notify() 方法,他们必须用在被 synchronized 同步的 Object 的临界区内。

通过调用 wait 方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁。而 notify 操作可以唤醒一个因调用了 wait 方法而处于阻塞状态的线程,使其进入就绪状态。

被重新唤醒的线程会试图重新获得临界区的控制权,继续执行临界区内 wait 之后的代码。

wait 方法可以使调用该方法的线程释放共享资源的锁,从运行状态退出,进入等待状态,直到再次被唤醒。
notify() 方法可以随机唤醒等待对列中等待同一共享资源的一个线程,并使该线程退出等待状态,进入可运行状态。
notifyAll() 方法可以随机唤醒等待对列中等待同一共享资源的所有线程,并使这些线程退出等待状态,进入可运行状态。

敲黑板,划重点:wait()立即释放锁,notify等线程执行完后再释放。

Java为每一个Object都实现了wait()和notify()方法,但它们必须用在被synchronized同步的Object的临界区,否则会抛出异常(IllegalMonitorStateException
这里写图片描述
新创建一个线程对象后,在调用它的 start() 方法,系统会为此线程分配 CPU 资源,使其处于 Runnable(可运行)状态,如果线程抢占到 CPU 资源,此线程就会处于 Running (运行)状态

Runnable 和 Running 状态之间可以相互切换,因为线程有可能运行一段时间后,有其他优先级高的线程抢占了 CPU 资源,此时线程就从 Running 状态变成了 Runnable 状态。

线程进入 Runnable 状态大致有如下五种情况:
* 调用 sleep() 方法后经过的时间超过了指定的休眠时间
* 线程调用的阻塞 IO 已经返回,阻塞方法执行完毕
* 线程成功的获得了试图同步的监视器
* 线程正在等待某个通知,其他线程发出了通知
* 处于挂状态的线程调用了 resume 恢复方法

Blocked 是阻塞的意思,例如线程遇到一个 IO 操作,此时 CPU 处于空闲状态,可能会转而把 CPU 时间片分配给其他线程,这时也可以称为 “暂停”状态。Blocked 状态结束之后,进入 Runnable 状态,等待系统重新分配资源

出现阻塞状态的有如下五种情况:
* 线程调用 sleep 方法,主动放弃占用的处理器资源
* 线程调用了阻塞式 IO 方法,在该方法返回之前,该线程被阻塞
* 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有
* 线程等待某个通知
* 程序调用了 suspend 方法将该线程挂起

run 方法运行结束后进入销毁阶段,整个线程执行完毕。

每个锁对象都是两个队列,一个是就绪队列,一个是阻塞队列
就绪队列:将要获得锁的线程,一个线程被唤醒后,才会进入就绪队列,等待CPU的调度
阻塞队列:被阻塞的线程,例如被wait后,就会进入阻塞队列

方法wait()锁释放与notify()锁不释放

当方法wait()被执行后,锁被自动释放,但执行完notify()方法,锁却不自动释放。

当interrupt方法遇到wait方法

当线程呈wait()状态时,调用线程对象的interrupt()方法会抛出如下异常

begin wait()java.lang.InterruptedException出现异常了,因为呈wait状态的线程被interrupt了!     at java.lang.Object.wait(Native Method)     at java.lang.Object.wait(Unknown Source)     at service.Service.testMethod(Service.java:9)     at extthread.ThreadA.run(ThreadA.java:17)

通知过早

如果通知过早,则会打乱程序正常的运行逻辑,例如在先notify()后,再wait(),会导致线程一直处于wait状态,不会被通知。

等待wait的条件发生变化

public class Add {     private String lock;     public Add(String lock) {          super();          this.lock = lock;     }     public void add() {          synchronized (lock) {              ValueObject.list.add("anyString");              lock.notifyAll();          }     }}public class Subtract {     private String lock;     public Subtract(String lock) {          super();          this.lock = lock;     }     public void subtract() {          try {              synchronized (lock) {                   if (ValueObject.list.size() == 0) {//                 while (ValueObject.list.size() == 0) {                        System.out.println("wait begin ThreadName="                                  + Thread.currentThread().getName());                        lock.wait();                        System.out.println("wait   end ThreadName="                                  + Thread.currentThread().getName());                   }                   ValueObject.list.remove(0);                   System.out.println("list size=" + ValueObject.list.size());              }          } catch (InterruptedException e) {              e.printStackTrace();          }     }}public class ValueObject {     public static List list = new ArrayList();}public class ThreadAdd extends Thread {     private Add p;     public ThreadAdd(Add p) {          super();          this.p = p;     }     @Override     public void run() {          p.add();     }}public class ThreadSubtract extends Thread {     private Subtract r;     public ThreadSubtract(Subtract r) {          super();          this.r = r;     }     @Override     public void run() {          r.subtract();     }}public class Run {     public static void main(String[] args) throws InterruptedException {          String lock = new String("");          Add add = new Add(lock);          Subtract subtract = new Subtract(lock);          ThreadSubtract subtract1Thread = new ThreadSubtract(subtract);          subtract1Thread.setName("subtract1Thread");          subtract1Thread.start();          ThreadSubtract subtract2Thread = new ThreadSubtract(subtract);          subtract2Thread.setName("subtract2Thread");          subtract2Thread.start();          Thread.sleep(1000);          ThreadAdd addThread = new ThreadAdd(add);          addThread.setName("addThread");          addThread.start();     }}在if (ValueObject.list.size() == 0)情况下,输出结果:wait begin ThreadName=subtract1Threadwait begin ThreadName=subtract2Threadwait   end ThreadName=subtract2Threadlist size=0wait   end ThreadName=subtract1ThreadException in thread "subtract1Thread" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0     at java.util.ArrayList.rangeCheck(Unknown Source)     at java.util.ArrayList.remove(Unknown Source)     at entity.Subtract.subtract(Subtract.java:24)     at extthread.ThreadSubtract.run(ThreadSubtract.java:16)更换为while(ValueObject.list.size() == 0)情况下,输出结果:wait begin ThreadName=subtract2Threadwait begin ThreadName=subtract1Threadwait   end ThreadName=subtract1Threadlist size=0wait   end ThreadName=subtract2Threadwait begin ThreadName=subtract2Thread

出现异常的原因:实现了两次删除remove()操作,第一次能正常删除,第二次则报异常
用while(…)替换掉if(…),当线程被通知(notify)后,会再进一次进行判断长度是非为0。

生产者/消费者模式实现

一生产与一消费:操作值

生产者:

package entity;//生产者public class P {     private String lock;     public P(String lock) {          super();          this.lock = lock;     }     public void setValue() {          try {              synchronized (lock) {                   if (!ValueObject.value.equals("")) {                        lock.wait();                   }                   String value = System.currentTimeMillis() + "_"                             + System.nanoTime();                   System.out.println("set的值是" + value);                   ValueObject.value = value;                   lock.notify();              }          } catch (InterruptedException e) {              e.printStackTrace();          }     }}

消费者:

package entity;//消费者public class C {     private String lock;     public C(String lock) {          super();          this.lock = lock;     }     public void getValue() {          try {              synchronized (lock) {                   if (ValueObject.value.equals("")) {                        lock.wait();                   }                   System.out.println("get的值是" + ValueObject.value);                   ValueObject.value = "";                   lock.notify();              }          } catch (InterruptedException e) {              e.printStackTrace();          }     }}

存储值的对象:

package entity;public class ValueObject {     public static String value = "";

生产者线程:

package extthread;import entity.P;public class ThreadP extends Thread {     private P p;     public ThreadP(P p) {          super();          this.p = p;     }     @Override     public void run() {          while (true) {              p.setValue();          }     }}

消费者线程:

package extthread;import entity.C;public class ThreadC extends Thread {     private C r;     public ThreadC(C r) {          super();          this.r = r;     }     @Override     public void run() {          while (true) {              r.getValue();          }     }}

运行类:

package test;import entity.P;import entity.C;import extthread.ThreadP;import extthread.ThreadC;public class Run {    public static void main(String[] args) {        String lock = new String("");        P p = new P(lock);        C r = new C(lock);        ThreadP pThread = new ThreadP(p);        ThreadC rThread = new ThreadC(r);        pThread.start();        rThread.start();    }}输出结果:set的值是1511090029379_209828520790577get的值是1511090029379_209828520790577set的值是1511090029379_209828520806165get的值是1511090029379_209828520806165set的值是1511090029379_209828520837751get的值是1511090029379_209828520837751set的值是1511090029379_209828520848006get的值是1511090029379_209828520848006set的值是1511090029379_209828520858262get的值是1511090029379_209828520858262set的值是1511090029379_209828520868517get的值是1511090029379_209828520868517set的值是1511090029379_209828520885336get的值是1511090029379_209828520885336

多生产与多消费:操作值-假死
在未将notify()更换为notifyAll()之前,容易造成假死现象。
假死”现象其实就是线程进入waiting等待状态。如果全部的线程都进入waiting状态,则呈现就不再执行任何业务功能了,整个项目呈停止状态。

假死出现的主要原因是有可能连续唤醒同类,导致所有线程呈waiting状态。

在将notify更换为notifyAll()以后,解除假死现象。
需要注意的是,在多生产与多消费中,一定要将if (ValueObject.value.equals(“”)) 更换为while(….),理由同上一节。

通过管道进行线程间通信:字节流/字符流

管道流是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
通过使用管道,实现不同线程间的通信,而无需借助类似于临时文件类的东西
1.PipedInputStream和PipedOutputStream
2.PipedReader和PipedWriter

public class ThreadRead extends Thread {     private ReadData read;     private PipedInputStream input;     public ThreadRead(ReadData read, PipedInputStream input) {          super();          this.read = read;          this.input = input;     }     @Override     public void run() {          read.readMethod(input);     }}package extthread;import java.io.PipedOutputStream;import service.WriteData;public class ThreadWrite extends Thread {    private WriteData write;    private PipedOutputStream out;    public ThreadWrite(WriteData write, PipedOutputStream out) {        super();        this.write = write;        this.out = out;    }    @Override    public void run() {        write.writeMethod(out);    }}package service;import java.io.IOException;import java.io.PipedInputStream;public class ReadData {    public void readMethod(PipedInputStream input) {        try {            System.out.println("read  :");            byte[] byteArray = new byte[20];            int readLength = input.read(byteArray);            while (readLength != -1) {                String newData = new String(byteArray, 0, readLength);                System.out.print(newData);                readLength = input.read(byteArray);            }            System.out.println();            input.close();        } catch (IOException e) {            e.printStackTrace();        }    }}package service;import java.io.IOException;import java.io.PipedOutputStream;public class WriteData {    public void writeMethod(PipedOutputStream out) {        try {            System.out.println("write :");            for (int i = 0; i < 300; i++) {                String outData = "" + (i + 1);                out.write(outData.getBytes());                System.out.print(outData);            }            System.out.println();            out.close();        } catch (IOException e) {            e.printStackTrace();        }    }}package test;import java.io.IOException;import java.io.PipedInputStream;import java.io.PipedOutputStream;import service.ReadData;import service.WriteData;import extthread.ThreadRead;import extthread.ThreadWrite;public class Run {    public static void main(String[] args) {        try {            WriteData writeData = new WriteData();            ReadData readData = new ReadData();            PipedInputStream inputStream = new PipedInputStream();            PipedOutputStream outputStream = new PipedOutputStream();            // inputStream.connect(outputStream);            outputStream.connect(inputStream);            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);            threadWrite.start();            Thread.sleep(2000);            ThreadRead threadRead = new ThreadRead(readData, inputStream);            threadRead.start();        } catch (IOException e) {            e.printStackTrace();        } catch (InterruptedException e) {            e.printStackTrace();        }    }}输出结果:write :1234567891011121314151 ......read  :1234567891011121314151........

通过 // inputStream.connect(outputStream);// outputStream.connect(inputStream); 使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。

通过管道进行线程间通信:字符流

    public void readMethod(PipedReader input) {          try {              System.out.println("read  :");              char[] byteArray = new char[20];              int readLength = input.read(byteArray);              while (readLength != -1) {                   String newData = new String(byteArray, 0, readLength);                   System.out.print(newData);                   readLength = input.read(byteArray);              }              System.out.println();              input.close();          } catch (IOException e) {              e.printStackTrace();          }     }.........大部分与字节流类似

实战:等待/通知之交叉备份

(最近也在琢磨做一个真正能派上用场的多线程备份数据库的小项目,后期完成了会在这里贴一个后续链接,先占个位吧~ 哈哈哈哈哈, 2017年11月19日留)
目标:创建20个线程,其中10个线程是将数据备份到数据库A,另外10个线程将数据备份到数据库B中去,并且备份数据库A和备份数据库B是交叉进行的。

DBTools:

package service;public class DBTools {     volatile private boolean prevIsA = false;     synchronized public void backupA() {          try {              while (prevIsA == true) {                   wait();              }              for (int i = 0; i < 5; i++) {                   System.out.println("★★★★★");              }              prevIsA = true;              notifyAll();          } catch (InterruptedException e) {              e.printStackTrace();          }     }     synchronized public void backupB() {          try {              while (prevIsA == false) {                   wait();              }              for (int i = 0; i < 5; i++) {                   System.out.println("☆☆☆☆☆");              }              prevIsA = false;              notifyAll();          } catch (InterruptedException e) {              e.printStackTrace();          }     }

BackupA/BackupB:

package extthread;import service.DBTools;public class BackupA extends Thread {     private DBTools dbtools;     public BackupA(DBTools dbtools) {          super();          this.dbtools = dbtools;     }     @Override     public void run() {          dbtools.backupA();     }}package extthread;import service.DBTools;public class BackupB extends Thread {     private DBTools dbtools;     public BackupB(DBTools dbtools) {          super();          this.dbtools = dbtools;     }     @Override     public void run() {          dbtools.backupB();     }}

Run:

package test.run;import service.DBTools;import extthread.BackupA;import extthread.BackupB;public class Run {    public static void main(String[] args) {        DBTools dbtools = new DBTools();        for (int i = 0; i < 20; i++) {            BackupB output = new BackupB(dbtools);            output.start();            BackupA input = new BackupA(dbtools);            input.start();        }    }}结果:★★★★★★★★★★★★★★★★★★★★★★★★★☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆★★★★★★★★★★★★★★★★★★★★★★★★★.........

交替打印的原理就是:volatile private boolean prevIsA=false;

方法join的使用

join():等待线程对象销毁。

用join()方法来解决

package extthread;public class MyThread extends Thread {     @Override     public void run() {          try {              int secondValue = (int) (Math.random() * 10000);              System.out.println(secondValue);              Thread.sleep(secondValue);          } catch (InterruptedException e) {              // TODO Auto-generated catch block              e.printStackTrace();          }     }}package test;import extthread.MyThread;public class Test {     public static void main(String[] args) {          try {              MyThread threadTest = new MyThread();              threadTest.start();              threadTest.join();              System.out.println("我想当threadTest对象执行完毕后我再执行,我做到了");          } catch (InterruptedException e) {              e.printStackTrace();          }     }}输出结果:9715我想当threadTest对象执行完毕后我再执行,我做到了

join 方法具有使线程排队运行的作用,有些类似同步的运行效果。join 与 synchronized 的区别是:join 在内部使用 wait() 方法进行等待,而 synchronized 关键字使用的是 “对象监视器” 原理做为同步

方法join与异常

在join过程中,如果当前线程对象被中断(interrupt),则当前线程出现异常(InterruptedException)。

方法join(long)的使用

方法join(long)中的参数是设定是等待的时间

public final synchronized void join(long millis)    throws InterruptedException {        long base = System.currentTimeMillis();        long now = 0;        if (millis < 0) {            throw new IllegalArgumentException("timeout value is negative");        if (millis == 0) {            while (isAlive()) {                wait(0);            }        } else {            while (isAlive()) {                long delay = millis - now;                if (delay <= 0) {                    break;                }                wait(delay);                now = System.currentTimeMillis() - base;            }        }    }

只等待XXXX毫秒时间

方法join(long)与sleep(long)的区别

方法join(long)的功能是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点
而Thread.sleep(long)方法却不释放锁

类ThreadLocal的使用

通过类ThreadLocal实现每一个线程都有自己的共享变量,解决的就是每个线程绑定自己的值。

线程变量的隔离性

package extthread;import tools.Tools;public class ThreadA extends Thread {     @Override     public void run() {          try {              for (int i = 0; i < 100; i++) {                   if (Tools.tl.get() == null) {                        Tools.tl.set("ThreadA" + (i + 1));                   } else {                        System.out.println("ThreadA get Value=" + Tools.tl.get());                   }                   Thread.sleep(200);              }          } catch (InterruptedException e) {              e.printStackTrace();          }     }}package extthread;import tools.Tools;public class ThreadB extends Thread {     @Override     public void run() {          try {              for (int i = 0; i < 100; i++) {                   if (Tools.tl.get() == null) {                        Tools.tl.set("ThreadB" + (i + 1));                   } else {                        System.out.println("ThreadB get Value=" + Tools.tl.get());                   }                   Thread.sleep(200);              }          } catch (InterruptedException e) {              e.printStackTrace();          }     }}package tools;public class Tools {     public static ThreadLocal tl = new ThreadLocal();}package test;import tools.Tools;import extthread.ThreadA;import extthread.ThreadB;public class Run {    public static void main(String[] args) {        try {            ThreadA a = new ThreadA();            ThreadB b = new ThreadB();            a.start();            b.start();            for (int i = 0; i < 100; i++) {                if (Tools.tl.get() == null) {                    Tools.tl.set("Main" + (i + 1));                } else {                    System.out.println("Main get Value=" + Tools.tl.get());                }                Thread.sleep(200);            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}输出结果:ThreadB get Value=ThreadB1Main get Value=Main1ThreadA get Value=ThreadA1ThreadB get Value=ThreadB1Main get Value=Main1ThreadA get Value=ThreadA1Main get Value=Main1ThreadB get Value=ThreadB1ThreadA get Value=ThreadA1Main get Value=Main1...........

解决get()返回null问题

通过继承ThreadLocal类,重写 initialValue方法

package ext;public class ThreadLocalExt extends ThreadLocal {     @Override     protected Object initialValue() {          return "我是默认值 第一次get不再为null";     }}

实现首次get()不为null;

InheritableThreadLocal 类的使用

该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。
通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数(parentValue+“XXXX”)。

阅读全文
0 0