java多线程编程核心技术3-线程间通信

一。为什么需要线程间通信,有哪几种方式实现:
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 + " 我在子线程加的~!";
			}
		}







 
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值