1*.怎么判断给出的类是否是线程安全的?
先行发生原则: 离散数学中曾经定义了“偏序”的概念,在Java中正是使用了这个概念。
偏序通俗的来理解就是拓扑结构,一个DAG。
如果操作A先行发生于B,那么B将能观察到A所做的所有事情(包括修改变量、发送消息、调用方法等)。
//线程A
i = 1;
//线程B
j = i;
//线程C
i = 2;
如果只考虑 A 和 B,并且保证 A 先行发生于 B,那么B的值很显然是 1。依据有两个:
1、根据先行发生的偏序关系,i 的值的改变可以被 j 观察到
2、 在线程 C 修改 i 值之前,线程 A 结束之后没有其他线程会修改
如果考虑 C 线程,j 的值会不确定。
因为线程 B 和线程 C 没有先行发生定义的偏序,意思就是 C 可以发生在 B 前,B 后的任意位置。
如果 C 发生在 A/B 之间,那么显然 j 的值就是 2 了。所以线程 B 就不具备多线程安全性。
JMM定义了一些先行发生关系来保证多线程的安全。
假如所有的操作都像 A/B 定义好了偏序关系,那么并发就不会有任何难度了。
但是为了更灵活的使用,Java 只对一些场景定了先行发生原则,所以遇到这几个规则范围内的问题,
就不需要考虑并发的各种问题,Java会自动帮我们解决。
所以,以下操作无须任何同步手段就能保证并发的成功:
程序次序规则: 一个线程内,代码的执行会按照程序书写的顺序
管程锁定原则: 对同一变量的 unlock 操作先行发生于后来的 lock 操作
volatile 变量规则: 对一个 volatile 的写操作先行发生于后来的读操作
线程启动原则: Thread 的 start() 先行发生于线程内的所有动作
线程终止原则: 线程内的所有动作都先行发生于线程的终止检测
线程中断原则: 对线程调用 interrupt() 先行发生于被中断的代码检测到是否有中断发生
对象终结原则: 一个对象的初始化操作先行发生于 finalize() 方法
传递性: A 先行发生于 B,B 先行发生于 C,那么 A 先行发生于 C
实例:
private int value = 0;
public int getValue() {
return this.value;
}
public void setValue(int value) {
this.value = value;
}
假设有 A/B 两个线程。线程 A 先(时间上的先后)调用了 setValue(2),
然后线程 B 调用了同一个对象的 getValue(),那么线程 B 收到的返回值是多少?
用先行发生原则来判断:
因为 A/B 不是一个线程,所以无法使用程序次序原则
因为没有 synchronized,所以不存在 lock/unlock 操作,无法使用管程锁定原则
没有 volatile 修饰,不能使用 volatile 变量原则
因为是两个独立的线程,所以线程启动、终止、终端原则都不能使用
因为不涉及对象的初始化和 finalize(),所以无法使用对象终结原则
因为根本就没有一个先行发生原则,所以也不能使用传递性
A/B 之间不满足所有的先行发生原则。所以 A/B 线程的操作不是线程安全的。
如果想要线程安全,必须自己去实现:
1、将 getter/setter 方法添加 synchronized 修饰,使之满足管程锁定原则
2、把 value 定义为 volatile,因为 setter 中对 value 的修改不依赖 value 的原值,所以符合 volatile 的使用场景
(一定要符合前提,如果这里方法是 value++ 就肯定不行了),然后套用 volatile 变量原则就可以保证了
时间上的先后顺序与先行发生原则之间没有太大的关系,所以我们衡量并发安全问题的时候不能受到时间顺序的干扰,
一切必须以先行发生原则为准。
2.实现线程安全的几种方法?
1、不使用单例模式,而是用多实例
2、使用锁机制 synchronized、lock 方式
3、使用 java.util.concurrent 下面的类库
隐式锁(线程同步 synchronized):同步方法,同步代码块
显示锁 Lock 和 ReentrantLock:在竞争条件下 ReentrantLock 的实现要比 synchronized 的实现更具有伸缩性,
这意味着当许多线程都竞争相同锁定时,使用 ReentrantLock 的吞吐量通常要比 synchronized 好
显示锁 ReadWriteLock 和 ReentrantReadWriteLock:提供了 readLock 和 writeLock 两种锁的操作机制,
一个用于只读操作,另一个用于写入操作。
只要没有 writer,读取锁可以由多个 reader 线程同时保持。
写入锁是独占的。读写锁使用的场景是一个共享资源被大量的读取操作而只有少量的写操作
显示锁 StampedLock:在大量都是读取、很少写入的情况下,乐观读锁可以极大的提高吞吐量,也可以减少这种情况下写的饥饿现象