
https://www.pdai.tech/md/java/thread/java-thread-x-theorty.html
不想看书,这个文章是基于上面文章的再总结。
重排序
三种类型:编译器优化的重排序,指令级并行的重排序,内存系统的重排序。
编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
指令级并行的重排序:现代处理器都有采用指令集秉性技术(ILP),这种技术将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
内存系统的重排序:由于处理器使用缓存和读写缓冲区,使得加载和存储操作可能在乱序执行。
从Java源代码到最终实际执行指令序列,会经历三种重排序。
JMM的编译器重排序规则会禁止特定类型的编译器重排序;
JMM的处理器重排序规则会要求Java再生成指令序列时,插入特定的内存屏障(memory barriers,Intel为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
Java如何解决并发问题:JMM(Java内存模型)
JMM本质:Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
理解1:
volatile,synchronized和final;
Happens-Before规则。
理解2:可见性、有序性、原子性。
原子性
即,不可再分的操作
1 | x = 10; // 将10赋值给x 为原子性操作 |
可见性
在程序运行过程中,对于一个变量:
使用时,将变量从“主存”放到“工作内存”,访问/赋值操作优先对工作内存的值进行操作,而不是主存。
在多线程下,不同线程使用不同的工作内存,可能导致变量的更新无法及时同步导致错误。
在java中,将变量用volatile修饰,可以实现变量的可见性。
有序性
即,代码有序执行,禁止指令重排。
Happens-Before规则
JMM规定了先行发生原则。(即,定义了一些需要正确执行的规则)
单一线程原则 Single Thread Rule
在一个线程内,程序前面的操作先行于发生于后面的操作;
管程锁定规则 Monitor Lock Rule
一个unlock操作先于对同一个锁的lock操作;
volatile变量规则 Volatile Variable Rule
对于一个Volatile变量,写操作先于读操作执行。
线程启动规则 Thread Start Rule
Thread对象的start()方法调用先于此线程的每一个动作。
线程加入规则 Thread Join Rule
Thread对象的结束先于join()方法返回。
线程的结束先于对线程对象的join()方法返回。
线程中断规则 Thread Interruption Rule
对线程的interrupt()方法先于线程对中断的检测;
也就是interrupt()先于interrupted()
对象终结规则 Finalizer Rule
一个对象的构造函数执行先于finalize()方法。
传递性 Transitivity
若操作A先于操作B,操作B先于操作C,则操作A先于操作B。
线程安全不是一个非真即假的命题
线程安全不是一个非真即假的命题,按照共享数据安全强弱顺序可以分为:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
不可变
不可变的对象一定是线程安全的。多线程环境下,尽量使对象不可变,来满足线程安全。
实现不可变:
final、String、枚举类型
Nubmer
部分子类,如Long和Double等,BigInteger
和BigDecimal
等;注意:
AtomicInteger
和AtomicLong
是可变的。对于集合类型,可以使用
Collections.unmodifiableXXX()
方法来实现一个不可变集合。1
2
3
4
5
6
7
8
9public class UnmodifiableMap {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>(){{
put("name","ZHangsan");put("age","12");
}};
Map<String,String> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("height","195");
}
}1
2
3
4
5Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at com.xxx.UnmodifiableMap.main(UnmodifiableMap.java:13)
Process finished with exit code 1
绝对线程安全
不论运行环境如何,调用者都无需做额外的操作;
相对线程安全
相对线程安全要求保证对这个对象单独的操作是线程安全的,而对于一系列连续的操作需要额外的同步手段来保证正确性。
如:对于Vector的使用,若其中有10个对象,此时有一个线程对Vector进行遍历,另一个线程进行remove,对于第一个线程就会出现
ArrayIndexOutOfBoundsException
错误。此时就需要额外的同步手段来保证正确性。
线程兼容
对象本身不是线程安全的,但可以通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用。
线程对立
线程对立指的是不论调用端如何采取同步措施,都无法在多线程环境中并发使用的代码。
线程安全的实现
互斥同步
synchronized
和ReentrantLock
互斥同步最主要的问题就是线程阻塞和唤醒带来的性能问题,因此互斥同步也称为阻塞同步。
互斥同步是一种悲观的并发策略,无论共享数据是否会真的出现竞争,它都要进行加锁。
非阻塞同步
非阻塞同步,也就是CAS。Compare-And-Swap,CAS,比较并交换。
随着硬件指令集的发展,硬件开始支持一些原子性操作,如:CAS,比较并交换。CAS指令需要有3个操作数,分别是内存地址V,预期值A和新值B,当执行操作时,当V等于A,则将V的值更新为B,否则不断尝试。
非阻塞同步是一种基于冲突监测的乐观并发策略。在多线程下,需要进行操作时,先进行操作,如果没有其他线程争用共享数据,则操作成功,否则不断尝试,直到成功为止。
AtomicInteger
中的CAS
在JUC包下有AtomicInteger
就使用了Unsafe类的CAS操作。
首先是使用:
1 | public static void main(String[] args) { |
1 | public final int getAndIncrement() { |
1 | //获得并设置 var1为变量 var2为期望值,var4为目标值 |
1 | public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); |
ABA问题
如果一个变量在初次读取的时候是A值,此时另一个线程将值改为B,又改为A,CAS操作就会认为该值并没有被改变过,这样就会影响CAS操作的正确性。
JUC提供了一个带有标记的原子引用类AtomicStampedReference来解决这个问题,AtomicStampedReference为变量添加了版本功能,当值发生改变,版本就会更新,通过检测版本,就可以观察到变量是否被改变。
在现实中大部分情况下,ABA问题都不会影响并发的正确性,
如果要解决ABA问题,改用传统的互斥同步可能比原子类更高效。
无同步方案
要保证线程安全,并不是一定就要同步。
如果方法本来就不涉及共享数据,则不需要同步措施去保证正确性。
栈封闭
局部变量线程私有,多个线程访问同一个方法的局部变量,不会出现线程安全问题。
线程本地存储
java.lang.ThreadLocal
可以为每个线程实现线程本地存储功能,每个线程访问ThreadLocal
中的对象都会调用到只属于当前线程的对象。ThreadLocal理论上并不是用来解决多线程并发问题的。
其他注意事项:ThreadLocal有内存泄漏情况,应该尽可能在每次使用ThreadLocal后手动调用remove(),以避免出现ThreadLocal内存泄露风险。
可重入代码
若一个程序或子程序可以安全地被并行执行,则称为可重入。
(e..栈封闭?)
其他
- 虚拟机会优化掉很大一部分不必要的加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程的需要唤醒等操作。