Java多线程2(补档)


volatile

volatile 关键字了解吗,有什么作用

volatile是java中的关键字,有以下两个作用:

  1. 保证变量的可见性
  2. 禁止指令重排序

volatile如何保证变量的可见性

写:当一个线程修改了一个 volatile 变量的值时,它会立即将最新的值刷新回主内存,并通知其他线程缓存中该变量的值已失效,需要重新从主内存中读取。

读:强制线程从主内存中读取变量的值,而不是从线程自己的缓存中读取,从而保证了变量的可见性。

实际上依靠的是内存屏障

在执行到内存屏障指令时,CPU会强制将写缓冲区中的数据刷新到主存,并清空读缓冲区,保证了可见性。

volatile如何保证代码有序性

volatile 关键字还有一个重要的作用就是防止 JVM 的指令重排序。 在变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

内存屏障的具体实现是依赖于CPU和编译器的。内存屏障会限制指令重排序,确保不会改变程序的语义。

 volatile 关键字禁止指令重排序的效果如下:

双重校验锁实现对象单例(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。

指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

volatile 可以保证原子性吗

volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。

synchronized

synchronized 是什么?有什么用?

synchronized 是 Java 中的一个关键字,是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized 底层原理了解吗

synchronized 关键字底层原理属于 JVM 层面的东西。

synchronized 修饰同步语句块:

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized 修饰方法:

synchronized 修饰的方法用ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

都是对对象监视器 monitor 的获取。

如何使用 synchronized?

1、修饰实例方法 (锁当前对象实例)

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。

2、修饰静态方法 (锁当前类)

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

3、修饰代码块 (锁指定对象/类)

对括号里指定的对象/类加锁:

  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

构造方法可以用 synchronized 修饰么?

构造方法不能使用 synchronized 关键字修饰。

构造方法本身就属于线程安全的,不存在同步的构造方法一说。

JDK1.6 之后的 synchronized 底层做了哪些优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

Java中的锁升级

在Java中,锁升级是指锁的状态从低级别到高级别的转换过程。Java提供了不同级别的锁来支持不同的并发需求,包括无锁、偏向锁、轻量级锁和重量级锁。

  1. 无锁:无锁是指没有加锁的情况,多个线程可以同时访问共享数据,没有竞争。
  2. 偏向锁:当只有一个线程访问同步资源时,会将对象头中的线程ID记录下来,以后该线程再次进入同步块时,无需执行额外的加锁操作。
  3. 轻量级锁:当多个线程竞争同一把偏向锁时,通过CAS操作将对象头中的标记升级为轻量级锁,并使用自旋来尝试获取锁。自旋期间,线程会不断尝试获取锁而不进入阻塞状态,减少线程上下文切换的开销。
  4. 重量级锁:重量级锁是一种适用于高并发场景的锁机制。当自旋一定次数无法获取锁时,线程会进入阻塞状态,由操作系统来进行调度。重量级锁使用了操作系统的互斥量来实现锁的功能。

Java中的锁升级过程是自动进行的,根据竞争情况和同步块的复杂性,锁的状态会在无锁、偏向锁、轻量级锁和重量级锁之间进行转换。这个过程是动态的,JVM会根据实际场景来优化锁的性能

synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在

  • 性能volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好
  • 用法volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • 原子性volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • 功能volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

ReentrantLock

ReentrantLock 是什么

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantLock 里面有一个内部类 SyncSync 继承 AQS,添加锁和释放锁的大部分操作都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

ReentrantLock 的底层就是由 AQS 来实现的

synchronized 和 ReentrantLock 有什么区别?

两者都是可重入锁可重入锁也叫递归锁,指的是线程可以再次获取自己的内部锁。

  1. synchronized 是依赖于 JVM 实现的,ReentrantLock 是 JDK 层面实现的

  2. ReentrantLock 比 synchronized 增加了一些高级功能

  • 等待可中断 : 正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
  • 可实现选择性通知: synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

说说对Condition的了解

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,

实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”*

各种类型的锁

公平锁和非公平锁有什么区别

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁 :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁

可中断锁和不可中断锁有什么区别

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

共享锁和独占锁有什么区别

  • 共享锁:一把锁可以被多个线程同时获得。(读锁)
  • 独占锁:一把锁只能被一个线程获得。(写锁)

线程持有读锁还能获取写锁吗

  • 在线程持有读锁的情况下,该线程不能取得写锁(不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(写锁被当前线程持有,锁的重入,锁的计数增加)

读锁为什么不能升级为写锁

写锁可以降级为读锁,但是读锁却不能升级为写锁。

因为读锁升级为写锁会引起线程的争夺,会影响性能

可能会有死锁问题发生。比如:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。

乐观锁和悲观锁

什么是悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,每次在获取资源操作的时候都会上锁。

悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。

什么是乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了。

乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。

如何实现乐观锁?

版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

CAS 算法

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

CAS存在哪些问题

ABA 问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,它可能之前被修改过,但是检查之前又修改回A了

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳

循环时间长开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录