JMM
概念
JMM 是一套规范,定义多线程环境下,Java 程序中的变量(特别是共享变量)如何被写入内存以及如何从内存中读取的规则,旨在解决由于多线程访问共享数据而可能引发的各种问题
注意:JMM 不是指 Java 程序运行时内存区域的划分(如堆、栈、方法区),那是 JVM 内存结构。这是两个不同的概念
三大特性
原子性
一次操作,要么所有操作全部执行并不受任何因素干扰,要么都不执行
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值
这里线程对本地内存修改后,另一线程读取的共享变量位于主内存中,该值不是最新的
有序性
程序执行的顺序按照代码的先后顺序执行
JMM 允许某些指令重排序以提高性能,但会保证线程内的操作顺序不会被破坏
Happens-Before 原则
是一组规则,用于描述两个操作之间的内存可见性。如果操作 A Happens-Before 于操作 B,那么 A 操作所做的任何修改对 B 操作都是可见的。
1)程序次序规则: 在一个线程内,书写在前面的操作先行发生于书写在后面的操作
2)管程锁定规则: 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
3)volatile 变量规则: 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作
4)线程启动规则: Thread 对象的start()方法先行发生于此线程的每一个动作
5)线程终止规则: 线程中的所有操作都先行发生于对此线程的终止检测
6)线程中断规则: 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7)对象终结规则: 一个对象的初始化完成先行发生于它的 finalize() 方法的开始
8)传递性: 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
volatile
1.可见性:
确保变量的可见性。当一个线程修改 volatile 变量的值,新值会立即被刷新到主内存中,其他线程中的缓存无效,需要在主存中读取新值
2.禁止指令重排序:
1)通过内存屏障来禁止部分指令重排序
2)保证执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行
示例:
两个线程分别依次调用writer、reader,volatile修饰flag变量关键字保证 2 操作执行先于 3 操作
class ReorderExample {
int a = 0;
boolean volatile flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
3.不保证原子性:
private static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> count++).start();
}
System.out.println(count); // 结果可能不为 1000
}
synchronized
Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁(类对象锁、实例对象锁)
作用
实现代码同步,同一时刻只有一个线程在执行某个方法或代码块
保证可见性,对于共享数据的变化,能够直接被其他线程读取(可以代替volatile)
同步方式
同步方法(实例对象锁)
在方法声明中加入synchronized关键字,保证在任意时刻,对于同一个对象,只有一个线程能执行该方法
正确示范:
这里i是static修饰的共享资源
两个线程由同一个instance对象创建
一个对象只有一把锁,当一个线程获取了该锁后,其他线程无法访问该对象的其他 synchronized 方法
这把锁只作用于 synchronized 方法,其他线程还是可以访问该对象的非 synchronized 方法
public class AccountingSync implements Runnable {
//共享资源(临界资源)
static int i = 0;
// synchronized 同步方法
public synchronized void increase() {
i ++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
AccountingSync instance = new AccountingSync();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
错误示范:
这里两个线程由不同对象创建,具有两把锁。所以最终i的值无法保证
public class AccountingSyncBad implements Runnable {
//共享资源(临界资源)
static int i = 0;
// synchronized 同步方法
public synchronized void increase() {
i ++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
// new 两个AccountingSync新实例
Thread t1 = new Thread(new AccountingSyncBad());
Thread t2 = new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
同步静态方法(类对象锁)
在静态方法声明中添加synchronized关键字,保证同一时刻,只有一个线程能够访问该类的静态同步方法
示例:
这里两线程由同一个类的不同对象创建,但只能争夺同一个类锁
访问静态 synchronized 方法占用的锁是当前类的锁,当类的锁被占用时,不影响访问对象锁占用的资源
此处对象仍然可以通过调用
increase4Obj()方法,修改i的值
public class AccountingSyncClass implements Runnable {
static int i = 0;
/**
* 同步静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase() {
i++;
}
// 非静态,访问时锁不一样不会发生互斥
public synchronized void increase4Obj() {
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new新实例
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
同步代码块(实例对象锁/类对象锁)
当我们编写的方法代码量较多时,而需要同步的代码块只有一小部分,此时可以使用同步代码块的方式对需要同步的代码进行包裹
括号内可以填写对象实例或者类对象,锁分别加在对象和类上
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//Class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
synchronized是可重入锁
当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态
但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
一个线程调用 synchronized 方法的同时,在其方法体内部调用该对象另一个 synchronized 方法是允许的

