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的作用:

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

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

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

      

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

  • 相关阅读:
    IT综合学习网站收集
    使用CSS实现表格细边框的三种方式
    安装Ionic遇到的问题
    未能解析此远程名称:'nuget.org' 的解决方法
    webapi 安全验证与权限验证
    iOS模拟器可以编译,真机无法编译
    Mac上安装FFmpeg命令行
    写在工作三周年
    MPMoviePlayerController概述
    NSStream概述
  • 原文地址:https://www.cnblogs.com/doucheyard/p/5689149.html
Copyright © 2011-2022 走看看