Java内存模型
Java内存模型是JVM设计的一套旨在
多并发的场景下,也能具有平台无关性的访问存储规则。
主内存 & 工作内存
- 主内存: 所有变量(静态字段、实例字段、数组对象元素等)都必须存在主内存中。
- 工作内存:线程独立的空间,保存了可能被该线程用到的变量
在主内存的方法拷贝。

对应于具体的操作系统,主内存对应硬件内存,JVM会让工作内存优先享有寄存器&告诉缓存
主内存与工作内存的交互
两者内存之间如何交互,JVM定义了八种操作,每种操作都是原子的,保证线程安全。
-
lock(锁定),unlock(解锁):作用于
主内存中变量 -
read(读取),write(写入):作用于
主内存中变量 -
load(载入),use(使用),assign(赋值,将工作内存值赋给主内存),store(存储):作用于
工作内存中变量
上诉八种操作没有强制要求必须连续执行,但对于两两的操作必须是顺序执行,即:
-
read & load; store & write必须成对出现
-
不允许线程抛弃assign操作,即工作内存值改变必须赋给主内存,也不允许没有assign操作就同步内存。
-
新变量必须在主内存中”诞生“,工作内存只存拷贝地址
-
一个变量同一时刻只允许一个线程lock(锁)
volatile关键字
- volatile关键字保证
可见性&禁止指令重排,但不保证原子性。- 可见性的实现:对变量操作完成后插入一个空操作,强制要求工作内存向主内存读。
- 禁止指令重排的实现:通过插入若干
内存屏障指令,防止JVM指令重排优化。 - 关于原子性的限制:对于变量的操作结果依赖当前值的情况,或需要与其他变量共同参与不变约束的情况下并不安全。
上述可见,用volatile修饰的变量读的性能和普通变量差不多,但写性能会略慢(整体比sychronized还是更快点的)
- 经典面试点:单例双重锁模式,为什么需要用volatile修饰单例对象?
Class SingleTon {
/** 单例 */
private static volatile SingleTon instance;
public static SingleTon getInstance() {
if (instance == null) {
synchronized(SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
-
禁止指令重排: 防止先修改instance值,再创建的情况,保证第一层的instance判断一定可靠。
-
可见性: 单个线程中对于对象的操作完成后,直接更新主内存,保证锁住对象后,从主内存中拿到的对象状态一定可靠。
long、double特殊规则
对于64位的long、double,JVM允许划分为两个32位的变量分别进行操作,因此,不用volatile修饰的long、double,是不安全的。(可能会得到一个既非原值也非新值的”中间值“)
Java与线程
线程
线程的大部分实现都是native的,无法使用平台无关的手段实现。实现线程主要有三种方式:
- 使用内核线程实现(由内核完成线程切换,需要内核中的线程支持,代价较高)
- 使用用户线程实现(在用户空间(非内核中)完成切换,无内核的支持,手动处理逻辑复杂)
- 使用用户线程+轻量级线程混合实现(较优权衡)
生命周期

线程调度
线程调度——为线程分配使用权的过程,有两种方式。
协同式线程调度
没有时间限制,当前线程执行完,主动通知下一个待执行的线程。实现简单,易发生阻塞(某个线程死循环)
抢占式线程调度
线程的切换不由线程本身决定,由系统进行分配。分配中,线程有
优先级概念。但这个优先级并不是很靠谱
线程安全性级别
- 不可变(final) 如String对象、枚举类型、Number部分子类、BigInteger等大数据类型等。
- 绝对线程安全(任何时候不对代码做任何操作也能保证线程安全)
- 相对线程安全 (对象单独操作是安全的,无需做任何操作,需要考虑并发操作,如插入删除等) 如:Vector、HashTable等。
- 线程兼容 (线程不安全)
- 线程对立 (无论是否做同步措施,都无法在多线程中使用)如:Thread的suspend和resume方法,在不同的线程调用会造成死锁。(这种设计也不该出现!)
线程安全实现方法
线程互斥&线程同步
- 线程同步指:多线程场景下,共享数据同一时刻只应被一个线程使用。
- 线程互斥——实现线程同步的一种方法,如临界区、互斥量、信号量都是互斥的实现方式。
sychronized VS reentrantLock
- 一个是Java关键字,由JVM保证实现,变量存在方法头中
- 一个是Java的工具类包,reentrantLock在sychronized的基础上支持公平锁、等待可中断、可绑定多个条件
需要了解几种不同的锁,以及阻塞同步&非阻塞同步(CAS)
性质分类
公平锁VS非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。
有可能会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
乐观锁/悲观锁
-
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
-
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法(compare and swap),典型的例子就是原子类,通过CAS自旋实现原子操作的更新,缺点是1.CPU开销大 2.不能保证原子性 3.ABA的场景下会造成脏读。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。
独享锁/共享锁(互斥锁/读写锁)
- 独享锁是指该锁一次只能被一个线程所持有。
- 共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享
设计方案分类
自旋锁/自适应锁
如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 优点是:自旋等待本身可以避免线程切换的开销。缺点是:自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的 自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长时间,比如100个循环。
- 如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源
偏向锁/轻量级锁/重量级锁
偏向锁 > 轻量级锁 > 重量级锁
这三种锁是指锁的状态。并且是针对Synchronized。
这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
synchronize VS lock
- Synchronized,它是一个:非公平,悲观,独享,互斥,可重入的重量级锁
- ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
- ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。
Synchronize VS volatile
1.volatile是线程同步的轻量级实现,所以volatile的性能要比synchronize好;volatile只能用于修饰变量,synchronize可以用于修饰方法、代码块。随着jdk技术的发展,synchronize在执行效率上会得到较大提升,所以synchronize在项目过程中还是较为常见的;
2.多线程访问volatile不会发生阻塞;而synchronize会发生阻塞;
3.volatile能保证变量在私有内存和主内存间的同步,但不能保证变量的原子性;synchronize可以保证变量原子性;
4.volatile是变量在多线程之间的可见性;synchronize是多线程之间访问资源的同步性; 对于volatile修饰的变量,可以解决变量读时可见性问题,无法保证原子性。对于多线程访问同一个实例变量还是需要加锁同步。
Synchronize 锁对象VS锁方法
synchronize底层原理: https://www.cnblogs.com/aspirant/p/11470858.html