学习笔记

Java并发编程实战-2基础知识_对象的共享
Publish: 2018/7/23   

可见性

内存可见性:当一个线程修改了对象的状态后,其他线程能够看到发生的状态变化。

重排序:在缺少同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

失效数据

get和set在没有同步的情况下访问value

@NotThreadSafe
public class MutableInteger{
    private int value;

    public int get(){return value;}
    public void set(int value){this.value=value}
}

非原子的64位操作

最低安全性:当现线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。

Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作和写操作分解为两个32位的操作。

加锁与可见性

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

Volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

volatile变量的正确使用方式包括:

发布与逸出

“发布”:使对象能够在当前作用域之外的代码中使用。

“逸出”:当某个不应该发布的对象被发布。

安全的对象构造过程

不要在构造过程中使this引用逸出。

//使用一个私有的构造函数和一个公共的工厂方法,避免不正确的构造过程。
public class SafeListener{
    private final EventListener listener;

    private SafeListener(){
        listener = new EventListener(){
            public void onEvent(Event e){
                doSomething(e);   
            }
        }
    }

    public static SafeListener newInstance(EventSouce source){
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

线程封闭

如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭。

可以将对象封闭在一个线程中,将自动实现线程安全性。

Ad-hoc 线程封闭

Ad-hoc 线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。

Ad-hoc 线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符,能够将对象封闭到目标线程上,尽量少用

只要能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作。

栈封闭

只能通过局部变量才能访问对象。

如果在线程内部上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。

ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal类。

ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

ThreadLocal对象通常用于防止对可变的单例或全局变量进行共享。

缺点:ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,需要小心使用。

不变性

不可变对象一定是线程安全的

当满足以下条件时,对象才是不可变的:

Final域

除非需要某个域是可变的,否则因将其声明为final域

示例:使用Volatile类型来发布不可变对象

@Immutable
class OneValueCache{
   private final BigInteger lastNumber;
   private final BigInteger[] lastFactors;

   public OneValueCache(BigInteger i,BigInteger[] factors){
      lastNumber = i;
      lastFactors = Arrays.copyOf(factors,factors.length);
   }   

   public BigInteger[] getFactors(BigInteger i){
      if(lastNumber==null || !lastNumber.equals(i))
         return null;
      else
         return Arrays.copyOf(lastFactors,lastFactors.length);   
   }
}

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
   /*通过使用包含多个状态变量的容器对象来维持不变性条件,
     并使用一个volatile类型的引用来确保可见性
    */
   private volatile OneValueCache cache = new OneValueCache(null,null);

   public void service(ServletRequest req,ServletResponse resp){
      BigInteger i = extractFormRequest(req);
      BigInteger[] factors = cache.getFactors(i);
      if(factors == null){
         factors = factor(i);
         cache = new OneValueCache(i,factors);
      }
      encodeIntoResponse(resp,factors);
   }
}

安全发布

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

安全发布的常用模式

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确构造的对象可以通过以下方式来安全的发布:

线程安全库中的容器类提供了以下的安全发布保证:

*通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

//静态初始化器由JVM在类的初始化阶段执行.
public static Holder holder = new Holder(42);

对象不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为事实不可变对象

在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

对象的发布需求取决于它的可变性:

安全地共享对象

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:



← Java并发编程实战-1基础知识_线程安全 Java并发编程实战-3基础知识_对象的组合 →

Powered by Hexo, Theme designs by @hpcslag.
Style-Framework Tocas-UI designs by @yamioldmel