八股-JUC/多线程

108 阅读11分钟

JMM(Java内存模型)

  • 主内存(线程共享的内存,有共享变量)
  • 缓存区(读写都可能用到)
  • 工作内存/本地内存(线程私有的内存,有共享变量的拷贝)

volatile

  • volatile是Java中的关键字,是Java虚拟机提供的最轻量级的同步机制。
  • 被修饰的变量,所有线程都需要通过共享内存来访问它。
  • 可以保证变量的可见性和有序性(也及禁止指令重排)。

保证可见性的原理:lock前缀+缓存一致性协议+嗅探机制

  • lock前缀: 被volatile修饰的变量,执行写操作时,JVM会在写操作指令上加上Lock前缀。将当前处理器修改后的缓存数据写回到主内存中
  • 缓存一致性协议: 协议通过在总线上传播数据的方式实现。当一个处理器修改(某个内存地址的)数据并将其写回内存时,它会通过总线广播这个信息。
  • 嗅探机制: 其他处理器通过嗅探(监听)总线上的广播信息,检查自己缓存的数据是否过期。如果发现缓存对应的内存地址的数据被修改了,就会将当前缓存数据设置为无效状态。当自己要用时再重新从主内存读到本地内存中。

保证有序性原理(禁止指令重排序):

  • volatile修饰的变量,编译器生成字节码文件时,会在读写操作指令前后插入读写内存屏障。
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障确保,不会将读屏障之后的代码排在读屏障之前

原文链接:blog.csdn.net/xinghui_liu…

final

final 也可以保证可见性

  • 被 final 修饰的字段在构造方法中初始化完成
  • 且构造方法没有把 this 引用传递出去,在其他线程中就能看见 final 字段值。

ThreadLocal

  • 本地线程变量。创建ThreadLocal变量后,访问这个变量的每个线程,都会有一个该变量的本地拷贝。
  • 多线程操作ThreadLocal变量时,其实是操作自己本地内存的变量,起线程隔离、避免线程安全问题的作用。 应用场景:数据库连接池、会话管理

实现原理

  • Thread类有一个ThreadLocal.ThreadLocalMap的实例变量。所以每个线程都有自己的map
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
  • 线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocal 内存泄露问题

  • 弱引用:垃圾回收时,不管JVM的内存空间是否充足,都会回收该对象占用的内存。 弱引用比较容易被回收。

因此,如果ThreadLocalMap的Key被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。

如何解决内存泄漏问题?

  • 使用完ThreadLocal后,及时调用remove()方法释放内存空间。

锁(多线程的)

概念:公平锁、非公平锁、重量级锁

  • 非公平锁:
    • 不考虑线程的等待时间,通过竞争直接尝试获取锁。如果锁空闲,线程立即获取到锁;如果锁被其他线程持有,将进入竞争状态,可能获取成功,也可能失败。
    • 能提高吞吐量,但会有“饥饿”线程(始终竞争不到锁)。
  • 公平锁:
    • 按线程等待的时间顺序来分配锁。线程尝试获取锁时,如果锁被其他线程占有,该线程会进入等待队列,按等待的先后顺序获取锁。
    • 保证了线程公平,不会有线程获取不到锁,但可能降低整体吞吐量
  • 重量级锁:
    • 严格排他的锁,同一时刻只能有一个获取到锁的线程执行。其他都阻塞等待。

synchronized

  • synchronized是Java 中实现线程同步的关键字,
  • 可重入、非公平的重量级锁,退出synchronized块时会自动释放。
  • 它保证在同一时刻只有一个线程执行方法或代码块。
  • 保证了原子性(只有一个拿到锁的线程执行)、可见性(本地内存修改的变量同步刷新到主内存)、有序性(有指令重排,不过单线程保证了有序性)
  • 修饰方法、代码块。会锁不同的范围
    • 修饰实例方法 : 锁当前对象实例。this 作为锁对象
    • 修饰静态方法 : 锁当前类的Class对象。类名 .class 字节码作为锁对象
    • 修饰代码块 : 锁是Sychonized括号里配置的对象。

synchronized底层实现原理⚡

  • 底层原理跟监视器(Monitor)有关。
  • Java对象都有与之相关联的监视器(就是一个锁)。方法或代码块被synchronized修饰后,编译时,代码块前后会生成monitorentermonitorexit(synchronized的进入和退出操作)。线程运行时就会尝试获取对象的监视器monitor。当一个线程拥有monitor后其他线程只能等待。

monitor内部有两个重要的成员变量:

  • owner: 保存持有锁的线程;
  • recursions:计数器,保存线程持有锁的次数。

当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会退出监视器、释放锁。

锁优化:

  • 自旋锁:
    • 不直接阻塞,继续自旋,循环尝试获取锁。默认十次。
    • 优点:线程不阻塞,减少线程上下文切换的消耗
  • 锁消除
    • 根据逃逸分析,对于不会逃逸,即不会被其他线程访问到,也即不会有锁竞争的共享数据。可当成是线程私有数据,如果加了锁就把锁取清除。
  • 锁粗化
    • 如果对同一个对象多次加锁,导致线程多次重入。可以组化锁的粒度,把锁扩展到整个操作流程的外部。
  • 自适应的自旋锁
    • 不严格按照自旋次数或时间,而是根据近期获取自旋锁的情况决定。
    • 如果近一次成功获取到了锁,那么此次获取该锁的自旋会增加一定等待时间。如果最近多次都没自旋获取到,以后直接取消自旋了。

