Java多线程(三) 多线程间的基本通信
多条线程在操作同一份数据的时候,一般需要程序去控制好变量。在多条线程同时运行的前提下控制变量,涉及到线程通信及变量保护等。
本博文主要总结:①线程是如何通信 ②如何保护线程变量
1、Java里的线程通信
在多线程的第二小节已经总结过:控制多条线程访问方法,可以通过synchronized关键字对方法上锁,保证每次只有一条线程能够调用该方法。但让程序交替执行方法,那得给线程上锁,且通过线程间的通信完成变量之间的共享及操作。
Java里面线程间通信对程序员是透明的,通过线程操作变量具体步骤如下:
上图为线程间共享数据时的通信图,在Java程序内发生线程通信的主要表现在第③步骤。这一步主要通过wait、notify 和 notifyall 三个方法完成,线程间的数据共享以及通信;
举个没什么实际意义的例子:现在有两条线程,一条线程对k变量进行累加,一条线程对k进行累减,交替执行5次。跟第二篇总结的例子基本一致,但第二篇的例子没有对数据进行操作,单纯地对内容进行加减。
1 package com.scl.thread; 2 3 public class ThreadCommunicateReview 4 { 5 public static void main(String[] args) 6 { 7 int k = 10; 8 // 把calculator作为内部类的操作成员,操作共享变量K 9 final Calculator calculator = new Calculator(k); 10 new Thread(new Runnable() 11 { 12 @Override 13 public void run() 14 { 15 SleepHelper.sleep(100); 16 // 进行四轮调换 17 for (int i = 0; i < 4; i++) 18 { 19 calculator.addNum(); 20 } 21 } 22 23 }, "add").start(); 24 25 new Thread(new Runnable() 26 { 27 28 @Override 29 public void run() 30 { 31 SleepHelper.sleep(100); 32 // 进行四轮调换 33 for (int i = 0; i < 4; i++) 34 { 35 calculator.subNum(); 36 } 37 } 38 }, "sub").start(); 39 } 40 } 41 42 // 建立计算类,把相关计算内容整合到同一个类里面进行管理 43 class Calculator 44 { 45 // 让操作变量属于同一个类,在外部使用 46 private int k = 0; 47 private volatile boolean isAdd = true; 48 49 public Calculator(int value) 50 { 51 this.k = value; 52 } 53 54 public synchronized void addNum() 55 { 56 while (!isAdd) 57 { 58 try 59 { 60 // 不是进行“加”操作时,线程进行等待,释放对象锁 61 wait(); 62 } 63 catch (InterruptedException e) 64 { 65 e.printStackTrace(); 66 } 67 } 68 // 循环五次进行递增 69 for (int i = 0; i < 5; i++) 70 { 71 System.out.println(Thread.currentThread().getName() + " " + ++k); 72 } 73 // 执行完递减操作后,把标识位标识为递减,通知其他线程竞争对象锁 74 isAdd = false; 75 notify(); 76 } 77 78 public synchronized void subNum() 79 { 80 while (isAdd) 81 { 82 try 83 { 84 // 进行“加”操作时,线程进行等待 85 wait(); 86 } 87 catch (InterruptedException e) 88 { 89 e.printStackTrace(); 90 } 91 } 92 // 循环五次进行递减 93 for (int i = 0; i < 5; i++) 94 { 95 System.out.println(Thread.currentThread().getName() + " " + --k); 96 } 97 // 执行完递减操作后,把标识位标识为增加,通知其他线程竞争对象锁 98 isAdd = true; 99 notify(); 100 } 101 }
1 package com.scl.thread; 2 3 public class SleepHelper 4 { 5 public static void sleep(long sleepTime) 6 { 7 try 8 { 9 Thread.sleep(sleepTime); 10 } 11 catch (InterruptedException e) 12 { 13 e.printStackTrace(); 14 } 15 } 16 }
输出结果如下:
应该注意的是:
1. wait和notify方法都是在Object里面集成过来的,但是两个方法都是被定义成final类型,没法通过子类的继承对这两个方法进行修改。
2. wait和notify方法必须放在Synchronized定义的代码块内,因为这两个方法必须得到对象锁。
当对象调用wait方法时,会释放掉对象的锁,然后进行等待。notify同样会把当前锁对象释放,唤醒等待的线程对对象进行锁竞争。
线程变量操作需要注意的是:
1. 共享的线程变量必须是外部变量/全局变量。synchronized修饰的方法内部不需要任何volatile变量约束,也不必要对这些局部变量约束
2. 使变量被多个线程操作具体方法有两个
①使用两条线程,线程内部有一个变量引用,通过变量应用共同操作同一个业务类
②把业务类定义被final约束,在匿名内部类Runnable内调用业务类的相关方法完成操作(如上述例子)
3. 根据面向对象的编程思想,对线程内的业务操作最好整合到一个类里面。
2、Java线程变量保护
上面的代码涉及了部分线程变量共享以及线程通信,但是怎么使用Java去保护每条线程独立的变量呢。即让线程A操作自己的变量,线程B操作自己的变量,两条线程的变量互不干涉?这个问题跟JDBC里面的事务很相似。因为事务必须是独立的,每个不同的事务需要在不同的连接上完成,且互不干涉。
线程之间互不干涉,那就把变量设置成线程的局部变量,让每条线程自己去完成任务就可以了。开始的时候,笔者也是如此想的。后来发现,如果要执行这种线程变量的传递,是件非常麻烦的事情!比如:有一个共享计算器(Calculator),可以提供给其他人进行加减操作,要求通过日志类(LogService)把相关的线程操作记录,同时记录每条线程操作的时间及线程调用方法。为避免重复,还需要在线程内生成相关的UUID,标注每个不同的线程。
根据上述的要求及面向对象的设计模式,程序必须设计三个类:
①计算器类Calculator,负责集成加减法的业务逻辑,每个线程内的加减法必须上锁
②日志类LogService,记录线程运行时间,记录线程UUID等。
③线程类,负责生成相关的UUID随机数,因模拟加减两个操作,需要分开两条线程:一个为AddRunable,另一个命名为SubRunable
大致如下:
线程类记录线程相关信息,与线程运行业务分离
1 package com.scl.thread.threadlocal; 2 3 import java.util.UUID; 4 5 class AddRunable implements Runnable 6 { 7 private Calculator calculator; 8 private String myRandomId; 9 10 public String getMyRandomId() 11 { 12 return myRandomId; 13 } 14 15 public void setMyRandomId(String myRandomId) 16 { 17 this.myRandomId = myRandomId; 18 } 19 20 public AddRunable(Calculator c) 21 { 22 this.calculator = c; 23 } 24 25 @Override 26 public void run() 27 { 28 calculator.addNum(1000); 29 } 30 31 private String CreateRandomId() 32 { 33 myRandomId = UUID.randomUUID().toString(); 34 return myRandomId; 35 } 36 37 } 38 39 class SubRunable implements Runnable 40 { 41 private Calculator calculator; 42 private String myRandomId; 43 44 public String getMyRandomId() 45 { 46 return myRandomId; 47 } 48 49 public void setMyRandomId(String myRandomId) 50 { 51 this.myRandomId = myRandomId; 52 } 53 54 public SubRunable(Calculator c) 55 { 56 this.calculator = c; 57 } 58 59 @Override 60 public void run() 61 { 62 calculator.subNum(1000); 63 } 64 65 private String CreateRandomId() 66 { 67 myRandomId = UUID.randomUUID().toString(); 68 return myRandomId; 69 } 70 71 }
Calculator的两方法设置为自增及自减
1 package com.scl.thread.threadlocal; 2 3 public class Calculator 4 { 5 6 public void addNum(int value) 7 { 8 LogTimeChecker.star(); 9 for (int i = 0; i < 10000000; i++) 10 { 11 value++; 12 } 13 System.out.println(value); 14 LogTimeChecker.end(); 15 } 16 17 public void subNum(int value) 18 { 19 for (int i = 0; i < 100; i++) 20 { 21 value--; 22 } 23 } 24 }
使用LogTimeChecker记录线程运行时间及调用内容
1 package com.scl.thread.threadlocal; 2 3 public class LogTimeChecker 4 { 5 static long beginMills; 6 static long endMills; 7 8 public static void star() 9 { 10 beginMills = System.currentTimeMillis(); 11 } 12 13 public static void end() 14 { 15 String methodName = Thread.currentThread().getStackTrace()[2].getMethodName(); 16 endMills = System.currentTimeMillis(); 17 System.out.println( methodName + " cost:" + (endMills - beginMills)); 18 } 19 }
客户端测试代码
1 package com.scl.thread.threadlocal; 2 3 import org.junit.Test; 4 5 public class TestLog4Thread 6 { 7 @Test 8 public void TestLog() throws InterruptedException 9 { 10 Calculator c = new Calculator(); 11 12 Thread t1 = new Thread(new AddRunable(c)); 13 t1.start(); 14 Thread t2 = new Thread(new AddRunable(c)); 15 t2.start(); 16 t1.join(); 17 t2.join(); 18 } 19 }
在没完成本段代码之前,必须说明下目前这段代码的问题。日志类代码跟计算业务类代码强关联,如果有一百个方法需要加日志,每次都要在类的方法内添加begin,和end两个方法,耦合度太高...此处不进行修改,详细修改内容可参见另一篇博文:动态代理模式
先撇开日志记录方法的问题,要把线程变量贯穿三层,最好就是这在日志类里面能够使用类似Thread.currentThread( )方法获取当前线程的类,然后使用类的getMyRandomId方法获取到在线程产生的UUID。可是JDK并没有通过Thread.currentThread( )去获取自定义线程内的类对象。
那么程序可能需要在每一层传输Runnable对象。
1 ① 把参数传到日志类对象内,那么日志类的方法可能变成这样: 2 public static void start(Runnable r) 3 { 4 AddRunable run = (AddRunable)r; 5 run.getMyRandomId(); 6 beginMills = System.currentTimeMillis(); 7 } 8 这里还要获取出对象到底是AddRunable还是SubRunable,然后转换. 9 10 ② 要在计算器对象内,把Runnable对象进行传递 11 public void addNum(int value,Runnable r) 12 { 13 14 LogTimeChecker.start(r); 15 for (int i = 0; i < 10000000; i++) 16 { 17 value++; 18 } 19 System.out.println(value); 20 LogTimeChecker.end(); 21 } 22 ③ 修改AddRunable里面的run方法 23 public void run() 24 { 25 calculator.addNum(1000, this); 26 }
我的天... 想想都觉得麻烦,而且还要去判断Runnable对象,在日志类内进行转换!这时候需要使用ThreadLocal,在一层内写代码,在三层内共享数据,且每个线程内的数据独立。简单地说,就是实现在日志类内通过Thread.currentThread( )的思想,获得每条线程自己的内容,不在层间传递。
修改如下:
1 package com.scl.thread.threadlocal; 2 3 public class LogTimeChecker 4 { 5 static long beginMills; 6 static long endMills; 7 8 public static void start() 9 { 10 beginMills = System.currentTimeMillis(); 11 } 12 13 public static void end() 14 { 15 String methodName = Thread.currentThread().getStackTrace()[2].getMethodName(); 16 endMills = System.currentTimeMillis(); 17 // 通过静态类获取ThreadLocal对象内容TestLog4Thread.threadLocal.get() 18 System.out.println(TestLog4Thread.threadLocal.get() + " " + methodName + " cost:" + (endMills - beginMills)); 19 } 20 }
1 package com.scl.thread.threadlocal; 2 3 public class Calculator 4 { 5 // 自增次数 6 public void addNum(int value) 7 { 8 9 LogTimeChecker.start(); 10 for (int i = 0; i < 10000000; i++) 11 { 12 value++; 13 } 14 System.out.println(value); 15 LogTimeChecker.end(); 16 } 17 18 // 递减循环 19 public void subNum(int value) 20 {
LogTimeChecker.start(); 21 for (int i = 0; i < 100; i++) 22 { 23 value--; 24 }
LogTimeChecker.end(); 25 } 26 }
1 package com.scl.thread.threadlocal; 2 3 import java.util.UUID; 4 5 class AddRunable implements Runnable 6 { 7 private Calculator calculator; 8 private String myRandomId; 9 10 public String getMyRandomId() 11 { 12 return myRandomId; 13 } 14 15 public void setMyRandomId(String myRandomId) 16 { 17 this.myRandomId = myRandomId; 18 } 19 20 public AddRunable(Calculator c) 21 { 22 this.calculator = c; 23 } 24 25 @Override 26 public void run() 27 { 28 this.setThreadLocal(); 29 calculator.addNum(1000); 30 } 31 32 private String CreateRandomId() 33 { 34 myRandomId = UUID.randomUUID().toString(); 35 return myRandomId; 36 } 37 38 private void setThreadLocal() 39 { 40 // 获取当前线程下ThreadLocal的内容,如果为空,设置相关的值 41 if (TestLog4Thread.threadLocal.get() == null) 42 { 43 TestLog4Thread.threadLocal.set(CreateRandomId()); 44 } 45 } 46 } 47 48 class SubRunable implements Runnable 49 { 50 private Calculator calculator; 51 private String myRandomId; 52 53 public String getMyRandomId() 54 { 55 return myRandomId; 56 } 57 58 public void setMyRandomId(String myRandomId) 59 { 60 this.myRandomId = myRandomId; 61 } 62 63 public SubRunable(Calculator c) 64 { 65 this.calculator = c; 66 } 67 68 @Override 69 public void run() 70 { 71 this.setThreadLocal(); 72 calculator.subNum(1000); 73 } 74 75 private String CreateRandomId() 76 { 77 myRandomId = UUID.randomUUID().toString(); 78 return myRandomId; 79 } 80 81 private void setThreadLocal() 82 { 83 // 获取当前线程下ThreadLocal的内容,如果为空,设置相关的值 84 if (TestLog4Thread.threadLocal.get() == null) 85 { 86 TestLog4Thread.threadLocal.set(CreateRandomId()); 87 } 88 } 89 }
1 package com.scl.thread.threadlocal; 2 3 import org.junit.Test; 4 5 public class TestLog4Thread 6 { 7 //在对象内定义threadLocal对象,并进行初始化 8 static ThreadLocal<String> threadLocal = new ThreadLocal<String>() 9 { 10 @Override 11 protected String initialValue() 12 { 13 return null; 14 } 15 }; 16 17 @Test 18 public void TestLog() throws InterruptedException 19 { 20 Calculator c = new Calculator(); 21 22 Thread t1 = new Thread(new AddRunable(c)); 23 t1.start(); 24 Thread t2 = new Thread(new SubRunable(c)); 25 t2.start(); 26 t1.join(); 27 t2.join(); 28 } 29 }
启动20条线程,测试如下:
以上就是使用ThreadLocal对线程的变量进行独立的操作。其实例子可以不使用ThreadLocal来贯穿三层代码,可以使用HashMap代替。但通过HashMap把线程和对应的变量存储,不但HashMap会变得很大,线程销毁的时候还要对HashMap里面的数据进行删除这样就显得比较麻烦。
关于ThreadLocal的源码解析可以查看以下链接 : http://www.iteye.com/topic/103804
最后总结下ThreadLocal的作用:
① 确保了层级间方法的独立,避免参数传递
② 确保线程间数据的独立,不进行数据同步
③ 提供了有效的变量回收机制,避免内存泄漏
以上为本人对线程通讯的总结,有错误的地方烦请指正。