Java 线程安全
什么是线程安全?
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
一:基本概念
- 共享资源:能够被多个线程同时访问的资源
- 竞态条件:当两个线程竞争统一资源时,如果对资源的访问顺序敏感,就称存在静态条件
- 临界区:导致竞态条件发生的代码区
原子性
一个操作(包含多个子操作)要么全部执行,要么全部不执行。
例如,银行转账,A转给B 1000元,那么需要执行A-1000,B+1000,这操作必须是原子的
可见性
当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能立刻看到。
由于CPU和内存之前会有几层缓存,所以会涉及到缓存更新算法,由操作系统或硬件层面支持。
顺序性
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。
这里就涉及到JVM为了提高性能,会对部分Java代码进行重排序,所以程序执行的顺序不一定会按照代码的先后顺序执行。
但是JVM会保证在单线程情况下,程序最终的执行结果和代码顺序执行一致。
二:线程安全级别
1.不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
2.绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
3.相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
4.线程非安全
ArrayList、LinkedList、HashMap等都是线程非安全的类
三:线程安全及解决方案
可能会存在线程安全的点:
- 对可变共享资源的操作
- 当前的锁对象
例如:
public class Counter {protected long count = 0;public void add(long value){this.count = this.count + value;}
}
如果有两个线程A、B同时对Counter类的同一个实例上执行add方法,A、B执行顺序是不可控的。
- 从内存中读取count值到CPU缓存
- CPU执行+value操作,并赋值给count
- 写回count值到内存
由于上述过程并不是原子性的,所以A、B线程交叉执行时,很可能就会出问题。
Java如何保证原子性
锁和同步
用来保证Java操作的原子性的方式是锁和同步。
使用锁,能够保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行锁所保护的代码。
使用同步(synchronized):
- synchronized修饰静态方法,锁的是该类的Class对象
- synchronized修饰非静态方法,锁的是该类的对象实例
- synchronized
CAS(Compare and swap)
基础类型变量自增(i++)虽然代码只有一行,但是并不是原子操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下:
public static void main(String[] args) throws InterruptedException {final AtomicInteger atomicInteger = new AtomicInteger(0);int count =1000;final CountDownLatch countDownLatch = new CountDownLatch(count);for(int i=0;i<count;i++) {Thread thread = new Thread(new Runnable() {public void run() {atomicInteger.incrementAndGet();countDownLatch.countDown();}});thread.start();}//等待所有线程执行完成countDownLatch.await();//此处输出必等于countSystem.out.println(atomicInteger.get());}
Java如何保证可见性
Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即更新到内存中,并且将其他缓存中对该变量的缓存置为失效。因此,其他线程读取该值时,只能从主内存中读取,从而保证可见性。
Java如何保证顺序性
上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。
Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。
synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。
除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。
happens-before原则(先行发生原则)
- 传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
- 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
- volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
- 程序次序规则:一个线程内,按照代码顺序执行
- 线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
- 线程终结原则:线程的终止检测后发生于线程中其它的所有操作
- 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
- 对象终结规则:一个对象构造先于它的finalize发生