并发系列之线程安全性

并发系列之线程安全性

前言

这篇博客开始算是《Java并发编程实战》的总结,其实这本书,不夸张的讲前几章我以及看了四遍以上了,但是一直为什么没有真真正正的看完这本书呢?每次看完几章可能被其他新的技术诱惑到了,就跑去研究别的去了,这本书就又放下了,再回来看时,前面的又忘了,循环往复。java中深感线程的强大,现在以博客的形式记录自己的学习,还是每周坚持最少更新一篇。

线程安全类和线程安全程序

二者的含义基本相同。线程安全的程序是否完全由线程安全类构成?答案是否定的,完全由线程安全类构成的程序并不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类。

什么是线程安全性

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

ps:无状态对象一定是线程安全的

原子性

竞态条件:某个计算的正确性取决于多个线程的交替执行的时序。(线程的时序不同,产生的结果可能会不同)

“先检查后执行”,即通过一个可能失效的观测结果来决定下一步的操作。

首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。常见的就是(Lazy Singleton)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 不要这么做
* describe: 懒汉式单例模式
* 优点:只有在需要使用LazySingleton1对象时,才真正生成一个LazySingleton1对象
* 缺点:会因为某些Java 平台内存模型允许无序写入,使得getInstance方法可能返回
* 一个尚未执行构造函数的对象
* Created by tianc on 2017/4/15.
*/
public class LazySingleton {
private static LazySingleton lazyInstance = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(lazyInstance == null){
synchronized (LazySingleton.class){
if(lazyInstance == null){
lazyInstance = new LazySingleton();
}
}
}
return lazyInstance;
}
}

LazySingleton

“读取-修改-写入”,基于对象之前的状态来定义对象状态的转换。即使是volatile修饰的变量,在多线程的环境里面进行自增操作,同样会发生竞态条件,所以volatile不能保证绝对的线程安全(360面试问题)。

  引用书中定义:假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

加锁机制

在线程安全的定义中,多个线程间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。当不变性条件中涉及多个变量时,各个变量之间并不是互相独立的,一个变量发生变化会对其他变量的值产生约束。因此,一个变量发生改变,在同一个原子操作里面,其他相关变量也要更新。

要保持状态的一致性,就需要在单个原子操作中跟新所有相关的状态变量。

内置锁:同步代码块(Synchronized Block)包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。关键字Synchronized修饰方法就是一种同步代码块,锁就是方法调用所在的对象,静态的Synchronized方法以Class对象作为锁。内置锁或监视锁就是以对象作为实现同步的锁。

1
2
3
synchronized (lock) {
//访问或修改由锁保护的共享状态
}

Java内置锁,进入的唯一途径是执行进入由锁保护的同步代码块或方法。它相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。

重入锁:当一个持有锁的线程再次请求进入自己持有的锁时,该请求会成功。”重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方式,为每个锁关联一个计数器和线程持有者。

如果内置锁不是可重入的,那么下面这段代码将发生死锁

1
2
3
4
5
6
7
8
9
10
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString + ": calling doSomething");
super.doSomething();
}

用锁来保护状态

由于锁能使其保护的代码路径以串行形式访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。

对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都要使用同一个锁来保护/同步。

当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或者控制台IO),一定不要持有锁。