Java多线程(三) 多线程间的基本通信

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

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

1、Java里的线程通信

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

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

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

输出结果如下:

Java多线程(三) 多线程间的基本通信

应该注意的是:

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条线程,测试如下:

Java多线程(三) 多线程间的基本通信

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

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

最后总结下ThreadLocal的作用:

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

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

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

  

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

上一篇:腾讯提供的TBS调试小程序页面


下一篇:18-10-31 Scrum Meeting 3