并发——对象的共享

1.1可见性的理解
我们已经知道了同步代码块和同步方法可以确保以原子性的方式执行操作,但是,同步还有另一方面的重要作用:内存可见性。我们希望一个线程在修改了对象状态后其他线程能够看到发生的状态变化。这就是说,为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

可见性,是针对处理器来说的,让处理器能过及时地看到变化。

造成变量状态可见性失效,不可见的原因可能是我们没有使用同步块,其中有一种现象,叫做“重排序”,可能导致可见性失效,“重排序”就是说,编译器会帮助我们优化程序,改变我们语句的顺序,根据我们各条语句的消耗,来排序、优化执行语句的顺序。

另外,由于我们与你及的设置,在线程没有同步情况下去读取飞volatile类型的64位数值变量(double和long)时,jvm会将这个操作分解为两个32位的操作,所以,在多线程中,long和double等64为变量也是不安全的,除非使用volatile来声明他们。

所以,我们一定要理解到,加锁的含义不仅仅局限于互斥行为,还包括内存可见性,确保所有线程都能看到共享变量的最新值。

1.2 关于Volatile变量
JAVA语言提供了一种稍弱的同步机制,即volatile变量,用了该确保将变量的更新操作通知到其他线程。 当把变量声明为volatile类型后,编译器与运行时线程都会注意到这个变量是共享地,因此不会将该变量上的操作和其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方 在访问volatile变量时不会执行加锁操作,因此也就不会使线程执行阻塞,所以我们成volatile是一种比synchronized更轻量的同步机制。

volatile变量一般用来设置状态,比如布尔类型,判断状态的标志,很方便,但是请注意,volatile的语义不足以确保递增操作的原子性(count++),除非我们能保证只有一个线程对变量执行操作。

2.1 关于发布和逸出(逃逸)
发布,public,就是让对象能够在当前作用域之外的代码中使用。在某些中,我们要确保对象及其内部状态被发布,在发布时要确保线程安全性,这可能就需要同步。 总之,发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。

当某个不该发布的对象被发布时,我们称之为逸出(逃逸)。

当发布某个对象时,可能会间接地发布其他对象,我们举个例子

public class ThisEscape{
    public ThisEscape(EventSource source){
        source.registerListener(
            new EventListener(){
                public void onEvent(Event e){
                    doSomething(e);
                }
            });
    }
}

当ThisEscape发布EventListener的时候,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。 尽管这并不一定受到攻击,但是这已经是不安全的了。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确的构造。

我们可以使用工厂方法来防止this引用在构造过程中逸出。

3.1关于如何共享可变数据

方法一:线程封闭。当访问共享可变数据的时候,通常都是要使用同步,如果我们不共享,只在单线程内访问数据,就不需要同步。这种技术,称为线程封闭。

方法二::栈封闭。局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。 简单说,就是使用局部变量。

方法三:ThreadLocal类。维持线程封闭性更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象的时候,就可以使用这项技术。

3.2不可变性与安全发布
满足同步需求的另一种方法是使用不可变对象,不可变对象一定是线程安全的。说的就是Final类型的域。 如同“除非需要更高的可见性,否则应该将所有的域声明为私有域”一样,“除非某个域确实是需要改变的,否则应将其声明为final域”,这是良好的变成习惯。

可变对象必须通过安全的方式来发布,则通常意味着发布和使用该对象的线程时都必须使用同步。 我们可以使用在线程安全容器内同步,例如ConcurrentMap。

猜你喜欢

转载自blog.csdn.net/qq_36120793/article/details/80543685
今日推荐