偏向锁

  • 是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程。
  • 锁获取:当一个线程访问同步块时,偏向锁会把标记位记为1,且记录该线程ID,表示锁处于偏向状态,且偏向于当前线程。这个过程是一个CAS操作
  • 锁释放:如果有其他线程竞争,偏向模式立即结束,标志位置0清除线程ID,偏向锁升级为轻量级锁,

轻量级锁

  • 如果有多个线程要对一个同步块加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化
  • 当一个线程进入同步块时,会先用CAS获取锁。
  • 如果CAS成功,当前线程获取了锁,就把锁的标记位设置成指向当前线程(锁记录)的指针,表示锁被当前线程持有。
  • 如果CAS失败,说明有其他线程正在竞争这个锁。当前线程尝试用自旋获取锁,成功仍然是轻量级锁。自旋失败就升级为重量级锁。

偏向锁、轻量级锁和重量级锁的区别?

偏向锁

  • 优点:加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距;
  • 缺点:如果存在锁竞争会带来额外锁撤销的消耗,适用于只有一个线程访问同步代码块的场景。

轻量级锁

  • 优点:竞争线程不阻塞,程序响应速度快;
  • 缺点:如果线程始终得不到锁会自旋消耗 CPU,适用于追求响应时间、同步代码块执行快的场景。

重量级锁

  • 优点:线程竞争不使用自旋不消耗CPU
  • 缺点:是线程会阻塞,响应时间慢,适应于追求吞吐量、同步代码块执行慢的场景。

锁升级:

顺序:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,随着竞争的增加,只能锁升级,不能降级。 过程:

  • 先判断锁的标志位里,是否有当前线程ID。若有则处于偏向锁,若无则尝试用CAS将标志位替换为线程ID,成功则偏向锁设置成功。
  • 失败则有竞争要升级成轻量级锁,尝试CAS替换标志位为锁记录指针,成功就获得锁,失败表示其他线程竞争锁,当前线程尝试使用自旋获取锁,获取成功依然处于轻量级锁。
  • 自旋失败升级成重量级。(轻量级锁不会自己释放锁)

偏向锁依赖当前线程ID,轻量级锁依赖锁记录,重量级锁依赖监视器monitor

锁的升级的目的:减低锁带来的性能消耗。

Lock 和 synchronized 有什么区别?

  1. 来源:  synchronized是关键字,lock是接口,实现类进行锁操作。
  2. 是否知道获取锁 synchronized无法判断锁的状态,lock可以判断是否获得锁(boolean b = lock.tryLock())
  3. 释放锁 : synchronized自动释放锁,lock手动释放(容易死锁) lock.unlock();
  4. 等待时间:synchronized阻塞后,其他线程一直等待,lock有超时时间。
  5. 调度机制 :  synchronized使用object对象本身的wait 、notify、notifyAll调度机制,Lock可以使用Condition进行线程之间的调度。
  6. 是否响应中断:lock等待锁过程中可以用interrupt来中断等待,synchronized只能等待锁的释放,不能响应中断。

12、synchronized与volatile区别

  1. 解决问题不同
  • volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即保证了变量的可见性。
  • synchronized 解决的是执行控制的问题,它会阻止其他线程获取当前对象的监视器锁,保护线程安全。
  1. 本质原理不同 虽然两个都保证可见性和有序性,但是本质不一样。
  • volatile本质是lock前缀、缓存一致性协议、嗅探机制保持可见性,用内存屏障禁止指令重排序,以保证有序性。
  • synchronized是通过阻塞其他线程实现,只有当前线程可以访问。
  1. 使用范围不同 :
  • volatile仅能使用在变量;
  • synchronized可以使用在方法、代码块。
  1. 功能不同 :
  • volatile仅能保证可见性和有序性;
  • synchronized保证可见性和有序性,此外还能保证原子性。
  1. 是否能被编译器优化 :  
  • 被volatile修饰禁止指令重排,不能被编译器优化;
  • synchronized可以,比如自适应自旋、锁消除、锁粗化等。
  1. 是否造成线程阻塞情况 :
  • volatile不会造成线程的阻塞;
  • synchronized会造成线程的阻塞(不会释放锁)。

13、synchronized和ReentrantLock区别是什么?

  1. 两者都是可重入锁
  • 可重入锁是指,线程已经持有锁时,它可以再次获取该锁,而不会被自己所持有的锁所阻塞。
  1. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
  • synchronized 是依赖于 JVM 实现的,是虚拟机层面的实现
  • ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成
  1. ReentrantLock 比 synchronized 增加了一些高级功能
  • 主要来说主要有三点:1、等待可中断; 2、可实现公平锁;3、可实现选择性通知(锁可以绑定多个条件)
    • 等待可中断:通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    • ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean  fair)构造方法来制定是否是公平的。
    • ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 synchronized在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”。