【Java 并发】对象的共享

来源:互联网 发布:更相减损术算法步骤 编辑:程序博客网 时间:2024/05/18 18:55


【Java 并发】对象的共享


【Java 并发】对象的共享

对于并发编程,我们不仅希望防止某个线程在修改使用对象状态而另一个线程在同时修改该对象的状态(保证原子性),而且希望确保当一个线程修改了对象的状态后,其他线程能够看到状态变化(保证可见性)。

一,可见性
(一),可见性的通俗理解:一个共享变量的改变,所有线程都知道它改变后的结果,不管是哪个线程去改变共享变量。特别是读操作和写操作在不同的线程执行时。

原子性是说一个操作是否可分割。可见性是说操作结果其他线程是否可见,他们需要同步机制,

可见性的底层细节就是Java内存模型(JMM,Java Memory Model)
JMM是语言级别的内存模型,主要用于控制一个共享变量的写入何时对另一个线程可见。JMM可以确保在不同的处理器平台和编译器之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性的保证。
这里写图片描述
描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
所有的变量都存储在主内存中。
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
两条规则:
不同线程之间无法直接访问其他线程工作内存中的变量,
线程间变量值得传递需要通过主内存来完成。
实现过程:
把工作内存1中更新过的共享变量刷新到主内存中。
将主内存中最新的共享变量的值更新到工作内存2中。

(转载:https://www.cnblogs.com/rocomp/p/4780532.html)
(关于编译器重排序和处理器重排序可以参考,有详细讲解:
http://blog.csdn.net/u011116672/article/details/50130367)

(二),实现可见性的方法
1,synchronized实现可见性

使用synchronized加锁的机制,不仅仅是为了互斥行为(原子性),还包含内存的可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作和写操作的线程都必须在同一个锁上同步。这也是保证写操作完成后可以把最新值写入主内存,读操作时可以从主内存中读取最新值。
2,volatile实现可见性
volatile是一种弱同步机制,当变量被声明为volatile变量是,编译器和处理器就会注意到这个变量是共享的,因此不会将对该变量上的操作与其他操作进行重排序,volatile变量不会被缓存在寄存器或者其他不可见的地方。
从内存可见性的角度,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
虽然volatile很方便,但是volatile只能保证可见性,却无法保证原子性。
当且仅当以下情况才可以使用volatile变量
a,对变量的写入操作不依赖与变量的当前值(比如:++count就依赖于当前值),或者保证只有单线程更新变量的值。
b,该变量不会与其他状态变量一起作为不变性条件,也就是说每个volatile相互独立。
c,访问变量时不需要加锁。

3,两种方式的对比
a,volatile不需要加锁,因此不会阻塞线程,比synchronized更轻量级,效率更高。
b,从内存可见性的角度,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
c,synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,不能保证原子性。

二,发布和逸出
发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系。
什么是发布?简单来说就是提供一个对象的引用给作用域之外的代码。比如return一个对象,或者作为参数传递到其他类的方法中。
什么是逸出?如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性。

class UnsafeStates{    private String[] states = new String[]{"AK", "AL"};    public String[] getStates(){        return states;    }}

states变量作用域是private而我们在getStates方法中却把它发布了,这样就称为数组states逸出了它所在的作用域。

还有更加隐蔽的逸出—-this引用逸出,常见的错误:
1,在构造函数中启动一个线程,this引用就被新创建的线程共享了。
2,在构造函数中注册一个事件监听器,this引用就被触发被监听事件的线程共享了。
看代码

public class ThisEscape {    private final int i;    public ThisEscape(EventSource source) {        source.registerListener(new EventListener() {            public void onEvent(Event e) {                doSomething(e);            }        });        i=100;    }    void doSomething(Event e) {        System.out.println(i);    }

在构造方法中我们定义了一个匿名内部类,匿名内部类是一个事件监听类,当事件监听类注册完毕后,实际上我们已经将EventListener匿名内部类发布出去了,当我们声明一个匿名内部类时,编译器会添加对外部类的一个隐式引用,该对象引起内部类对象的构造,该对象引起内部类对象的构造。如果外部类的名字为Outer,那么隐式引用就是Outer.this。而此时我们实际上已经携带了this逸出,重点在于这个时候我们还有一些初始化工作没有做完(i 的赋值还未完成),这也就是上面所说的,一个类还没有构造结束我们已经将发布了。

解决办法是:使用一个私有的构造函数和一个公共的工厂方法,

public class SafeListener {    private final EventListener listener;    private SafeListener(){        listener = new EventListener(){            public void onEvent(Event e){                doSomething(e);            }        }    }    public static SafeListener newInstance(EventSource source){        SafeListener safe = new SafeListener();        source.registerListener(safe.listener);        return safe;    }}

将构造函数设定为private,使用工厂方法,在工厂方法newInstance中待构造函数执行完毕后再将对象进行发布(代码中即为registenerListener注册监听)。这实际上就是修构造完毕才发布对象。这样就将整个构造过程变成原子性。

以上都是阅读《Java编程思想》-并发和《Java并发编程实践》的笔记,不喜勿喷!

原创粉丝点击