Java线程安全
Java多线程读写原则JMM
JMM(Java Memory Model),是一种基于计算机内存模型(定义了共享内存系统中多线程程序读写操作行为的规范),屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性。
原子性:
对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。
可见性:
多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
有序性:
程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。
线程安全的本质
多线程工作时,当子线程A,B同时需要更正主内存的共享变量X,那么主内存的共享变量数据会被破坏,此时主内存的共享变量不是线程安全的。
线程非安全案例:
下方代码中两个子线程同时对主线程中的变量X操作,理论上每次执行,都会有一个线程的输出结果是2000000,但实际上执行结果都小于2000000。
public class ThreadDemo { private int x = 0; private void count() { x++; } public void runTest() { new Thread() { @Override public void run() { for (int i = 0; i < 1_000_000; i++) { count(); } System.out.println("final x from 1: " + x); } }.start(); new Thread() { @Override public void run() { for (int i = 0; i < 1_000_000; i++) { count(); } System.out.println("final x from 2: " + x); } }.start(); } public static void main(String[] args) { new ThreadDemo().runTest(); } }
理论输出结果:
final x from 1: 1000000
final x from 2: 2000000
实际输出结果:
final x from 1: 989840
final x from 2: 1872479
产生上述误差的原因是,当第一个线程执行到一半的时候,可能会发生线程切换,1000000次的i++还未执行完全,切换期间,因为另一个线程也在操作x,等再次切回来的时候x的值发生了变化,导致输出结果产生了误差。这种现象即线程不安全现象。
Java线程安全策略
1.Synchronized关键字(悲观锁)
原子性
Synchronized 保证方法内部或代码块内部资源(数据)的互斥访问。即同一时间、由同一个 Monitor(监视锁) 监视的代码,最多只能有一个线程在访问。
被 Synchronized 关键字描述的方法或代码块在多线程环境下同一时间只能由一个线程进行访问,在持有当前 Monitor 的线程执行完成之前,其他线程想要调用相关方法就必须进行排队,知道持有 持有当前 Monitor 的线程执行结束,释放 Monitor ,下一个线程才可获取 Monitor 执行。
可见性
保证多线程环境下对监视资源的数据同步。即任何线程在获取到 Monitor 后的第一时 间,会先将共享内存中的数据复制到线程的缓存中;任何线程在释放 Monitor 的第一 时间,会先将缓存中的数据复 制到共享内存中。
有序性
Synchronized 的原子性保证了由其描述的方法或代码操作具有有序性,同一时间只能由最多只能有一个线程访问,不会触发 JMM 指令重排机制。
2.Volatile 关键字
保证被 Volatile 关键字描述变量的操作具有可见性和有序性(禁止指令重排)
3.java.util.concurrent.atomic
java.util.concurrent.atomic 包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等类。使用这些类来声明变量可以保证对其操作具有原子性来保证线程安全。实现原理上与 Synchronized 使用 Monitor(监视锁)保证资源在多线程环境下阻塞互斥访问不同,java.util.concurrent.atomic 包下的各原子类基于 CAS(CompareAndSwap) 操作原理实现。
CAS(乐观锁)简述
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作,该操作会一直循环直至新值修改成功。
CAS优点:
CAS是一种乐观锁策略,多线程环境下各线程访问共享变量不会加锁阻塞排队,线程不会被挂起。
CSA缺点:
1.循环时间长开销很大。cas通常是配合无限循环使用的,若一直更新操作一直失败,会一直进行尝试,长时间的循环会占用较大的资源,给cpu带来很重的负担。
2.只能保证一个变量的原子操作。(可以通过将多个变量封装成对象的方式来实现多变量的原子操作)
3.ABA問題。(解决方案:Java并发包提供的原子引用类“AtomicStampedReference”通过控制变量值的版本号来确保cas的正确性。)
4.Lock
Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock 提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作。
线程安全策略总结:
1. 保证共享资源在同一时间内,只有一个线程对其做操作。(原子性,有序性)
2. 及时将修改后的资源更新到主内存中,保证所有线程看到的资源一致。(可见性)
Java中线程安全的集合
参考:https://blog.csdn.net/lixiaobuaa/article/details/79689338
https://www.cnblogs.com/linliquan/p/11606297.html
文章参考连接:
https://blog.csdn.net/v123411739/article/details/79561458
https://zhuanlan.zhihu.com/p/73899015

浙公网安备 33010602011771号