Java并发编程学习记录#3

共享对象

我们已经见识到同步方法和同步代码块能够保证操作执行的原子性,但同时这也是一个常见的误区:同步仅仅关于原子性。其实,同步还有另一个重要而微妙的方面–内存可见性。我们不仅仅希望阻止一个线程修改另一个线程正在使用的对象,我们还希望当一个线程修改了某个对象后,其改变后的状态能够被其它线程观察到。

可以使用具体的同步或是已经封装好的类库,来保证对象改变后,能将状态安全的发布出去。

可见性

可见性是个微妙的话题,特别是在未同步的并发情况下,一个例子来说明。

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

上述例子的结果有3个,分别解释下:

  • 输出42,这是最直观,最容易理解的方式,不解释;
  • 陷入死循环,因为ready的值在主线程改变之后,未必会发布给其它线程;
  • 输出0,这个显得非常怪异。因为新ready的值可能发布在number的值修改前,这种现象称为reording。在一个线程中,若是reording内部不能被检测到的话,那么操作将不一定按照程序给定的顺序执行,即使这个reording能被其它线程检测到。例子中,在主线程中,先给number写值,再给ready写值这个顺序无法在没有同步的情况下得以保证,也就是说,读线程可能先检测到ready的值的改变。这可能是一个比较差的设计,但的确能让JVM充分的的利用现代处理器的硬件。

没有使用同步时,Java内存模型将允许编译器打乱操作顺序并在寄存器里缓存值,也允许CPUs打乱指令顺序并在处理器定制缓存里缓存值,即,多线程环境下,对于未使用同步的代码,不能推导出其真正的执行顺序。

上述这么个简单小例子就非常容易出错了,复杂点的更容易出现问题,好在有同步机制,记住,在多线程数据共享时,一定要加入合适的同步代码。

1.过期数据

上述例子的结果可以简化描述为:过期数据。它可能出现,也可能不出现,也可能只部分出现:即一个变量是新数据,另一个变量仍是过期数据。过期数据可以造成很多严重后果:异常,损坏数据结构,计算错误还有死循环等。

2.非原子性的64位操作

Java内存模型要求读取和存储操作必须是原子性的,但是对于非volatile的long型和double型,JVM允许将这些64位的读写操作当做两个单独的32位操作。如果读写操作出现在不同的线程中的时候,这就有可能会出现读取到一个nonvolatile的长整型或者得到高位的32比特或者低位的32比特。这样即使你不去关心过期数据的问题,在多线程环境中使用可变的long和double变量也是不安全的。除非他们被声明为volatile的或者被锁守护。

3.锁和可见性

内部锁机制可以保证一个线程使用一种可以预期的方式看到另外一个线程的结果。

在多线程中,当访问一个共享可变变量时,应该给所有的线程使用同一个锁加以同步,这样可以保证当一个线程修改了这个变量时,其它的线程将会观察到最新数据。

锁,不仅仅关乎互斥操作,它还关乎内存可见性,为了保证所有的线程能够看到共享变量的最新状态,它们应该使用同一个锁进行同步。

4.Volatile变量

Java语言提供一种可选的,弱化的同步机制-volatile变量,来保证对某个变量的修改以可以预见的形式被其他线程获得。当一个变量被声明为volatile类型之后,编译器和运行时环境就会住注意到该变量是被共享的,这样这个变量之上的操作就不会与其他内存操作打乱时序。Volatile变量不会在寄存器中缓存也不会在cpu私有的缓存中进行缓存,因此读取一个volatile类型的变量将肯定会返回被线程修改后的最新值。

依赖于volatile变量来获取任意状态可见比使用锁机制更加更脆弱,也更加难以理解。 所有Volatile变量要慎用。

当代码逻辑的实现非常简单,或者验证你的同步策略的时候,才会用到volatile变量。Volatile变量通常会用来作为竞争、中断、状态标记。

锁既可以保证原子性,也可以保证内存可见性;volatile变量只能保证内存可见性。

如果代码的正确性验证需要考虑到可见性的时候,不要使用volatile变量。对于volatile变量的正确使用方式包括:

• 对变量的修改并不依赖于变量的当前值,或者你能够保证只有一个线程可以修改该变量值。

• 该变量不会与其他状态一起参与不变性的维护。

• 在变量被访问的时候,没有其他对锁机制的需求。

对象发布和逃逸

对象发布:使得这个对象可以在当前范围之外的地方使用。常见方式:

  • 引用放到一个可以公共访问的地方,比如public;
  • 引用放到一个可以间接被访问的地方,比如添加到一个集合中;
  • 引用作为非私有有方法的返回值提供给外部;
  • 引用作为方法的参数,传递给其它类;
  • 对外提供一个内部类的实例,这个实例将会隐式持有本对象引用。

对象逃逸:一个对象在并未声明发布时却被发布了,就会造成这个对象的逃逸。

对象逃逸的可能场景:

  • 声明了一个不想发布的对象,却把它放在了对外发布的集合中,外界便可通过遍历该集合,获得这个对象;
  • 内部类的实例对外发布时,会隐式的携带这个对象的引用;
  • 执行的线程方法中,可能持有所在对象的引用,进一步会造成对象逃逸;
  • 构造函数中创建并启动一个线程。

对象逃逸的危害:逃逸的对象总是存在被误用的风险,无论对其它类或是线程中。

对象的安全发布

一个正确创建的对象,可以通过下列条件正确的发布:

  • 通过静态初始化器初始化对象的引用;
  • 将它的引用存储在volatile域中或者atomicReference中;
  • 将它的引用存储到正确创建的对象的final域中
  • 将它的引用存储到由锁正确保护的域中

在并发程序中,使用和共享对象的几种最佳实践:

  • 线程限制:被一个线程独占,也只能在这个线程中修改;
  • 共享只读:任何线程都不能修改它;
  • 共享线程安全:对象自身设置成线程安全,其它线程可随意访问;
  • 被守护的对象只能通过获取特定的锁来进行访问和修改。

//待下篇

主要参考自_ Java Concurrency in Practice
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值