并发编程-线程安全
- 1、volatile 关键字
- 1.1 作用
- 1.2 底层实现原理
- 2、synchronized
- 2.2 synchronized 用法
- 2.2 synchronized 和 volatile 的区别
- 3、Lock
- 3.1 Lock 和 synchronized 的区别
- 3.2 ReentrantLock 和 synchronized 的区别
- 4、CAS
- 4.1 CAS 执行流程
- 4.2 ABA问题
- 5、AQS(AbstractQueuedSynchronizer :用于实现各种同步器的抽象类)
- 5.1 核心思想
- 5.2 资源共享模式
1、volatile 关键字
1.1 作用
volatile
是一种关键字,用于保证多线程情况下共享变量的可见性。当一个变量被声明为 volatile 时,每个线程在访问该变量时都会立即刷新其本地内存(工作内存)中该变量的值,确保所有线程都能读到最新的值。并且使用 volatile 可以禁止指令重排序,这样就能有效的预防,因为指令优化(重排序)而导致的线程安全问题。也就是说 volatile 有两个主要功能:保证内存可见性
和禁止指令重排序
。下来我们具体来看这两个功能
例如:单例模式的双重校验锁使用volatile
保证变量的可见性以及禁止指令重排序。(指的是单例模式的懒汉模式中的私有变量要加 volatile)懒汉模式指的是对象的创建是懒加载的方式,并不是在程序启动时就创建对象,而是第一次被真正使用时才创建对象。
单例模式使用volatile
主要是为了 防止指令的重排序。从而避免多线程执行的情况下,因为指令重排序而导致某些线程得到一个未被完全实例化的对象,从而导致程序执行出错的情况
public class Singleton {
//volatile 防止指令重排 和可见性
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
//先判断对象是否已经实例化过,没有实例化才进入加锁代码
if (null == instance) {
//类对象加锁
synchronized (Singleton.class) {
//避免 singleTon== null时,第一个线程实例化后,
// 进入阻塞状态的线程被唤醒后仍会进行实例化。
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
1.2 底层实现原理
内存可见性:volatile
内存可见性主要通过 lock
前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通过 MESI
协议使其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。
指令重排序:指令重排序是通过内存屏障
来实现的。
内存屏障(Memory Barrier 或 Memory Fence)
是一种硬件级别的同步操作,它强制处理器按照特定顺序执行内存访问操作,确保内存操作的顺序性,阻止编译器和 CPU 对内存操作进行不必要的重排序。内存屏障可以确保跨越屏障的读写操作不会交叉进行,以此维持程序的内存一致性模型。
写内存屏障(Store Barrier / Write Barrier)
: 当线程写入 volatile 变量时,JMM 会在写操作前插入 StoreStore
屏障,确保在这次写操作之前的所有普通写操作都已完成。接着在写操作后插入 StoreLoad
屏障,强制所有后来的读写操作都在此次写操作完成之后执行,这就确保了其他线程能立即看到 volatile 变量的最新值。
读内存屏障(Load Barrier / Read Barrier)
: 当线程读取 volatile 变量时,JMM 会在读操作前插入 LoadLoad
屏障,确保在此次读操作之前的所有读操作都已完成。而在读操作后插入 LoadStore
屏障,防止在此次读操作之后的写操作被重排序到读操作之前,这样就确保了对 volatile 变量的读取总是能看到之前对同一变量或其他相关变量的写入结果。
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。synchronized可以保证这两个
2、synchronized
2.2 synchronized 用法
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
。
synchronized 修饰实例方法:(锁当前对象实例)
//给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 。
synchronized void method() {
//业务代码
}
** 修饰静态方法 (锁当前类)**
//进入同步代码前要获得 当前 class 的锁。
synchronized static void method() {
//业务代码
}
静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥
!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁
,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
。
修饰代码块 (锁指定对象/类)
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。
synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
总结
- synchronized关键字修饰静态方法(
static
修饰的方法)和synchronized(类.class)
代码块都是给Class类上锁 - synchronized 加到实例方法上面是给对象实例加锁。
- 不要使用 synchronized(String s) 因为 JVM 中,字符串常量池具有缓存功能。
注意:构造方法不能使用synchronized 修饰 因为构造方法本身就是线程安全的,不存在同步构造方法这一说。
2.2 synchronized 和 volatile 的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
00## 2.3 synchronized 底层实现
synchronized 底层是通过监视器
实现的。监视器的执行流程如下:
1、 线程通过 CAS(对比并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说明当前线程已经持有锁,并将 _recursions 重入次数的属性 +1。如果获取失败则先通过自旋 CAS 尝试获取锁,如果还是失败则将当前线程放入到 EntryList 监控队列(阻塞)。
2、当拥有锁的线程执行了 wait 方法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线程放入 WaitSet 待授权队列中等待被唤醒。
3、当调用 notify 方法时,随机唤醒 WaitSet 队列中的某一个线程,当调用 notifyAll 时唤醒所有的 WaitSet 中的线程尝试获取锁。
4、线程执行完释放了锁之后,会唤醒 EntryList 中的所有线程尝试获取锁。
3、Lock
3.1 Lock 和 synchronized 的区别
3.2 ReentrantLock 和 synchronized 的区别
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
1、等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
2、可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
3、可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
4、CAS
CAS 的全称是 Compare And Swap(比较与交换)
是一种轻量级的同步操作,也是乐观锁的一种实现,它用于实现多线程环境下的并发算法。CAS 操作包含三个操作数:内存位置(或者说是一个变量的引用)、预期的值和新值
。如果内存位置的值和预期值相等,那么处理器会自动将该位置的值更新为新值,否则不进行任何操作。在多线程环境中,CAS 可以实现非阻塞算法,避免了使用锁所带来的上下文切换、调度延迟、死锁等问题,因此被广泛应用于并发编程中
4.1 CAS 执行流程
CAS 执行的具体流程如下:
1、将需要修改的值从主内存中读入本地线程缓存(工作内存);
2、执行 CAS 操作,将本地线程缓存中的值与主内存中的值进行比较;
3、如果本地线程缓存中的值与主内存中的值相等,则将需要修改的值在本地线程缓存中修改;
4、如果修改成功,将修改后的值写入主内存,并返回修改结果;如果失败,则返回当前主内存中的值;
5、在多线程并发执行的情况下,如果多个线程同时执行 CAS 操作,只有一个线程的 CAS 操作会成功,其他线程的 CAS 操作都会失败,这也是 CAS 的原子性
保证。
4.2 ABA问题
所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了,这就是 ABA 问题。
解决ABA 问题就是通过带版本号的 CAS
来解决、
解决 ABA 问题的一种方法是使用带版本号的 CAS,也称为双重 CAS(Double CAS)或者版本号 CAS。具体来说,每次进行 CAS 操作时,不仅需要比较要修改的内存地址的值与期望的值是否相等,还需要比较这个内存地址的版本号是否与期望的版本号相等。如果相等,才进行修改操作。这样,在修改后的值后面追加上一个版本号,即使变量的值从 A 变成了 B 再变成了 A,版本号也会发生变化,从而避免了误判。使用 AtomicStampedReference 来解决 ABA 问题
5、AQS(AbstractQueuedSynchronizer :用于实现各种同步器的抽象类)
AQS(AbstractQueuedSynchronizer)是一个用于实现各种同步器的抽象类,是 JUC(java.util.concurrent)并发包中的核心类之一,JUC 中的许多并发工具类和接口都是基于 AQS 实现的。它提供了一种基于队列的、高效的、可扩展的同步机制,是实现锁、信号量、倒计时器等同步器的基础。
同步器:同步器指的是用于控制多线程访问共享资源的机制。同步器可以保证在同一时间只有一个线程可以访问共享资源,从而避免了多线程访问共享资源时可能出现的数据竞争和不一致性问题。Java 中的同步器包括 synchronized 关键字、ReentrantLock、Semaphore、CountDownLatch 等。
5.1 核心思想
用双向队列来保存等待锁的线程,同时利用一个 state 变量来表示锁的状态。AQS 的同步器可以分为独占模式和共享模式两种。
独占模式:同一时刻只允许一个线程获取锁,例如ReentrantLock
共享模式:同一时刻允许多个线程同时获取锁,Semaphore、CountDownLatch、CyclicBarrier
5.2 资源共享模式
AQS 资源共享模式分为两种:
独占模式:AQS 维护了一个同步队列,该队列中保存了所有等待获取锁的线程。当一个线程尝试获取锁时,如果锁已经被其他线程持有,则将该线程加入到同步队列的尾部,并挂起线程,等待锁被释放。当锁被释放时,从同步队列中取出一个线程,使其获取锁,同时将它从队列中移除,唤醒该线程继续执行。独占模式又分为公平锁和非公平锁。
公平锁
:按照线程在队列中的排队顺序,先到者先拿到锁;
非公平锁
:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
共享模式:AQS 维护了一个等待队列和一个共享计数器。共享计数器表示当前允许获取锁的线程数,当一个线程尝试获取锁时,如果当前允许获取锁的线程数已经达到了最大值,则将该线程加入到等待队列中,并挂起线程,等待其他线程释放锁或者共享计数器增加。当锁被释放时,会从等待队列中取出一个线程,使其获取锁,同时将它从队列中移除,唤醒该线程继续执行。