线程安全性
标签:Java并发

线程安全性

编写线程安全的代码主要是对状态访问进行管理,特别是共享的(Shared,可以被多个线程访问)和可变的(Mutable,变量的值在其生命周期内可以发生变化)状态的访问。

非正式意义上讲,对象的状态值存储在状态变量(例如实例或静态域)中的数据。对象的状态还可能包括其他依赖对象的域。

可见我们对于多线程程序的关注点应该如何保证对这些状态变量的正确访问。

Java的主要同步机制是 synchronized,一种独占锁的方式,还包括 volatile 类型变量,显示锁 Explicit Lock 以及原子变量。

我们对于并发的控制主要是对上面这四种的操作。

访问某个变量的代码越少,越容易确保对变量的所有访问都实现正确的同步。

设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。程序状态的封装性越好,就越容易实现线程的安全性。

线程安全程序不一定完全由线程安全类构成,线程安全的类构成的程序并不一定就是线程安全的。

什么是线程安全性

当多个线程访问某个类是,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

上面给出的是线程安全性的定义。

一个无状态的对象,它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在线程栈上的局部变量中,并且只能由正在执行的线程访问

大多数Servlet都是无状态的,只有当Servlet在请求处理的时候需要保存一些信息,才会存在线程安全性问题。

原子性

静态条件

基于一种可能失效的观察结果来做出判断或者执行某个计算。这种类型的竟态条件称为“先检查后执行”(Check-Then-Act)

先检查后执行的的一种常见情况就是延迟初始化,延迟初始化的目的就是将对象的初始化操作推迟到时间被使用时才进行,同时要确保只被初始化一次。例如单例模式的饿汉模式等。

加锁机制

内置锁

Java提供了一种内置锁机制来支持原子性:同步代码块(Synchronized Block),它包含两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。

更多关于 Synchronized 修饰的查看:Java并发-线程安全性

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁(Monitor Lock),线程在进入同步代码块之前将获取锁,在退出同步代码块的时候将自动释放锁(无论以什么方式退出),而获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法

重入

内置锁是可以重入的,某个线程获取已经由它自己持有的锁,那么这个请求就会成功。重入的实现方式是为每个锁关联一个获取计数值一个所有者线程,为 0 的时候表示没有线程获取获取到锁。

对象的共享

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整,即指令重排序。

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

故一个线程读,一个线程写,读的线程可能会读到某个值的高32位和另一个值的低32位。

volatile

当把变量声明为 volatile 类型后,编译器在运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作进行重排序。

volatile 类型的变量不会被缓存在寄存器或者对其他处理器不可见的地方。故读取 volatile 类型的变量总会返回最新写入的值。

当前仅当满足以下所有条件时,才能使用volatile类型变量

  • 5 min read

CONTRIBUTORS


  • 5 min read