1. 为什么需要通信:
1. 线程是操作系统中独立额个体,但这些个体如果不经过特殊的处理就不能成为一个整体。
2. 通信后,系统之间的交互性会更强大,能提高CPU的利用率,还能使程序员对各线程任务在处理的过程中进行有效的把控与监督。
2. 线程间通信的几种方式:
1. 使用wait/notify 实现线程间的通信
2. 生产者/消费者 模式的实现。
3. 方法join的使用。
4. ThreadLocal类的使用。
二。等待 / 通知机制。
1.使用sleep()结合while(true)死循环法来实现多线程间通信。
public class ThreadA extends Thread {
private MyList list;
public ThreadA(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list.add();
System.out.println("添加了" + (i + 1) + "个元素");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
private MyList list;
public ThreadB(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
while (true) {
if (list.size() == 5) {
System.out.println("==5了,线程b要退出了!");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyList {
private List list = new ArrayList();
public void add() {
list.add("王伟");
}
public int size() {
return list.size();
}
}
public class Test {
public static void main(String[] args) {
MyList service = new MyList();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
总结:
1.此种方式只能在JVM设置为非-server时通信.
2.浪费cpu资源,B必须得不停的轮询
这种通信机制不是等待/通知,两个线程完全是主动式地读取一个共享变量而实现的通信。
如果以服务员/厨师传菜为例,服务员不知道厨师到底有没有做好菜放到传菜台,只能不停的去传菜台查看,此时如果有一种等待/通知机制无疑
能节省很多资源,厨师做好菜放到传菜台上时,通知服务员,此时服务员才去传菜台。
2. wait()/notify()
1.在调用wait()之前,线程必须获得该对象的对象锁,即只能在同步方法或同步块中调用wait()方法。
2.在调用notify()之前,线程页必须获得该对象的对象锁,执行notify()后,当前线程并不会马上释放该对象锁,wait状态的线程也并不能马上获取该
对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁。
3.使用wait()/notify()实现:
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();
}
}
}
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();
}
}
}
public class MyList {
private static List list = new ArrayList();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
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();
}
}
}
3.阻塞:Blocked,例如线程遇到了IO操作,此时线程只能等待IO操作,而CPU处于空闲状态,可能会把CPU时间片分配给其他线程,Blocked
状态结束后进入Runnable状态,必须等待系统重新分配资源后才能继续运行。
出现阻塞的五种情况:
1.线程调用了sleep方法,主动丢弃占用的处理器资源。
2.线程调用了阻塞式的IO方法,在该方法返回前,该线程被阻塞。
3.线程试图获得一个同步监视器(即对象同步锁),但该同步监视器正被其他线程所持有。
4.线程等待某个通知。(wait())
5.线程调用了suspend方法将该线程挂起。(此方法容易导致死锁,尽量避免使用)。
4.就绪队列和阻塞队列
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。
就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,一个线程被唤醒后,才能进入就绪队列,等待CPU的调度,反之,一个线程被
wait后就会进入阻塞队列,等待下一次被唤醒。
5.当线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。
6.方法wait(long)方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过时间则自动唤醒。
7.注意wait()的条件发生变化引起的程序混乱,如两个从list里移除元素的线程,进入时判断list中若无元素则wait(),被唤醒后才继续从list中remove元素,
另外一个增加元素到list的线程增加了一个元素,然后notifyAll(),则那两个线程后执行的那个会因为list中没有元素再去remove而报异常,所以当wait被唤醒后一定要
有再次检查是否满足remove条件的逻辑。
public class Subtract {
private String lock;
public Subtract(String lock) {
super();
this.lock = lock;
}
public void subtract() {
try {
synchronized (lock) {
// 只有一次判定,notify后不会再次检查size是否为0
if (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 Subtract {
private String lock;
public Subtract(String lock) {
super();
this.lock = lock;
}
public void subtract() {
try {
synchronized (lock) {
// notify后会再次检查size是否为0,若是0则继续wait
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();
}
}
}
三。生产者/消费者模式的实现。
1.一个生产者和一个消费者:
// 生产者
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();
}
}
}
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();
}
}
}
public class ThreadP extends Thread {
private P p;
public ThreadP(P p) {
super();
this.p = p;
}
@Override
public void run() {
while (true) {
p.setValue();
}
}
}
public class ThreadC extends Thread {
private C r;
public ThreadC(C r) {
super();
this.r = r;
}
@Override
public void run() {
while (true) {
r.getValue();
}
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
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();
}
}
2.留意多生产者和多消费者假死现象,即所有生产者和消费者都进入了等待状态。
这种现象的发生是因为notify的线程不确定是哪个,可能是同类(生产者notify生产者),也可能是异类(生产者notify消费者)。
例如:两个生产者和消费者的情况,分别命名为P1,P2,C1,C2,他们都会一直监视ValueObject的值,生产者当ValueObject有值是wait,
消费者当ValueObject没有值时wait,考虑下列顺序:
1.P1设置了ValueObject值,notify无(此时其他线程并未启动),并在其下一次while中判断ValueObject非空,从而进入wait
2.p2发现ValueObject非空,从而进入wait。
3.C1进入消费了ValueObject值,notify了P1,并在其下一次while中判断ValueObject为空,从而进入wait.
4.C2发现ValueObject为空,从而进入wait。
5.P1被唤醒后设置了ValueObject值,并notify了P2,并在其下一次while中判断ValueObject非空,从而进入wait
6.P2发现ValueObject非空,从而进入wait。
解决方案:
很简单,生产者或消费者在生产或消费了之后唤醒其他所有等待线程notifyAll,而不是只唤醒一个线程notify。
这样,当5步时notify了P2,C1,C2 ,执行第6步时P2继续wait,切换给P1或P2任意一个都能继续执行下去,这样程序就能永远运行,因为总有一个是符合运行条件的
(生产了的东西肯定能被消费,消费完了肯定能再次生产,即三个等待的线程里必然有生产者和消费者,总有一个能执行)。
3.把上例中的ValueObject换成list,不就成了一个中间队列了吗,不过两个生产者和消费者的情况下还是会有假死,解决方案同上
使用while循环判定条件是否满足并且使用notifyAll通知所有等待线程。
四。通过管道流进行线程间通信
1.管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
2.JDK中提供了4个类来使线程间可以进行通信:
PipedInputStream和PipedOutputStream
PipedReader和PipedWriter
3.实例代码,通过管道字节流交互(PipedInputStream和PipedOutputStream),还可以通过字符流(PipedReader和PipedWriter),大同小异:
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();
}
}
}
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();
}
}
}
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);
}
}
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);
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
try {
WriteData writeData = new WriteData();
ReadData readData = new ReadData();
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
// inputStream.connect(outputStream);
outputStream.connect(inputStream);
ThreadRead threadRead = new ThreadRead(readData,
inputStream);
threadRead.start();
Thread.sleep(2000);
ThreadWrite threadWrite = new ThreadWrite(writeData,
outputStream);
threadWrite.start();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五。实战:wait/notify之交叉备份
1.创建20个线程,其中10个线程将数据备份至A数据库,另外10个线程备份数据到B数据库,使备份A数据库和B数据库是交叉进行的。
public class BackupA extends Thread {
private DBTools dbtools;
public BackupA(DBTools dbtools) {
super();
this.dbtools = dbtools;
}
@Override
public void run() {
dbtools.backupA();
}
}
public class BackupB extends Thread {
private DBTools dbtools;
public BackupB(DBTools dbtools) {
super();
this.dbtools = dbtools;
}
@Override
public void run() {
dbtools.backupB();
}
}
public class DBTools {
volatile private boolean prevIsA = false;
synchronized public void backupA() {
try {
while (prevIsA == true) {
wait();
}
//模拟备份A数据库操作
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();
}
//模拟备份B数据库操作
for (int i = 0; i < 5; i++) {
System.out.println("☆☆☆☆☆");
}
prevIsA = false;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
六。方法join的使用
1.作用:主线程创建并启动子线程,如果子线程需要运行较长时间,主线程将早于子线程结束,此时如果想让主线程等待子线程执行完后在结束就要
用到join()方法了。
2.引例,threadTest线程执行时间不确定,调用它的join加入主线程后,主线程总是能阻塞的等待threadTest执行完后再继续执行:
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();
}
}
}
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();
}
}
}
3.方法join具有使线程排队运行的作用,有些类似同步的运行效果。但join和synchronized的内部实现上是有区别的,join内部使用wait()
方法进行等待,而synchronized关键字使用的是“对象监视器”原理做为同步。
4.如果ThreadA是一个运行时间较长的线程,它join到ThreadB后,如果ThreadB被ThreadC打断Interrupted了,ThreadB会抛出异常,但是ThreadA会继续运行。
5.方法join(long)的使用,参数是设定等待的时间,指主线程等待的时间,如上例中指的是ThreadB,而不是ThreadA。
6.方法join(long) 与sleep(long)的区别:
1.join(long)内部使用wait实现,具有释放锁的特点。
public class ThreadA extends Thread {
private ThreadB b;
public ThreadA(ThreadB b) {
super();
this.b = b;
}
@Override
public void run() {
try {
synchronized (b) {
b.start();
Thread.sleep(6000);
// Thread.sleep()不释放锁!
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
@Override
public void run() {
try {
System.out.println(" b run begin timer="
+ System.currentTimeMillis());
Thread.sleep(3000);
System.out.println(" b run end timer="
+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public void bService() {
System.out.println("打印了bService timer="
+ System.currentTimeMillis());
}
}
public class ThreadC extends Thread {
private ThreadB threadB;
public ThreadC(ThreadB threadB) {
super();
this.threadB = threadB;
}
@Override
public void run() {
threadB.bService();
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
try {
ThreadB b = new ThreadB();
ThreadA a = new ThreadA(b);
a.start();
Thread.sleep(1000);
ThreadC c = new ThreadC(b);
c.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.Thread.sleep(long)方法具有不释放锁的特点。
修改上面线程ThreadA的代码如下
public class ThreadA extends Thread {
private ThreadB b;
public ThreadA(ThreadB b) {
super();
this.b = b;
}
@Override
public void run() {
try {
synchronized (b) {
b.start();
b.join();// join会释放线程ThreadB的对象锁,从而使得ThreadC继续运行!
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String newString = new String();
Math.random();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.方法join()后面的代码提前运行。
public class Run1 {
public static void main(String[] args) {
try {
ThreadB b = new ThreadB();
ThreadA a = new ThreadA(b);
a.start();
b.start();
b.join(2000);
System.out
.println("main end " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
说明:ThreadA和ThreadB执行过程中都会有个sleep(5000)的步骤,ThreadA的run方法会同步ThreadB对象,
此时主线程的完成时间是确定不了的,这要取决于ThreadA释放ThreadB的锁后,是ThreadB的run还是join先抢占到锁。
七。类ThreadLocal的使用:
1.作用:一般把对象的某属性对象定义为ThreadLocal类型的,则多线程时对应每个线程会有一个此属性对象的独立副本,相互不影响
从而能达到线程安全的目的。
八。类InheritableThreadLocal的使用。
1.作用:可以在子线程中取得父线程继承下来的值,可以让子线程从父线程中取得值。
2.如下例:主线程和子线程都从InheritableThreadLocal中取值,尽管主线程取值后sleep了5秒,子线程取得值仍然和主线程一样,说明是从主线程继承下来的值。
public class InheritableThreadLocalExt extends InheritableThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
}
public class Tools {
public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
}
public class ThreadA extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("在ThreadA线程中取值=" + Tools.tl.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
try {
for (int i = 0; i < 10; i++) {
System.out.println(" 在Main线程中取值="
+ Tools.tl.get());
Thread.sleep(100);
}
Thread.sleep(5000);
ThreadA a = new ThreadA();
a.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.值继承再修改:继承的同时还可以对值进行进一步的处理(追加删减等)
如下例:其他代码和上面一致,修改InheritableThreadLocalExt如下。
public class InheritableThreadLocalExt extends InheritableThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
@Override
protected Object childValue(Object parentValue) {
return parentValue + " 我在子线程加的~!";
}
}