对象的共享

对象的共享

1 可见性

当多个线程在没有同步的情况下共享数据时出现的错误。无法保证主线程写入的ready值和number值对于读线程来说是可见的。

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();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

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

1.1 失效数据

在多线程中,读取数据后,同时另一个线程已经修改了变量,让读取到的数据失效。


非线程安全的可变整数类

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

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

线程安全的可变整数类

@NotThreadSafe
public class SynchronizedInteger {
    @GuardedBy("this")
    private int value;

    public synchronized int get() {
        return value;
    }

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

1.2 非原子的64为操作

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volition类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值得低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volition来声明它们,或用锁保护起来。

1.3 加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。
当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况下可以保证,在锁被释放之前,A看到的变量值在B获得锁之后同样可以由B看到。
在这里插入图片描述
为什么在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确锁的情况下对其某个变量,那么读到的可能是一个失效值。

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

1.4 Volatile变量

Java语言提供了一种消弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操组一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。


检查某个状态标记以判断是否退出循环。

volatile boolean asleep;
...
	while(!asleep)
		countSomeSheep();

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

当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变条件中。
  • 在访问变量时不需要加锁。

2 发布与逸出

“发布(Publish)”一个对象的意思是指,是对象能够在当前作用域之外的代码中使用。比如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其它类的方法中。
发布某个对象时,可能会间接地发布其他对象。
当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。
最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。

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

3 线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(ThreadConfinement),它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
例如:Swing中大量使用了线程封闭技术。Swing的可视化组件和数据模型对象都不是线性安全的,Swing通过将它们封闭到Swing的事件分发线程中来实现线程安全性。
另一种常见应用是JDBC的Connection对象。由于大多数请求都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。

3.1 Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特征,例如可见性修饰符或局部变量,能将对象封闭到目标线程上,事实上,对线程封闭对象的引用通常保存在共有变量中。
当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在某种情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。
在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性保证还确保
其他线程能看到最新的值。

3.2 栈封闭

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

3.3 ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。

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

ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

4 不变性

如果对象在被创建后其状态就不能被修改,那么这个对象称为不可变对象。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造器函数创建的,只要它们的状态不改变,那么这些不变性条件就能得以维持。

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

不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存可变对象的引用。

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

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(在对象的创建期间,this引用没有溢出)。

4.1 Final域

final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)。然而,在Java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步。

即使对象时可变的,通过将对象的某些域声明为final类型,任然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象任然比包含多个可变状态的对象简单。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

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


对数值及其因数分解结果进行缓存的不可变容器类

@Immutable
public 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类型引用以缓存最新的结果

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

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

5 安全发布

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

如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时任然需要同步。

安全发布的常用模式:

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

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

事实不可变对象
如果对象再发布后不会被修改,那么对于其他在没有额外同步的情况下安全地访问这些对象的线程来说,安全发布是足够的。所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且如果对象状态不再改变,那么就足以确保任何访问都是安全的。

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

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

可变对象

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

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

安全地共享对象

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其它线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值