多条线程在操作同一份数据的时候,一般需要程序去控制好变量。在多条线程同时运行的前提下控制变量,涉及到线程通信及变量保护等。

本博文主要总结:①线程是如何通信  ②如何保护线程变量

1、Java里的线程通信

在多线程的第二小节已经总结过:控制多条线程访问方法,可以通过synchronized关键字对方法上锁,保证每次只有一条线程能够调用该方法。但让程序交替执行方法,那得给线程上锁,且通过线程间的通信完成变量之间的共享及操作。

Java里面线程间通信对程序员是透明的,通过线程操作变量具体步骤如下:

上图为线程间共享数据时的通信图,在Java程序内发生线程通信的主要表现在第③步骤。这一步主要通过wait、notify 和 notifyall 三个方法完成,线程间的数据共享以及通信;

举个没什么实际意义的例子:现在有两条线程,一条线程对k变量进行累加,一条线程对k进行累减,交替执行5次。跟第二篇总结的例子基本一致,但第二篇的例子没有对数据进行操作,单纯地对内容进行加减。

 package com.scl.thread;

 public class ThreadCommunicateReview
{
public static void main(String[] args)
{
int k = 10;
// 把calculator作为内部类的操作成员,操作共享变量K
final Calculator calculator = new Calculator(k);
new Thread(new Runnable()
{
@Override
public void run()
{
SleepHelper.sleep(100);
// 进行四轮调换
for (int i = 0; i < 4; i++)
{
calculator.addNum();
}
} }, "add").start(); new Thread(new Runnable()
{ @Override
public void run()
{
SleepHelper.sleep(100);
// 进行四轮调换
for (int i = 0; i < 4; i++)
{
calculator.subNum();
}
}
}, "sub").start();
}
} // 建立计算类,把相关计算内容整合到同一个类里面进行管理
class Calculator
{
// 让操作变量属于同一个类,在外部使用
private int k = 0;
private volatile boolean isAdd = true; public Calculator(int value)
{
this.k = value;
} public synchronized void addNum()
{
while (!isAdd)
{
try
{
// 不是进行“加”操作时,线程进行等待,释放对象锁
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 循环五次进行递增
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread().getName() + " " + ++k);
}
// 执行完递减操作后,把标识位标识为递减,通知其他线程竞争对象锁
isAdd = false;
notify();
} public synchronized void subNum()
{
while (isAdd)
{
try
{
// 进行“加”操作时,线程进行等待
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 循环五次进行递减
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread().getName() + " " + --k);
}
// 执行完递减操作后,把标识位标识为增加,通知其他线程竞争对象锁
isAdd = true;
notify();
}
}

