当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
不可变(Immutable)的对象一定是线程安全的。
如果共享数据是一个基本数据类型,定义时使用final关键字修饰可保证它不可变。
如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。其中最简单的是把对象中带有状态的变量都声明为final,这样在构造函数结束后,它就是不可变的。
Java API中符合不可变要求的类型:java.lang.String/java.lang.Number部分子类,枚举类。
绝对安全的线程的类,完全符合线程安全的定义,但在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全,如Vector。
举例:
public class MainTest {
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) throws Exception {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThred = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThred = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
});
removeThred.start();
printThred.start();
while (Thread.activeCount() >= 20);
}
}
}
这段代码会报错。虽然 vector 是线程安全的,但是可能出现都进入了for循环的最后一个元素,但是前者刚刚 remove 后者就是 get 出错。解决方案是在 for 循环外面加 synchronized
是通常意义上的线程安全,它需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保证措施。
线程兼容是指对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中是可以安全使用的。Java API中的大部分的类都是属于线程兼容的,如ArrayList和HashMap等。
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中使用的代码。线程对立这种排斥多线程的代码是很少出现的,通常都是有害的,应当避免。如Thread类的suspend()和resume()方法。如果两个线程同时持有一个线程对象,两个线程并发对该线程对象执行suspend()和resume()方法,无论是否采用了同步,都存在死锁风险。
互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。
互斥同步最主要的问题是进行线程阻塞和唤醒时带来的性能问题,这种同步也称为阻塞同步Blocking Synchronization。从处理问题的方式来说,互斥同步属于一种悲观的并发策略:总是认为只要不去做正确的同步措施(加锁),那就肯定会出问题。无论共享数据是否真的会出现竞争,它都进行加锁、用户态核心态转换、维护锁计数器、检查是否有被阻塞的线程需要唤醒等操作。
非阻塞同步是一种基于冲突检测的乐观并发策略的同步操作:先进行操作,如果没有其他线程争用共享数据,那操作就成功;如果共享数据有争用,产生了冲突,就在采取其他的补偿措施(比如不断的重试,直到成功)。这种乐观并发策略的很多实现都不需要把线程挂起,因此称为非阻塞同步。
乐观并发策略需要硬件指令集的发展,因为上述过程中的操作和冲突检测这两个步骤需要具备原子性,而这种原子性保证如果使用互斥手段实现就失去意义,所以只能靠硬件通过一条处理器指令来完成这种从语义上看起来需要多次操作的行为。这里的非阻塞同步进行的操作主要涉及CAS(Compare And Swap)这条指令。使用该指令完成的操作具备原子性,称为CAS操作。
CAS指令执行时,需要3个操作数:内存位置(V)、旧的预期值(A)、新值(B)。当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新。但是无论是否更新了V的值,都会返回V的旧值。
CAS的逻辑漏洞——ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,此时并不能说它的值没有被其他线程修改过,有可能在这期间它的值先被改成了B,后又被改为了A,而CAS操作就会认为它从来没有改变过。大部分情况下ABA情况不会影响程序并发的正确性,如果需要解决ABA问题(JDK通过引入AtomicStampedReference来保证CAS的正确性),改用传统的互斥手段可能会比原子类更高效。
AtomicStampedReference多了一个 stamp 参数。字面上是时间戳,实际上它可以是任何一个整数。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新 stamp
同步只是保证共享数据争用时的正确性的手段,要保证线程安全,并不是一定就要进行同步。