线程最主要的目的就是提高程序的运行性能,线程可以使程序更加充分地发挥系统的可用处理能力,从而提高系统的资源利用率,但是在有多个线程访问同一个对象时,我们就要保证其安全性。
要编写线程安全的代码,其核心就是对状态访问操作进行管理,特别是对共享的和可变的状态的访问
对象的状态和安全性
定义: 对象的状态是指存储在状态变量(例如实例或静态变量)中的数据,而对象的线程安全性则是对象的行为和其规范是否完全一致
无状态对象一定是线程安全的: 因为它既不包含任何域,也不包含任何对其他类中域的引用;计算过程中的临时状态仅存在于线程栈上的局部变量中,且只能由正在执行的线程访问。
判别: 当多个线程访问某个类时,不管运行时环境采用何种调度方法或者这些线程将如何交替执行,并且在主调代码中不需要进行其他额外的同步或协同,这个类始终都能表现正确的行为,那么这个类就是线程安全的。
用锁来保护状态:
锁能使其保护的代码路径以串行的形式来访问
- 访问共享状态的符合操作,例如递增操作(读取-修改-写入)或者延迟初始化(先检查后执行),都必须保证是原子操作以避免产生竟态条件。
- 对象的内置锁与状态没有内在联系
- 常见加锁约定,将所有的可变状态封装在对象内部,并由对象的内置锁对所有可变状态的代码路径进行同步,使得在该对象上不会发生并发访问
-并非所有的数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过加锁来保护 - 当类的不变性条件涉及多个状态变量时,在不变性条件中的每个变量必须有同一个锁来保护;只有这样才可以在单个原子操作中访问或更新这些变量,从而确保不变性条件不被破坏
当执行时间较长的计算或者可能无法快速完成的操作时(网络I/O或控制台I/O等)一定不要持有锁
内存可见性:
同步可以确保以原子的方式执行操作 同时还有另外一个重要的方面:内存可见性
即某个线程正在使用对象状态而另一个线程在同时能够修改该状态,而且确保当一个线程修改了对象状态后,其他线程能看到发生的线程状态的变化
为了确保多个线程对之间对内存的写入操作的可见性,必须使用同步机制
指令重排序:
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整;无法确保线程中的操作将按照程序中指定的顺序执行。
只要在某个线程中无法检查到重排序情况,那么就无法确保线程中的操作将按照程序中指定的顺序来执行。
public class PoosibleRoordering{
static int x = 0, y=0;
static int a = 0, b=0;
public static void main(String[] args) throws InterruptedException{
Thread A= new Thread(new Runnable(){
public void run(){
a = 1;
x= b;
}
});
Thread B= new Thread(new Runnable(){
public void run(){
b = 1;
y = a;
}
});
A.start(); B.start();
A.join(); B.join();
System.out.println("x = "+ x +", y = "+y);
}
}
多次运行测试结果:
x = 0, y = 1
x = 1, y = 1
x = 1, y = 0
这是因为没有正确的同步,线程A可以在线程B开始之前执行完成,线程B也可以在A开始执行之前执行完成,或者二者的操作交替完成。 但除了这几种情况之外还可能输出 x=0,y=0,因为每个线程的各个操作之间不存在数据流依赖性,因此可以乱序执行
失效数据:
一个线程可能获得某个变量的最新值,而获得另一个变量的失效值
public class MutableInteger{
private int value;
public int get() {return value}
public void set(int value){this.value = value;}
}
如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到
最低安全性:
定义: 线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,不是随机值,这种安全性称为最低安全性
最低安全性适用于绝大部分变量,但对非volatile类型的64位数值变量不适用(float和long);因为Java内存模型要求,变量的读取和写入操作都必须是原子操作,但对非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解成两个32位的操作。 这时,如果对非volatile类型的long变量进行读操作和写操作在不同的线程中执行,那么很可能读取到某个值的高32位和另一个值的低32位。
加锁与可见性:
访问某个共享且可变的变量时,所有线程要在用一个锁上同步;只有这样才能确保某个线程写入该变量的值对其他变量是可见的,否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么很可能读到一个失效值
volatile变量:
volatile变量是Java中一种削弱的同步机制,主要功能有两个:
- 禁止指令重排序:当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存一起重排序
- 保持可见性:并且volatile变量不会被缓存到到寄存器或者其他处理器不可见的地方,因此在读取volatile变量的时候总会返回最新值
volatile变量对可见性的影响比volatile变量本身更为重要。 写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块
与锁的区别:
volatile虽然很方便,但也有局限性,一般用做某个操作完成、发生中断或者状态的标志。 因为volatile变量不足以确保操作的原子性,例如递增操作(读-改-写) ,加锁机制既可以保证原子性又可以保证可见性,而volatile变量只能确保可见性,所以volatile不能保证线程安全性
这里可以用一个例子证明:
public class volatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
for(int i =0; i<1000; i++) {
increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount() >1) {
Thread.yield();
}
System.out.println(race);
}
}
运行三次输出结果:
19832
17964
18000
可以多次测试,结果都不一样