线程安全问题的主要原因:
解决这些问题的根本办法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完毕再对共享数据进行操作。
互斥锁的特性:
synchronized锁的不是代码,而是对象。
获取的锁可分为:
获取对象锁的方法:
获取类锁的方法:
类锁和对象锁互不干扰。
实现synchronized的基础
对象在内存中的布局
对象头结构:
Monitor:每个Java对象天生自带了一把看不见的锁
什么是重入:当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,会处于阻塞状态,当这个线程再次请求时,属于重入。
为什么很多人不愿使用synchronized
Java6以后,synchronized性能得到了很大提升:
自旋锁:
自适应自旋锁:
锁消除
更彻底的优化
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化
另一种极端
通过扩大加锁的范围,避免反复加锁和解锁
synchronized的四种状态
锁膨胀方向:无锁→偏向锁→轻量级锁→重量级锁
偏向锁:减少同一线程获取锁的代价
核心思想:若一个线程获得了锁,锁就进入偏向模式,此时Mark word的结构变成偏向锁结构,当线程再次请求锁时,无须任何同步操作,即获取锁时只需检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark World的ThreadID即可,省去了大量有关锁请求操作。
不适用于锁竞争比较激烈的多线程场合
轻量级锁:
由偏向锁升级而来,偏向锁运行在一个线程进入同步块的情形,当第二个线程加入参与锁竞争时,偏向锁就转化为轻量级锁
适应场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
锁的内存语义:
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
当线程获取锁,Java会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
偏向锁、轻量级锁、重量级锁汇总
锁 | 优点 | 缺点 | 使用场景 |
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法仅存在纳秒级的差距 | 线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块或同步方法 |
轻量级锁 | 竞争的线程不会阻塞,提高响应速度 | 若线程长时间抢不到锁,自旋会消耗CPU性能 | 线程交替执行同步块或同步方法 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步方法或同步方法执行时间较长的场景 |
ReentrantLock(再入锁)
ReentrantLock公平性的设置
ReentrantLock将锁对象化
是否能将wait/notify/notifyAll对象化
synchronized和ReentrantLock的区别
Java内存模型JMM
Java内存模型(Java Memory Model,JMM)是种抽象的概念,并不真实存在,它描述一组规则或规范,通过这组规范定义程序中各个变量地访问方式。
JMM中的主内存
JMM中的工作内存
JMM和Java内存区域划分是不同的概念层次
主内存与工作内存的数据存储类型以及操作方式归纳
JMM如何解决可见性问题
指令重排序需要满足的条件
无法通过happens-before原则推导出来的,才能进行指令的重排序
A操作的结果需要对B操作可见,则A与B存在happens-before关系
volatile:JVM提供的轻量级同步机制
volatile变量为何立即可见?
volatile如何禁止重排优化
内存屏障(Memory Barrier)
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷新各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
实现线程安全的单例写法:
单例的双重检测实现:
public class Singleton {
//禁止指令重排序优化
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
//第一次检测
if (instance == null){
//同步
synchronized (Singleton.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new Singleton();
}
}
}
return instance;
}
}
volatile和synchronized的区别
volatile | synchronized |
本质是告诉JVM当前变量在寄存器(工作内存)中的值不确定,需要从主内存读取 | 锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞朱直到该线程完成变量操作 |
仅使用在变量级别 | 可使用在变量、方法和类级别 |
仅能实现变量的修改可见性,不保证原子性 | 可保证变量修改的可见性和原子性 |
不会造成线程阻塞 | 可能会造成线程阻塞 |
标记的变量不会被编译器优化 | 标记的变量可被编译器优化 |
一种高效实现线程安全性的方法
CSA思想
包含三个操作数——内存位置(V)、预期原值(A)和新值(B)
CAS多数情况下对开发者是透明的
缺点:
利用Executors创建不同的线程池满足不同场景的需求
1. newFixedThreadPool(int nThreads)
指定工作线程数量的线程池
2. newCachedThreadPool()
处理大量短时间工作任务的线程池
1)试图缓存线程并重用,当无缓存线程可用时,救会创建新的工作线程;
2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
3)系统长时间闲置的时候,不会消耗什么资源
3.newSingleThreadExecutor()
创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代他。
4.newSingleThreadScheduleExecutor()与newScheduledThreadPool(int corePoolSize)
定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
5.newWorkStealingPool()
内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序
Fork/Join框架
把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架
为什么使用线程池
J.U.C的三个Executor接口
ThreadPoolExecutor的构造函数
handler:线程池的饱和策略
线程池的状态
线程池的大小如何选定