2.同步(Synchronization)

来源:互联网 发布:千万不要在淘宝上搜索 编辑:程序博客网 时间:2024/05/22 10:31

当多个线程不会相互作用时,扩展多线程的应用是非常容易的,典型是在变量之间共享。当多线程相互作用时,那么多个问题将会出现,这些问题让应用处于线程不安全(在多线程的上下文中处于不正确的位置。)在这一章节,读者将会学习关于这些问题,和学习如何去解决它们,通过Java的同步语句。

2.1.线程的问题(The Problems with Threads)

  java提供线程可以促进发展请求和可变的应用。然而,提供这些将会增长复杂的代价,一不小心,你的代码可能会出现难以琢磨的问题。例如:竞争状态、数据状态、缓存变量。

2.1.1竞争状态(Race Conditions)

   当正确计算依赖于相对固定时间或多个线程交织的调试程序时,那么竞争状态就会出现。思考下面的代码片断,只要有确定的前提条件就会执行计算。

 if (a == 10.0)
b = a / 2.0;

   如果这个代码片断是在单一线程的上下文中是没有问题有,和当a和b是局部变量时,在多线程的上下文中也是没有问题的。然而,假设a和b是明确的实例或类是全局变量,而两个线程同时调用这个代码,那么就有问题了。

   假设一个线程已经执行if(a==10.0)和它是关于去执行b = a /2.0时被程序的线程挂起,然而继续另一个线程改变了a的值。当被挂起的那个线程继续执行时,那么变量b就不是等于5了。(如a和b是本地变量,这个竞争的状态是不会出现的,因为每一个线程都会拷贝自己局部变量。)

   这个代码片断是我们普遍的竞争状态的例子,是我们所知的检查-行动(check-then-act),这个就是决定下一步要做什么。前面的例子片断中,“check”是if(a==10.0)和“act”是b=a/2.0.

其它的竞争状态是读-修改-写(read-modify-write),新的状态来自于前一个状态。前一个状态是读,然后修改,最后更新影响到修改的结果,分为三个步骤来操作。然而,结合的操作是不可以分隔的。

一个普通的例子关于读-修改-写(read-modify-write),涉及的变量是在增加。例如,下面的代码片断,提供一个计算器,这个计算器是一个固定域的int类型(初始化为1),之后有两个线程线程同时调用这个代码。

public int getID(){return counter++;} 

尽管它看起来是一个操作,但是counter++实际上是三个操作:读counter的值,添加1到这个counter,和储蓄更新的值。读这个将是这个值的表现。

提供线程1请求getID()和读counter的值,在调度程序暂停之前它将会是1.现在有线程2运行,请求getID(),读counter的value(1),添加1到这个值,在counter中储蓄result(2),和返回1给请者。

这里关键的是,假定线程2重新恢复,添加1到先前读的value(1),储蓄result(2)在counter中,和返回1给请求者。因为线程1撤消了线程2执行的步骤,我们也就会失去了一次增加的数和一个不唯一的ID就会出现。为个方法是没有用的,有问题。

2.1.2数据竞争(Data Race)

一个竞争状态经常会在两个或多个线程共同使用本地内存时出现数据竞争的混淆,在此期间至少有一个线程是在访问写操作,而这些线程不能协调它们访问内存。当这些条件保持,访问顺序是非确定性的。

private static Parser parser;public static Parser getInstance(){if (parser == null)parser = new Parser();return parser;} 

假设线程1第一次执行getInstance()方法。因为parser的值是null,所以线程1就会去实例Parser和赋值给parser.当线程2随后请求getInstance()方法时,它可能显示parser的值不为空的引用,所以就会返回一个parser的值。与此相同,线程2可能显示parser是空,和创建一个新的Parser对象。也就是说,一个行为必须早于另一个行为,在线程1写parser和线程2读parser时,数据竞争已经出现了。

2.1.3缓存变量(Cached Variable)

为了提高性能,编译器、java虚拟机和操作系统共同去缓存一个变量,在注册或本地处理器的缓存,而不是仅仅依赖于内存。每一个线程拥有它自己副本变量。当一个线程写这个变量时,它会写到自己的副本处;其它线程不可能看到这个副本更新的。

private static BigDecimal result;public static void main(String[] args){Runnable r = () ->{result = computePi(50000);};Thread t = new Thread(r);t.start();try{t.join();}catch (InterruptedException ie){// Should never arrive here because interrupt() is never// called.}System.out.println(result);}

这个全局的变量result显示了一个缓存变量的问题。这个域访问一个工作的线程执行result=computePi(50000);在lambda的上下文,和在主线程执行System.out.println(result)。

这个工作线程可以存储computePi返回的值在它的副本result,而主线程打印出的是副本的值。主线程可能看不到result = computePi(50000)的值;注册和它的副本可能保留着默认的空值。此值将输出而不是结果的字符串表示(计算的PI值)。

源码下载:git@github.com:owenwilliam/Thread.git




原创粉丝点击