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 }
view code

 

 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 }
SleepHelper

 

   输出结果如下:

      

    应该注意的是:

          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 }
两个Runnable

   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 }
Calculator

 使用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 }
日志类LogTimeChecker

   客户端测试代码

 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 }
Runnable类代码
 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的作用:

   ① 确保了层级间方法的独立,避免参数传递

   ② 确保线程间数据的独立,不进行数据同步

   ③ 提供了有效的变量回收机制,避免内存泄漏

  

  以上为本人对线程通讯的总结,有错误的地方烦请指正。

 

posted @ 2016-07-23 17:32  小灯笼  阅读(1197)  评论(0编辑  收藏  举报