在并发编程的情况下,如果需要保证线程安全,需要解决三个问题:
原子性问题需通过加锁来解决,而可见性和有序性,Java则依靠JMM
JMM区别与JVM JMM 指的是Java内存模型,而JVM 指代的是Java数据存储模型
解决缓存带来的可见性问题需要禁用缓存,解决有序性问题则需要禁止指令重排序。JMM提供了如何按需禁用缓存和指令重排的方法。主要通过8个操作命令 与 7个 Happens-Before规则
JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。
lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。
assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。
write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
JMM 还规定了上述 8 种基本操作,需要满足以下规则:
read 和 load 必须成对出现;store 和 write 必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。
不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须把变化同步到主内存中。
不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。
一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。
如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。
对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)
比如下面的代码 (i=0)
i+=1
此代码包含了6个命令:
这条规则是指在一个线程中,按照程序顺序(可能是重排序后的顺序),前面的操作 Happens-Before 于后续的任意操作,程序前面对某个变量的修改一定是对后续操作可见的
ClassReordering {int x = 0, y = 0;public void writer() {x = 1;y = 2;}public void reader() {int r1 = y;int r2 = x;}
}
如果先执行 writer() 方法,那么 当 reader() 执行时,r1与r2的值分别是1,2 因为 writer() 先于 reader() 方法执行。
这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作
class VolatileExample {int x = 0;volatile boolean v = false;// 线程A 先public void writer() {x = 1;v = true;}// 线程B 后public void reader() {if (v == true) {// 这里x会是多少呢? }}
}
这里由于A 先执行 对 volatile 修饰的变量 V 的写操作,因此,当线程B启动的时候,对变量 V进行读操作时,
V的值就是 true。 再由于 Happen-Before的 传递性 ,在线程 A 中 ,x=1 在前,那么根据规则一(程序的顺序性规则),x=1 Happen-Before v =true这一步,而线程A 对 volatile 修饰的变量 V 的写操作 Happen-Before
线程B 对 volatile 修饰的变量 V 的读操作,因此 x=1 Happen-Before 线程B 对 volatile 修饰的变量 V 的读操作
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁
int x = 1;public void syn() {synchronized (this) { //此处自动加锁if (this.x < 2) {this.x = 22; } } //此处自动解锁}
此处对 this 的加锁操作一定发生在解锁操作前,众所周知,调用Object类的wait方法会释放锁,如果在非锁的代码中调用 wait命令是会抛出异常的
它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
static int var = 1;
// 主线程A
public static void t1() {Thread B = new Thread(()->{// 主线程调用B.start()之前// 所有对共享变量的修改,此处皆可见// var=2});// 此处对共享变量var修改var = 2;// 主线程启动子线程B.start();
}
如上述代码,主线程先修改了变量 var 的 值为 2 后启动了 子线程B。因此,当线程B对 主线程修改后的var值2是可见的
它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作结果可见。
static int var = 5;//主线程A
public static void t1() {Thread B = new Thread(()->{// 此处对共享变量var修改var = 6;});// 主线程启动子线程B.start();//主线程等待子线程B结束B.join(),// var==6}
上述代码中,主线程启动B线程后,调用join方法,等待B执行结束,在获取变量var的值一定是B线程修改后的最新值。
对线程interrupt()方法的调用 Happens-Before 被中断线程的代码检测到中断事件的发生,比如我们可以通过Thread.interrupted()/isInterrupted方法检测到是否有中断发生。
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
final int x;
// 错误的构造函数
public FinalFieldExample() { x = 3;y = 4;// 此处就是讲this逸出,global.obj = this;
}
如上述代码 x 被 final 修饰,是不可变的,在jdk1.5中,可能会优化成
final int x;public FinalFieldExample() { global.obj = this;x = 3;y = 4;
}
此时将存在一个问题,外面函数通过global.obj.x获取x可能得到0。这是由于构造函数内可能重排序,先执行了global.obj.x=this,然后再对x赋值。所以不要在构造函数中将对象赋值给外部变量,有可能对象的属性还未初始化
上一篇:JAVA练习76-子集