Effective Java读书笔记二二(Java Tips.Day.22)

来源:互联网 发布:淘宝代销管理软件 编辑:程序博客网 时间:2024/05/23 01:57

TIP 66 同步访问共享的可变数据


关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块


读或写一个变量是原子的,除非这个变量的类型是long或者double。

然而,即使原子操作,如果没有使用同步,仍然是错误的、非常危险的。


下面来看示例代码:

public class Main {    private static boolean stopRequested  = false;    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(() -> {            int i = 0;            while (!stopRequested){                i++;            }        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        stopRequested = true;    }}

你可能期待程序运行大约1秒之后,线程内的循环终止。

然而事实上,线程会一直运行,永不停止。


问题就在于,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested 的值所做的改变。

没有同步,虚拟机会将代码优化,从

while (!stopRequested){   i++;}

优化成这样:

if (!stopRequested){    while (true){       i++;    }}

于是子线程只会判断一次stopRequested的值,然后就会进入死循环。


解决方法是同步访问stopRequested,这个程序会在大约一秒钟之后如期停止:

public class Main2 {    private static boolean stopRequested  = false;    public static synchronized boolean isStopRequested() {        return stopRequested;    }    public static synchronized  void setStopRequested(boolean stopRequested) {        Main2.stopRequested = stopRequested;    }    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(() -> {            int i = 0;            while (!isStopRequested()){                i++;            }            if (!stopRequested){                while (true){                    i++;                }            }        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        setStopRequested(true);    }}

读和写方法都被同步了,只同步写方法是不够的! 读/写方法必须都同步,否则,子线程循环依然停不下来!

读写方法内的动作都是原子的,所以synchronized只是为了它的通信效果,而不是为了互斥访问。


另外一个解决办法,是在声明stopRequested 时,使用volatile关键字:
private static volatile boolean stopRequested = false;

这个关键字不执行互斥访问,但是可以保证任何一个线程在读取该field时,都将看到最近刚刚被写入的值。

在使用volatile时必须要小心。如果你想这样来使用它:

private static volatile int nextSerialNumber = 0;public static int getNextSerialNumber(){     return nextSerialNumber ++;}

这个方法的目的是要确保每个调用都返回不同的值。然而,这个方法不一定会正常工作。

虽然nextSerialNumber 是可原子访问的,但运算符++不是原子性操作,它相当于:
nextSerialNumber = nextSerialNumber +1;

所以如果方法不加同步,多次调用方法就有可能返回相同的值。这就是安全性失败


安全发布


对于类:

public class Holder {    private int n;    public Holder(int n) {        this.n = n;    }    public void assertSanity() {        if(n != n) {            throw new AssertionError("This statment is false.");        }    }}

不安全的发布:

public Holder holder;  public void initialize() {     holder = new Holder(42);  }  

如果holder已被初始化,但还没来得及为n初始化为42,其它线程由调用了assertSanity(),而且在两次读取n时,分别读到了0(未初始化) 和 42(已初始化) ,那么就会很悲剧的抛出断言错误。


要安全的发布对象,有几种方法:

  1. 将对象保存在static field中,作为类初始化的一部分: public static Holder holder = new Holder(42);
  2. 将对象引用保存到volatile类型的域或者AtomicReference对象中
  3. 将对象引用保存到某个正确构造对象的final类型域中
  4. 将对象保存到某个类型安全的容器中

后记

本条目开始,就进入多线程并发领域了。这块知识与实践也是博主欠缺的,但由于工作所限,现在也只能边学习边做笔记,错误的地方还请批评指正。

本条目关于安全发布的内容,参考了《Java并发编程实战》相关的章节。但博主在测试过程中,并没有发现(n != n) 返回true的情况,JDK和JVM相关信息如下:

java version “1.8.0_111”
Java(TM) SE Runtime Environment (build
1.8.0_111-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)

如果有朋友测试发现了这个异常,请联系博主,万分感谢~~

原创粉丝点击