zoukankan      html  css  js  c++  java
  • 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的作用:

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

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

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

      

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

  • 相关阅读:
    JavaScript对原始数据类型的拆装箱操作
    Javascript继承(原始写法,非es6 class)
    动态作用域与词法作用域
    自行车的保养
    探索JS引擎工作原理 (转)
    C语言提高 (7) 第七天 回调函数 预处理函数DEBUG 动态链接库
    C语言提高 (6) 第六天 文件(续) 链表的操作
    C语言提高 (5) 第五天 结构体,结构体对齐 文件
    C语言提高 (4) 第四天 数组与数组作为参数时的数组指针
    C语言提高 (3) 第三天 二级指针的三种模型 栈上指针数组、栈上二维数组、堆上开辟空间
  • 原文地址:https://www.cnblogs.com/doucheyard/p/5689149.html
Copyright © 2011-2022 走看看