线程安全进阶
一、什么是线程安全的
线程安全就是多线程同时访问一个对象时,需要保证这个对象,在多线程操作下获得的结果是正确的,或者说这个对象在多线程同时访问下,内部状态是正确的。
二、Java中线程安全
Java中多线程操作一个共享对象才会有线程安全问题。
1. 不可变
不可变的对象一定是线程安全的,不管是对象的实现方法还是对象方法的调用方都不需要考虑线程安全的问题。
final关键字
使用final关键字修饰的基本数据类型(int、char、long等),通过final关键字修饰,表示一个常量。
使用final关键字修饰一个对象,在多线程交替执行下需要保证对象内部状态不变。
2. 绝对线程安全
绝对的线程安全就是在多线程下不需要考虑额外的同步保护机制,在多线程操作下能获得正确的结果。
Java中提供了许多线程安全的类供使用,但是,大多数多不是绝对的线程安全。
比如:Vector是Java提供的一个线程安全的容器。
public class ThreadMain { private static Vector<Integer> mSafeVar = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { mSafeVar.add(i); } Thread removeThread = new Thread(() -> { for (int i = 0; i < mSafeVar.size(); i++) { mSafeVar.remove(i); } }); Thread printThread = new Thread(() -> { for (int i = 0; i < mSafeVar.size(); i++) { System.out.println((mSafeVar.get(i))); } }); removeThread.start(); printThread.start(); while (Thread.activeCount() > 20) { Thread.yield(); } } } }
结果:
Exception in thread "Thread-8505" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 10 at java.util.Vector.get(Vector.java:751) at example.ThreadMain.lambda$main$1(ThreadMain.java:24) at java.lang.Thread.run(Thread.java:748)
正常Vector是线程安全的容器,Vector.size()、Vector.get()、Vector.remove()方法是同步方法,但是,在多线程操作下不该出错的,所以,上面程序在多线程下依然是线程不安全的。
修改如下:
public class ThreadMain { private static Vector<Integer> mSafeVar = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { mSafeVar.add(i); } Thread removeThread = new Thread(() -> { synchronized (mSafeVar) { for (int i = 0; i < mSafeVar.size(); i++) { mSafeVar.remove(i); } } }); Thread printThread = new Thread(() -> { synchronized (mSafeVar) { for (int i = 0; i < mSafeVar.size(); i++) { System.out.println((mSafeVar.get(i))); } } }); removeThread.start(); printThread.start(); while (Thread.activeCount() > 20) { Thread.yield(); } } } }
按线程安全的定义,绝对线程安全调用方是不需要做同步机制的。
3. 相对线程安全
操作对象的单次调用是线程安全的,在多线程按某种规则顺序调用需要在调用方保证线程调用安全。如上面的Vector,相反ConcurrentHashMap就是绝对线程安全的。
三、实现线程安全的方法
1. 互斥同步,又称堵塞同步
互斥同步(Mutual Exlusion & Synchronization)是指多线程并发访问同一个共享变量时,同一时刻共享变量只能被一条线程访问并使用。互斥同步的实现方式有:临界区、互斥量、信号量。
互斥同步属于一种悲观的并发策略,认为只要不使用保证线程安全的机制(互斥同步保证线程安全机制是指加锁),就一定会出错。无论访问共享变量是否存在竞争,都需要同步机制保证访问共享变量的安全性。那么,不可避免的对共享变量加锁。在多线程环境下,互斥同步实现是共享变量在同一时间只有一条线程访问并使用,其他线程处于堵塞等待状态。在主流的Java虚拟机的线程是用户线程,Java虚拟机的线程需要映射到操作系统的内核线程上。线程的堵塞和唤醒需要操作系统调度完成,这样,需要在用户态和内核态间切换,占用内核的执行时间。
在Java中最常用的互斥同步方式是互斥量,比较常用的就是synchronized和ReentrantLock(重入锁)。
2. 非堵塞同步
非堵塞同步是基于冲突检测的乐观并发策略,也就是说,访问共享变量不管风险,先进性操作,如果,共享变量不存在竞争,访问共享变量操作成功。反之,共享变量被竞争,操作失败,需要额外的补充机制,补充机制就是重复访问共享变量,直到共享变量没有被竞争为止。这种机制就是非堵塞同步。
非堵塞同步需要硬件指令集的发展,常用硬件指令集:
-
- 测试并设置(Test-and-Set)。
- 获取并增加(Fetch-and-Increment)。
- 交换(Swap)。
- 比较并交换(Compare-and-Swap,又称CAS)。
- 加载链接/条件存储(Load-Linked/Store-Conditional)。
在Java中通过原子类实现同步:java.util.concurrent.atomic类库。