【Java并发】JAVA并发编程实战-读书笔记4

来源:互联网 发布:繁体字笔画顺序软件 编辑:程序博客网 时间:2024/06/06 04:45

无论是什么原因保证线程安全的实际要求,都只是存在于一线开发人员编码的那一刻。如果线程内部用法的设定没有清楚地文档化,那么后期维护人员会错误放任对象的逸出。

一种维护线程限制的更加规范的方式是使用ThreadLocal,他允许你将每个线程与持有数值的对象关联在一起。ThreadLocal提供了setget访问器,get总是返回由当前执行线程通过set设置的最新值。

private static ThreadLocal<Connection> connectionHolder=    new ThreadLocal<Connection>(){        public Connection initialValue(){            return DriverManager.getConnection(DB_URL);        }    }public static Connection getConnection(){    return connectionHolder.get();}

假设你正在将一个单线程的应用迁移到多线程环境中,你可以将共享的全局变量都转换为ThreadLocal类型,前提是全局共享的语义允许这样,如果将应用级的缓存变成一堆线程本地缓冲,他将毫无价值。

创建后状态不能被修改的对象叫做不可变对象。不可变对象永远是线程安全的。

无论是Java语言规范还是Java存储模型都没有关于不可变性的正式定义,但是不可变性并不是简单等于将对象的所有域都声明为final类型,所有域都是final类型的对象仍然可以是可变的,因为final域可以获得一个到可变对象的引用。

通过使用不可变对象来持有所有的变量,可以消除在访问和更新这些变量时的竞争条件。如下,在不可变的容器中缓存数字和它的因数。

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);   }}

如果更新变量,会创建新的容器对象,不过在此之前任何线程都还和原先的容器交互,仍然看到他处于一致的状态。

当一个线程设置volatile类型的cache域引用到一个新的OneValueCache后,新数据会立即对其他线程可见。与cache域相关的操作不会相互干扰,因为OneValueCache是不可变的,而且每次只有一条相应的代码路径访问他。不可变的容器对象持有与不变约束相关的多个状态变量,并利用volatile引用确保及时的可见性,使得下面的代码虽然没有显示的用到锁,但仍然是线程安全的。

public class VolatileCachedFactorizer implements Servlet{  private volatile OneValueCache cache=new OneValueCache(null,null);  public void servie(ServletRequest req,ServletResponse resp){    BigInteger i=extractFromRequest(req);    BigInteger[] factors=cache.getFactors(i);    if(factors==null){      factos=factor(i);      cache=new OneValueCache(i,factors);    }    encodeIntoResponse(resp,factors);  }}
下面的这个例子是失败的,由于可见性的问题,容器还是会在其他线程中被设置为一个不一致的状态,即使他的不变约束已经在构造函数中得以正确创建。这种不正确的发布导致其他线程可以观察到局部创建对象。

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

如果下面的代码用上面的方式发布,那么除了发布线程,其他线程调用assertSanity时都可能抛出AssertionError。此种问题并非出在Holder类自身,而是Holder没有被正确发布,但是将域n声明为final类型,使得Holder成为不可变的,可以避免出现不正确发布的问题。

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

因为没有同步来确保Holder对其他线程可见,会导致两种错误。首先,发布线程以外的任何线程都可以看到Holder域的过期值,因而看到的是一个null引用或者旧值,即使此刻Holder已经被赋予新值。其次,更坏的情况是,线程看到的Holder引用是最新的,然而Holder状态却是过期的。这将使程序执行变得更加不可预测。可以看出,在构造函数中设置的域值,应该是向这些域写入的第一个值,因此没有更旧的值可以作为所谓的默认值,但是Object的构造函数会先于子类的构造函数运行,并首先向所有域写入默认值,这些默认值可能成为域的过期值。线程首次读取某个域可能会看到过期值,再次读取该域会得到一个更新值,这正是抛出AssertionError的原因。
我们处于自我复制的风险中,如果没有充足的同步,跨线程共享数据时会发生一些非常奇怪的事情。
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
1,通过静态初始化器初始化对象的引用;
2,将他的引用存储到volatile域或AtomicReference;
3,将他的引用存储到正确创建的对象的final域中;
4,或者将他的引用存储到由锁正确保护的域中;

最简单的方式就是使用静态初始化器因为没有同步来确保Holder对其他线程可见,会导致两种错误。首先,发布线程以外的任何线程都可以看到Holder域的过期值,因而看到的是一个null引用或者旧值,即使此刻Holder已经被赋予新值。其次,更坏的情况是,线程看到的Holder引用是最新的,然而Holder状态却是过期的。这将使程序执行变得更加不可预测。可以看出,在构造函数中设置的域值,应该是向这些域写入的第一个值,因此没有更旧的值可以作为所谓的默认值,但是Object的构造函数会先于子类的构造函数运行,并首先向所有域写入默认值,这些默认值可能成为域的过期值。线程首次读取某个域可能会看到过期值,再次读取该域会得到一个更新值,这正是抛出AssertionError的原因。

public static Holder holder=new Holder(42);

静态初始化器由JVM在类的初始化阶段执行,由于JVM内在的同步,该机制确保了以这种方式初始化的对象可以被安全地发布。
一个对象在技术上不是不可变的,但是他的状态不会在发布后被修改,这样的对象成为有效不可变对象。
比如,Date自身是可变的,但是如果你把他当做不可变对象来使用就可以忽略锁。否则,每当Date被跨线程共享时,都要用锁确保安全。假设你正在维护一个map,存储每位用户的最近登录时间:

pulbic Map<Stirng,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());

如果Date值在置入map中后就不会改变,那么synchronizedMap中同步的实现对于安全地发布Date值是至关重要的,而访问这些Date值时就不在需要额外的同步。
在并发程序中,使用和共享对象的一些有效的策略如下
线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有她的线程修改。
共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改他,共享只读对象包括可变对象和有效不可变对象。
共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外同步,就可以通过公共接口随意的访问他。
被守护的:一个被守护的对象只能通过特定的锁来访问。被守护的对象包括那些被线程安全对象封装的对象和已知被特定的锁保护起来的已发布对象。

0 0
原创粉丝点击