一.Java语言中的线程安全
可以将Java语言中各种操作共享的数据分为五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
JDK5以后,不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全措施。
只要一个不可变的对象被正确的构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。
如果多个线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不变的。
如果共享数据是一个对象,由于Java语言展示还没有提供值类型的支持,那就需要对象自行保证其行为不会其状态产生任何影响才行。例如:java.lang.String类的对象实例,它是一个典型的不可变对象,用户调用它的subString()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有多种,最简单的就是把对象里面带有状态的变量都声明为final,这样构造函数结束后,它就是不可变的
在Java类库API中符合不可变要求的类型:String 枚举类型及java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。但同为Number子类型的原子类AtomicInteger和AtomicLong是可变的,因为J.U.C包的原子类专门用来处理并发的,value是使用了volatile修饰的,如果不可变就没法在并发下安全的增加或者减少了
2.线程绝对安全
不管运行环境如何,调用者都不需要任何额外的同步措施,Brian Goetz给出的线程安全定义。
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程全。
例如:java.util.Vector是一个线程安全的容器,因为它的add()、get()和size()等方法都是被synchronized修饰,尽管这样效率不高,但保证了具备原子性、可见性和有序性。
不过,即使所有方法都用sychronized修饰,也不意味着调用它的时候就永远不再需要同步手段,比如 一个线程remove,一个线程get就可能会报java.lang.ArrayIndexOfBoundException。
public class VectorTest { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } Thread removeThread = new Thread(() -> { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } }); Thread getThread = new Thread(() -> { for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } }); removeThread.start(); getThread.start(); // 不要同时产生过多线程,否则会导致操作系统假死 while (Thread.activeCount() > 100) {
return;
} } }
运行结果如下:我运行了多次出现的,并不一定会出现,次数多了一定会出现
7
0
5
0
Exception in thread "Thread-17" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1
at java.util.Vector.get(Vector.java:748)
at VectorTest.lambda$main$1(VectorTest.java:19)
at java.lang.Thread.run(Thread.java:748)
尽管这里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境中,如果不再方法调用端做额外的同步措施,使用这段代码仍然是不安全的。因为如果一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用,
再用i访问数组就会抛出一个ArrayIndexOutOfBoundException异常。如果需要保证上面的代码正常运行,我们做一些改进 如下:只需要再线程的内部加上同步关键字即可
Thread removeThread = new Thread(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }); Thread getThread = new Thread(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } } });
3.相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对一个对象单次的操作是线程安全的,我们在调用的时候不需要额外的保障措施。上面的绝对安全线程里的代码都属于相对线程安全
在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector,HashTable 、Collections的sychronizedColletion()方法包装的集合等。
4.线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境汇总可以安全地使用。平常说一个类不是线程安全的,就是指这种情况。
Java类库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类 ArrayList 和 HashMap 等。
5.线程对立
线程对立是指调用端不管是否采取了同步手段,都无法在多线程环境中并发使用代码。由于Java语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的 suspend() 和 resume() 方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发情况下,无论调用时是否进行了同步,目标线程都存在死锁风险——例如 suspend() 中断
的线程就是是即将要执行 resume() 的那个线程,那就肯定要产生死锁了。
二.线程安全的实现方法
1.互斥同步(Mutual Exclusion & Synchronization)
同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条(或是一些,当使用信号量的时候)线程使用。
互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方法。互斥是因,同步是果;互斥是方法,同步是目的
临界资源:是一次仅允许一个进程使用的共享资源
临界区:指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性
互斥量:互斥锁
信号量:
关于sychronized关键字的结论:
- 被sychronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
- 被sychronized修饰的同步块在吃有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
- sychronized是可以重入锁
- 可重入锁:如果线程获取了锁,同步块对于同一个线程来说可可以多次重入,每次计数器加1,释放锁的时候每次计数器减1,不会出现死锁
- sychronized是块结构的同步语法
java.util.concurrrent.locks.Lock是Java的另一种互斥同步手段,基于Lock的接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,在类类库层面实现同步。
重入锁(ReentrantLock)与sychronized一样可重入,ReentrantLock与synchronized相比增加了一些高级功能,主要有:等待可中断。可实现公平锁及锁可以绑定多个条件。
等待可中断:是指持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁:是指多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁不保证这一点,在锁别释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平的,ReentrantLock默认情况下是非公平的,
但可以通过代布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait() 跟它的 notify() 或者 notifyAll() 方法配合可以实现一个隐含的条件,如果要和多于一个条件关联的时候,就不得不额外添加
一个锁;而ReentrantLock则必须这样做,多次调用newCondition()方法即可。
满足以下条件时,在synchronized和ReentrantLock都可满足需要时优先使用synchronized:
- synchronized是在Java语言层面的同步,足够清晰,也足够简单。每个程序员都熟悉synchronized,但J.U.C中的Lock接口并非如此。因此只需要基础同步功能时,更推荐synchronized。
- Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。
- 尽管在JDK5时代ReentrantLock曾经在性能上领先过synchronized,但这已经时十多年之前的胜利了。从长远来看,Java虚拟机更容易针对synchronized来就能行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用J.U.C中的Lock的话,Java虚拟机时很难得知具体哪些锁对象是由特定线程锁持有的。
2.非阻塞同步
互斥同步面临的主要问题是进行线程阻塞和唤醒多带来的性能开销,这种同步称为阻塞同步(Blocking Synchronized)。互斥同步属于一种悲观的并发策略,其总是认为不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据
是否真的会出现竞争,它都会进行加锁(实际上虚拟机会优化掉很大一部分不必要的加锁),这将会导致用户态到核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销。
随着指令集的发展,我们已经有了另外一种选择:基于冲突检测的乐观并发策略,通俗的说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,
最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。不再需要把线程阻塞挂起,这种同步操作称为非同步阻塞(Non-Blocking Synchronized),使用这种措施的代码也常常被称为无锁(Lock-Free)编程。
CAS: 比较并进行交换,CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单的理解为变量的内存地址,用V表示)、旧的预期值(用A表示)、和准备设置的新值(用B表示)。
CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述操作是一个原子操作,执行期间不会被其他线程中断。
JDK5之后,Java类库才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt() 和 compareAndSwapLong() 等几个方法保证使用
CAS会有ABA的问题:如果一个变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A值,那就说明它的值没有被其他线程改变过吗?这是不可能的,因为在这 期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为
它重来没有被改变过。解决方案:J.U.C包提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证。
3.无同步方案
同步和线程安全没有必然联系,同步只是保障存在共享数据争用时正确性的手段。如果让一个方法本来就不涉及共享数据,那它自然就不需要任何同步 措施去保证其正确性,因此会有一些代码天生就是线程安全的。
可重入代码(Reentrant Code):又称纯代码(Pure Code)是指可以在代码执行的任何时刻中断它,转而去执行另一段代码(包括地柜调用它本身),而控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
在特指多线程的上下文语境里(不涉及信号量等因素),我们可以认为可重入代码时线程安全代码的一个真子集。 所有可重入的代码都是线程安全的,但是并非所有线程安全代码都是可重入的。
可重入代码共同特征:不依赖全局变量 、 存储在堆上的数据和公用系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等
判断代码是否具有可重入性的一个简单原则:如果一个方法的返回结果是可以预测的,只要输入相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就时线程安全的。
线程本地存储(Thread Local Storage):
ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
浙公网安备 33010602011771号