view code

 package com.scl.thread;

 public class SleepHelper
{
public static void sleep(long sleepTime)
{
try
{
Thread.sleep(sleepTime);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

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

大致如下:

 线程类记录线程相关信息,与线程运行业务分离

 package com.scl.thread.threadlocal;

 import java.util.UUID;

 class AddRunable implements Runnable
{
private Calculator calculator;
private String myRandomId; public String getMyRandomId()
{
return myRandomId;
} public void setMyRandomId(String myRandomId)
{
this.myRandomId = myRandomId;
} public AddRunable(Calculator c)
{
this.calculator = c;
} @Override
public void run()
{
calculator.addNum(1000);
} private String CreateRandomId()
{
myRandomId = UUID.randomUUID().toString();
return myRandomId;
} } class SubRunable implements Runnable
{
private Calculator calculator;
private String myRandomId; public String getMyRandomId()
{
return myRandomId;
} public void setMyRandomId(String myRandomId)
{
this.myRandomId = myRandomId;
} public SubRunable(Calculator c)
{
this.calculator = c;
} @Override
public void run()
{
calculator.subNum(1000);
} private String CreateRandomId()
{
myRandomId = UUID.randomUUID().toString();
return myRandomId;
} }

两个Runnable

Calculator的两方法设置为自增及自减

 package com.scl.thread.threadlocal;

 public class Calculator
{ public void addNum(int value)
{
LogTimeChecker.star();
for (int i = 0; i < 10000000; i++)
{
value++;
}
System.out.println(value);
LogTimeChecker.end();
} public void subNum(int value)
{
for (int i = 0; i < 100; i++)
{
value--;
}
}
}

Calculator

 使用LogTimeChecker记录线程运行时间及调用内容

 package com.scl.thread.threadlocal;

 public class LogTimeChecker
{
static long beginMills;
static long endMills; public static void star()
{
beginMills = System.currentTimeMillis();
} public static void end()
{
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
endMills = System.currentTimeMillis();
System.out.println( methodName + " cost:" + (endMills - beginMills));
}
}

日志类LogTimeChecker

客户端测试代码

 package com.scl.thread.threadlocal;

 import org.junit.Test;

 public class TestLog4Thread
{
@Test
public void TestLog() throws InterruptedException
{
Calculator c = new Calculator(); Thread t1 = new Thread(new AddRunable(c));
t1.start();
Thread t2 = new Thread(new AddRunable(c));
t2.start();
t1.join();
t2.join();
}
}

客户端代码

在没完成本段代码之前,必须说明下目前这段代码的问题。日志类代码跟计算业务类代码强关联,如果有一百个方法需要加日志,每次都要在类的方法内添加begin,和end两个方法,耦合度太高...此处不进行修改,详细修改内容可参见另一篇博文:动态代理模式

 先撇开日志记录方法的问题,要把线程变量贯穿三层,最好就是这在日志类里面能够使用类似Thread.currentThread( )方法获取当前线程的类,然后使用类的getMyRandomId方法获取到在线程产生的UUID。可是JDK并没有通过Thread.currentThread( )去获取自定义线程内的类对象。

那么程序可能需要在每一层传输Runnable对象。

 ① 把参数传到日志类对象内,那么日志类的方法可能变成这样:
public static void start(Runnable r)
{
AddRunable run = (AddRunable)r;
run.getMyRandomId();
beginMills = System.currentTimeMillis();
}
这里还要获取出对象到底是AddRunable还是SubRunable,然后转换. ② 要在计算器对象内,把Runnable对象进行传递
public void addNum(int value,Runnable r)
{ LogTimeChecker.start(r);
for (int i = 0; i < 10000000; i++)
{
value++;
}
System.out.println(value);
LogTimeChecker.end();
}
③ 修改AddRunable里面的run方法
public void run()
{
calculator.addNum(1000, this);
}

我的天... 想想都觉得麻烦,而且还要去判断Runnable对象,在日志类内进行转换!这时候需要使用ThreadLocal,在一层内写代码,在三层内共享数据,且每个线程内的数据独立。简单地说,就是实现在日志类内通过Thread.currentThread( )的思想,获得每条线程自己的内容,不在层间传递。

修改如下:

 package com.scl.thread.threadlocal;

 public class LogTimeChecker
{
static long beginMills;
static long endMills; public static void start()
{
beginMills = System.currentTimeMillis();
} public static void end()
{
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
endMills = System.currentTimeMillis();
// 通过静态类获取ThreadLocal对象内容TestLog4Thread.threadLocal.get()
System.out.println(TestLog4Thread.threadLocal.get() + " " + methodName + " cost:" + (endMills - beginMills));
}
}

日志类对象代码

 package com.scl.thread.threadlocal;

 public class Calculator
{
// 自增次数
public void addNum(int value)
{ LogTimeChecker.start();
for (int i = 0; i < 10000000; i++)
{
value++;
}
System.out.println(value);
LogTimeChecker.end();
} // 递减循环
public void subNum(int value)
{
LogTimeChecker.start();
for (int i = 0; i < 100; i++)
{
value--;
}
LogTimeChecker.end();
}
}

计算器对象代码

 package com.scl.thread.threadlocal;

 import java.util.UUID;

 class AddRunable implements Runnable
{
private Calculator calculator;
private String myRandomId; public String getMyRandomId()
{
return myRandomId;
} public void setMyRandomId(String myRandomId)
{
this.myRandomId = myRandomId;
} public AddRunable(Calculator c)
{
this.calculator = c;
} @Override
public void run()
{
this.setThreadLocal();
calculator.addNum(1000);
} private String CreateRandomId()
{
myRandomId = UUID.randomUUID().toString();
return myRandomId;
} private void setThreadLocal()
{
// 获取当前线程下ThreadLocal的内容,如果为空,设置相关的值
if (TestLog4Thread.threadLocal.get() == null)
{
TestLog4Thread.threadLocal.set(CreateRandomId());
}
}
} class SubRunable implements Runnable
{
private Calculator calculator;
private String myRandomId; public String getMyRandomId()
{
return myRandomId;
} public void setMyRandomId(String myRandomId)
{
this.myRandomId = myRandomId;
} public SubRunable(Calculator c)
{
this.calculator = c;
} @Override
public void run()
{
this.setThreadLocal();
calculator.subNum(1000);
} private String CreateRandomId()
{
myRandomId = UUID.randomUUID().toString();
return myRandomId;
} private void setThreadLocal()
{
// 获取当前线程下ThreadLocal的内容,如果为空,设置相关的值
if (TestLog4Thread.threadLocal.get() == null)
{
TestLog4Thread.threadLocal.set(CreateRandomId());
}
}
}

Runnable类代码

 package com.scl.thread.threadlocal;

 import org.junit.Test;

 public class TestLog4Thread
{
//在对象内定义threadLocal对象,并进行初始化
static ThreadLocal<String> threadLocal = new ThreadLocal<String>()
{
@Override
protected String initialValue()
{
return null;
}
}; @Test
public void TestLog() throws InterruptedException
{
Calculator c = new Calculator(); Thread t1 = new Thread(new AddRunable(c));
t1.start();
Thread t2 = new Thread(new SubRunable(c));
t2.start();
t1.join();
t2.join();
}
}

客户端测试代码

启动20条线程,测试如下:

  以上就是使用ThreadLocal对线程的变量进行独立的操作。其实例子可以不使用ThreadLocal来贯穿三层代码,可以使用HashMap代替。但通过HashMap把线程和对应的变量存储,不但HashMap会变得很大,线程销毁的时候还要对HashMap里面的数据进行删除这样就显得比较麻烦。

  关于ThreadLocal的源码解析可以查看以下链接 : http://www.iteye.com/topic/103804

最后总结下ThreadLocal的作用:

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

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

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

  

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

Java多线程(三) 多线程间的基本通信的更多相关文章

  1. java多线程三之线程协作与通信实例

    多线程的难点主要就是多线程通信协作这一块了,前面笔记二中提到了常见的同步方法,这里主要是进行实例学习了,今天总结了一下3个实例: 1.银行存款与提款多线程实现,使用Lock锁和条件Condition. ...

  2. Java学习笔记46(多线程三:线程之间的通信)

    多个线程在处理同一个资源,但是线程的任务却不相同,通过一定的手段使各个线程能有效地利用资源, 这种手段即:等待唤醒机制,又称作线程之间的通信 涉及到的方法:wait(),notify() 示例: 两个 ...

  3. Java 并发和多线程(三) 多线程的代价 [转]

    原文链接:http://tutorials.jenkov.com/java-concurrency/costs.html 作者:Jakob Jenkov     翻译:古圣昌        校对:欧振 ...

  4. 多线程(三)多线程同步_基本介绍及mutex互斥体

    同步进制的引入为了解决以下三个主要问题:1.控制多个线程之间对共享资源访问,保证共享资源的完整性例如:线程A对共享资源进行写入,线程B读取共享资源2.确保多个线程之间的动作以指定的次序发生例如:线程B ...

  5. Java多线程编程核心技术(三)多线程通信

    线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时 ...

  6. Java多线程基础——线程间通信

    在使用多线程的时候,经常需要多个线程进行协作来完成一件事情.在前面两章分析了Java多线程的基本使用以及利用synchronized来实现多个线程同步调用方法或者执行代码块.但上面两章的内容涉及到的例 ...

  7. java多线程与线程间通信

    转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...

  8. iOS开发多线程篇—线程间的通信

    iOS开发多线程篇—线程间的通信 一.简单说明 线程间通信:在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信 线程间通信的体现 1个线程传递数据给另1个线程 在1个线程中执行完特定任 ...

  9. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

随机推荐

  1. 如何在Swift里用UnsafeMutablePointer

    下午在适配iPadUI的时候,用到了UIPopoverPresentationController,然后在转屏的时候需要调用UIPopoverPresentationControllerDelegat ...

  2. android之多媒体篇(三)

    录像 Android提供了2种方案去录像. 方案一: 最简单的方式就是使用Intents去启动App来帮助你完成.这个方案使你能够指定输出的位置和视频的质量.这方案通常是最好的方法,应该可以用在多种情 ...

  3. StarlingMVC Framework 原理。。。

    向starlingmvc 中添加bean后..会根据Metadata标签,分别交给不同的Processor去处理...然后会执行每个bean的postConstruct函数.相当于初始化函数...可以 ...

  4. 优秀js插件收藏

    1. 滚动视差效果,类似锤子主页等效果实现 https://github.com/hahnzhu/parallax.js 2. jQuery全屏滚动插件 http://www.dowebok.com/ ...

  5. mydumper原理2

    使用mydumper备份发生Waiting for table flush,导致所有线程都无法读和写 版本 mydumper 0.9.1OS centos6.6 X86_64mysql 5.6.25- ...

  6. VirtualBOX 虚拟机安装 OS X 10.9 Mavericks 及 Xcode 5,本人X220亲测

    原文链接:http://bbs.weiphone.com/read-htm-tid-7625465.html 建议电脑要求    Windows 7/8, 32 / 64 bit    CPU Int ...

  7. 小白日记42:kali渗透测试之Web渗透-SQL盲注

    SQL盲注 [SQL注入介绍] SQL盲注:不显示数据库内建的报错信息[内建的报错信息帮助开发人员发现和修复问题],但由于报错信息中提供了关于系统的大量有用信息.当程序员隐藏了数据库内建报错信息,替换 ...

  8. BootStrap2学习日记7---表格

    基本表 代码: <div class="container"> <h1 class="page-header">基本表</h1&g ...

  9. 安卓AlertDialog的使用

    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setMessage("数 ...

  10. javascript 关于语义化作用的理解

    看代码实例1 var a=1; function m(a){ //此处为形参第一个传入函数的参数,既为arguments[0] alert(a); //此处a为与形参绑定的 } m(a);//1 此时 ...