并发相关java对象_java并发编程(二): 对象的共享

对象的共享:

要编写并发程序,关键在于:在访问共享的可变状态时需要进行正确的管理

可见性:

/**

* 可见性问题导致,程序运行结果不正确

* 有可能由于编译器,处理器及运行时做一些重排序

*/

public class Novisibility {

private static boolean ready;

private static int number;

private static class ReaderThread extends Thread{

@Override

public void run() {

while (!ready){

Thread.yield(); //主动让出cpu, 进入就绪队列

}

System.out.println(number);

}

}

public static void main(String[] args) {

new ReaderThread().start();

number = 42;

ready = true;

}

}

失效数据:就如同上面的代码,没有同步的情况下可能产生错误的结果。

又如:

/**

* get操作可能与最近set值不一致,产生数据失效

*/

@NotThreadSafe

public class MutableInteger {

private int value;

public int getValue() {

return value;

}

public void setValue(int value) {

this.value = value;

}

} 可做如下修改:

/**

* 将get, set同步化,可防止数据失效

*/

@ThreadSafe

public class MutableInteger {

private int value;

public synchronized int getValue() {

return value;

}

public synchronized void setValue(int value) {

this.value = value;

}

}

非原子的64位操作:对于非volatile类型的long和double, jvm会将64位的读写操作分解为2个32位操作(java虚拟机栈中的操作数栈每个slot为32位),对于这种变量在多线程读写操作下,有可能读取到某个值的高32位或某个值的低32位,建议在多线程环境下,对共享可变的long或double变量进行volatile声明。

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

3eedf213c115bb7db7be11a34710a85f.gif

Volatile变量:之前转载过一篇相关文章(http://my.oschina.net/indestiny/blog/208541), 当变量声明为volatile后,那么编译器或运行时会主要这个变量是共享的,不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他处理器不可见的地方,因此读取volatile变量总会返回最新的值。

比较典型的用法:

private volatile boolean asleep;

...

while (!asleep){

// do sth.

}

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只可确保可见性,所以说volatile是一种轻量级同步机制;

满足以下所有条件时,可用volatile:

1. 对变量的写入操作不依赖变量的当前值,或者能确保只有一个线程更新变量的值;

2. 该变量不会与其他状态变量一起纳入不变性条件;

3. 访问该变量不需要加锁。

发布与逸出:

发布对象:使对象能够在当前作用域之外被使用, 如:

//对共有静态变量的发布,集合内部的变量也会被发布

public static Set publishedObject;

public void init(){

publishedObject = new HashSet<>();

}

//通过共有方法发布

private Object publishedObject;

public Object get(){

return publishedObject;

}

/**

* 通过发布类的内部实例

* this引用被逸出

*/

public class ThisEscape {

public ThisEscape(EventSource source){

source.registerListener(new EventListener() {

@Override

public void onEvent(Event e) {

// ThisEscape.this实例逸出,但此时该实例并没有构造完成

}

});

}

}

安全的对象构造过程:

不要在构造过程中使this引用逸出,如上面的ThisEscape;

可通过工厂方法避免this逸出:

/**

* 通过工厂方法防止this逸出

*/

public class SafeListener {

private final EventListener listener;

private SafeListener(){

listener = new EventListener() {

@Override

public void onEvent(Event e) {

//do sth.

}

};

}

public static SafeListener newInstance(EventSource source){

SafeListener safe = new SafeListener();

source.registerListener(safe.listener);

return safe;

}

}

线程封闭:

线程封闭:在单线程内访问数据,不需要同步,这是实现线程安全最简单的方式。

Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担,它很脆弱,因为没有一种语言特性能将对象封闭到目标线程上。

栈封闭:是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。比Ad-hoc线程封闭更易维护,强壮。

ThreadLocal类:一种维持线程封闭更规范的方法,它会为每个使用ThreadLocal变量的线程存放一份独立的副本,因此对于该变量线程之间不会相互干扰,你可以想ThreadLocal想成Map, 当线程终止时 ,这些值会作为垃圾回收。比如,多线程环境下,我们可以这样获取数据库连接:

private static ThreadLocal connectionHolder =

new ThreadLocal(){

@Override

protected Connection initialValue() {

try {

return DriverManager.getConnection("DB_URL");

} catch (SQLException e) {

e.printStackTrace();

}

return null;

}

};

public static Connection getConnection(){

//不同线程每次得到的Connection, 都是独立的备份

return connectionHolder.get();

}

不变性:

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

对象不可变应满足:

1.对象创建后其状态不能修改;

2.对象的所有域都是final类型;

3.对象是正确创建的(在对象创建期间,this未逸出)。

Final域:final类型的域不能修改(但其指向的引用对象可修改),重新改造之前文章的CachedFactorizer:

/**

* 不可变类:

* 所有域都是final

*/

public class OneValueCache {

private final BigInteger lastNumber;

private final BigInteger[] lastFactors;

public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {

this.lastNumber = lastNumber;

this.lastFactors = lastFactors;

}

public BigInteger[] getFactors(BigInteger i){

if (lastNumber == null || ! lastNumber.equals(i)){

return null;

} else{

return Arrays.copyOf(lastFactors, lastFactors.length);

}

}

}

/**

* 使用Volatile类型发布不可变对象

*/

@ThreadSafe

public class VolatileCachedFactorizer implements Servlet {

private volatile OneValueCache cache = new OneValueCache(null, null); //volatile保证每次写后最新值对其他线程可见

@Override

public void service(ServletRequest req, ServletResponse repo) {

BigInteger i = extractFromRequest(req);

BigInteger[] factors = cache.getFactors(i);

if (factors == null) {

factors = factor(i);

cache = new OneValueCache(i, factors);

}

reponseTo(i, factors);

}

}

安全发布:

不正确的发布:正确的对象被破坏:

/**

* 多线程访问下,有可能出错,问题不在Holder本身,而在于未正确地发布,可将n声明为final,避免不正确发布

*/

public class Holder {

private int n;

public Holder(int n){

this.n = n;

}

public void assertSanity(){

if (n != n){

throw new AssertionError("");

}

}

}

不可变对象与初始化安全性:

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

安全发布地常用模式:

一个正确构造的对象可以通过以下方式来安全地发布:

1.在静态初始化函数中初始化一个对象引用;

2.将对象的引用保存到volatile类型地域或AtomicReference对象中;

3.将对象的引用保存到某个正确构造对象地final类型域中;

4.将对象的引用保存到一个由锁保护的域中。

事实不可变对象:

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

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

可变对象:

可变对象:对象构造后,其状态可以发生改变;

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

1.不可变对象可以通过任意机制来发布;

2.事实不可变对象必须通过安全方式来发布;

3.可变对象必须通过安全方式来发布,并且必须是线程安全的或由某个锁保护起来。

安全地共享对象:

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

1.线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,只允许这个线程修改;

2.只读共享。在没有同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

3.线程安全共享。线程安全的对象在其内部实现同步,多个线程可以通过公有接口对其访问而不需进一步同步;

4.保护对象。被保护对象只能通过持有特定锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

不吝指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值