线程安全性包括以下三点:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock)
- 可见性:一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)
- 有序性:如果两个线程不能从 happens-before原则 观察出来,那么就不能保证他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(happens-before原则)
CAS (compare and swap)
CAS是JUC并发包的核心实现,本质是一种乐观锁,自旋锁,当多线程同时对某资源进行操作时,只能有一个线程操作成功,但不会阻塞其他线程而是通知它们操作失败,最底层调用的是native方法.
该算法有三个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当A和V的值相同时,才将内存值V修改为B,否则什么都不做.
相关应用: **自旋锁,**CAS操作失败时一直循环,直到成功为止.
在JAVA9之前,JUC的CAS操作是由Unsafe这个类来完成的,但是在java9之后变成了VarHandle这个类,主要是出于安全性和可移植性考虑.
Atomic包
在并发环境中,共享资源不能使用int,long double等基本类型,而是使用AtomicXXX等类,例如AtomicInteger,AtomicLong等的incrementAndGet ()方法.
AtomicXXX -> Unsafe类的compareAndSwapInt.
AtomicLong与LongAdder
Atomic包的CAS操作在竞争激烈的环境对于CPU的消耗过大,因此java8中新增了LongAdder,核心实现思想是热点分离.
原理: 高并发时将对单一变量的CAS操作分散为对cells数组中多个元素的CAS操作(通过hash判定操作哪个元素),取值时进行求和;低并发时仅对base变量(初始值V)进行CAS操作.
优缺点:
- AtomicLong操作简单,适合并发度低或者对结果严谨的环境.
- LongAdder操作复杂,适合高并发的环境,但是可能会出现统计有偏差的情况.
AtomicReference
作用是原子性更新对象引用.
AtomicIntegerFieldUpdater
作用是原子性更新该类的某个指定字段的值,该字段需要用volatile修饰且提供get方法.
CAS的ABA问题
某资源的初始值的A,一个线程把它修改为B,另一个线程又把它修改回A,这样该资源会被CAS判定为未修改过,但实际上却被修改过了.解决方法是AtomicStamperReference类,为资源增加版本标记,每次操作将版本号加1,进行CAS比较时不仅比较数值,也比较版本号.
两种锁:synchronized和Lock
区别在于:
- 前者是关键字,后者为接口
- synchronized在发生异常时,会自动释放锁,不会又死锁问题,而Lock不会释放,建议在finally中释放锁
- Lock的子类包括可重入锁ReentrantLock,读写锁ReentrantReadWriteLock等,可通过lockInterruptibly()方法实现可中断性,通过构造方法来实现公平性等.
- Lock能起到的作用更多,例如中断等待锁的线程,获知是否成功获取锁.
- ReentrantReadWriteLock可通过读写分离提高并发效率,而synchronized在高并发下效率较低.
可重入锁
在Java中,synchronized和ReentrantLock属于可重入锁,即同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁,不会造成死锁.
偏向锁/轻量级锁/重量级锁
这几个是描述锁的状态,是JVM底层与synchronized的实现有关的概念,从1.6开始优化synchronized而引入的锁机制,原理比较复杂,目前也不太需要关心,使用JVM默认的策略即可.
可见性-volatile
通过加入内存屏障和禁止重排序优化来实现
- 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存中.(写完后及时保存)
- 对volatile变量读操作时,会在读操 作前加入一条load屏障指令,从主内存中读取共享变量.(读之前先更新)
有序性 - happens-before原则
共8条,意思大概是A操作必须先于B操作,太长不想记.
线程封闭
- ad-hoc 线程封闭 : 程序控制实现,最糟糕,忽略
- 堆栈封闭 : 局部变量,无并发问题
- ThreadLocal : 如果多线程需要操作的是全局变量而非线程内部的变量,可以用threadlocal进行封闭.应用有数据库连接池,获取connection
StringBuffer与StringBuilder与String
string为final类,绝对的线程安全.但是每个string都是一个新的对象,内存消耗大.
string重写了equals和hashcode方法,两个字符串比较内容而不是地址.
单线程下的效率builder>buffer>string,多线程下修改字符串首选buffer.
StringBuffer如何实现线程安全?
在append等方法上使用synchronized修饰.
线程不安全类与写法
- StringBuilder -> StringBuffer
- SimpleDateFormat -> joda-time or java8的日期api
- ArrayList,HashSet,HashMap等
- 非原子性操作,例如先检查再执行: if( condition(a) ) { handle(a); }
同步容器与并发容器
同步容器包括HashTable,Vector等,是java早期对于线程安全的解决方案,能保证线程安全,但不保证并发效率.如今不推荐使用
并发容器包括JUC包下的ConcurrentXXX等工具类