线程安全性
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式
,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同
,这个类都能表现出正确的行为
,那么就称这个类是线程安全的。
1. 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行访问。
Atomic包:
- AtomicXXX:CAS、Unsafe.compareAndSwapInt
- AtomicLong、LongAdder
- AtomicReference、AtomicReferenceFieldUpdater
- AtomicStampReference:CAS的ABA问题
原子性 - synchronized(同步锁)修饰代码块
:大括号括起来的代码,作用于调用的对象修饰方法
:整个方法,作用于调用的对象修饰静态方法
:整个静态方法,作用于所有对象修饰类
:括号括起来的部分,作用于所有类
原子性 - 对比synchronized
:不可中断锁,适合竞争不激烈,可读性好Lock
:可中断锁,多样化同步,竞争激烈时能维持常态Atomic
:竞争激烈时能维持常态,比Lock性能好;只能同步一个值
2. 可见性:一个线程对主内存的修改可以及时的被其他线程观察到。
导致共享变量在线程见不可见的原因:
- 线程交叉执行
- 冲排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主内存之间急事更新
synchronized、volatile
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新制刷新到主内存
- 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(
注意:加锁与解锁是同一把锁
)
volatile - 通过加入内存屏障
和禁止重排序
优化来实现
- 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
- 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
volatile变量在每次被线程访问时,都强迫从主内存中读取该变量的值,而当变量的值发生变化时,又会强迫线程将该变量最新的值强制刷新到主内存,这样一来,任何时候不同的线程总能看到该变量的最新值
3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。volatile、synchronized、Lock。【volatile变量规则】
:对一个变量的写操作先行发生于后面对这个变量的读操作。(如果一个线程进行写操作,一个线程进行读操作,那么写操作会先行于读操作。)【传递规则】
:如果操作A先行于操作B,而操作B又先行于操作C,那么操作A就先行于操作C。【线程启动规则】
:Thread对象的start方法先行发生于此线程的每一个动作。【线程中断规则】
:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。【线程终结规则】
:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()方法的返回值手段检测到线程已经终止执行。【对象终结规则】
:一个对象的初始化完成先行发生于他的finalize()方法的开始。
发布对象
发布对象
:使一个对象能够被当前范围之外的代码所用。对象溢出
:一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。
安全发布对象
在静态初始化函数中初始化一个对象
将对象的引用保存到volatile类型域或者AtomicReference对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中
/**
* 懒汉模式
* 双重同步锁单例模式
* @author Guo
*
*/
public class SingletonExample1 {
private SingletonExample1(){
}
// volatile禁止指令重排
private volatile static SingletonExample1 instance = null;
public static SingletonExample1 getInstance(){
if(instance == null){
synchronized(SingletonExample1.class){
if(instance == null){
instance = new SingletonExample1();
}
}
}
return instance;
}
}
避免并发两种方式
- 不可变对象
线程封闭
线程封闭: 把对象封装到一个线程里,只有这一个线程可以看到这个对象,即使这个对象不是线程安全也不会出现任何线程安全问题,因为只在一个线程里
堆栈封闭
:局部变量,无并发问题。
栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。ThreadLocal线程封闭
:比较推荐的线程封闭方式。
【ThreadLocal结合filter完成数据保存到ThreadLocal里,线程隔离。】通过filter获取到数据,放入ThreadLocal, 当前线程处理完之后interceptor将当前线程中的信息移除。
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭
线程不安全类与写法
【线程不安全】
:如果一个类类对象同时可以被多个线程访问,如果没有做同步或者特殊处理就会出现异常或者逻辑处理错误。
【1. 字符串拼接】:
StringBuilder(线程不安全)、
StringBuffer(线程安全)
【2. 日期转换】:
SimpleDateFormat(线程不安全,最好使用局部变量[堆栈封闭]保证线程安全)
JodaTime推荐使用
(线程安全)
【3. ArrayList、HashSet、HashMap等Collections】:
ArrayList(线程不安全)
HashSet(线程不安全)
HashMap(线程不安全)
【**同步容器**synchronized修饰】
Vector、Stack、HashTable
Collections.synchronizedXXX(List、Set、Map)
【**并发容器** J.U.C】
ArrayList
->CopyOnWriteArrayList
:(读时不加锁,写时加锁,避免复制多个副本出来将数据搞乱)写操作时复制,当有新元素添加到CopyOnWriteArrayList中时,先从原有的数组中拷贝一份出来,在新的数组上进行写操作,写完之后再将原来的数组指向新的数组。
HashSet、TreeSet
-> CopyOnWriteArraySet、ConcurrentSkipListSet
:HashMap、TreeMap
-> ConcurrentHashMap、ConcurrentSkipListMap
:
相比ConcurrentHashMap,ConcurrentSkipListMap具有如下优势:
- ConcurrentSkipListMap的存取速度是ConcurrentSkipListMap的4倍左右
- ConcurrentSkipListMap的key是有序的
- ConcurrentSkipListMap支持更高的并发(它的存取时间和线程数几乎没有关系,更高并发的场景下越能体现出优势)
安全共享对象策略 - 总结
线程限制
:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改共享只读
:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它线程安全对象
:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它被守护对象
:被守护对象只能通过获取特定锁来访问