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修饰后,编译时,代码块前后会生成monitorenter和monitorexit(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 有什么区别?
- 来源: synchronized是关键字,lock是接口,实现类进行锁操作。
- 是否知道获取锁 synchronized无法判断锁的状态,lock可以判断是否获得锁(
boolean b = lock.tryLock()) - 释放锁 : synchronized自动释放锁,lock手动释放(容易死锁)
lock.unlock();。 - 等待时间:synchronized阻塞后,其他线程一直等待,lock有超时时间。
- 调度机制 : synchronized使用object对象本身的wait 、notify、notifyAll调度机制,Lock可以使用Condition进行线程之间的调度。
- 是否响应中断:lock等待锁过程中可以用interrupt来中断等待,synchronized只能等待锁的释放,不能响应中断。
12、synchronized与volatile区别
- 解决问题不同:
volatile解决的是内存可见性问题,会使得所有对volatile变量的读写都直接写入主存,即保证了变量的可见性。synchronized解决的是执行控制的问题,它会阻止其他线程获取当前对象的监视器锁,保护线程安全。
- 本质原理不同 虽然两个都保证可见性和有序性,但是本质不一样。
- volatile本质是lock前缀、缓存一致性协议、嗅探机制保持可见性,用内存屏障禁止指令重排序,以保证有序性。
- synchronized是通过阻塞其他线程实现,只有当前线程可以访问。
- 使用范围不同 :
- volatile仅能使用在变量;
- synchronized可以使用在方法、代码块。
- 功能不同 :
- volatile仅能保证可见性和有序性;
- synchronized保证可见性和有序性,此外还能保证原子性。
- 是否能被编译器优化 :
- 被volatile修饰禁止指令重排,不能被编译器优化;
- synchronized可以,比如自适应自旋、锁消除、锁粗化等。
- 是否造成线程阻塞情况 :
- volatile不会造成线程的阻塞;
- synchronized会造成线程的阻塞(不会释放锁)。
13、synchronized和ReentrantLock区别是什么?
- 两者都是可重入锁
- 可重入锁是指,线程已经持有锁时,它可以再次获取该锁,而不会被自己所持有的锁所阻塞。
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
- synchronized 是依赖于 JVM 实现的,是虚拟机层面的实现
- ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成
- ReentrantLock 比 synchronized 增加了一些高级功能
- 主要来说主要有三点:1、等待可中断; 2、可实现公平锁;3、可实现选择性通知(锁可以绑定多个条件)
- 等待可中断:通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
- ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 synchronized在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”。