解决Java并发编程中的原子性问题
解决并发编程的原子性问题
原子性是指一个或多个操作不被CPU中断的特性,造成原子性问题的源头是CPU线程的切换,CPU线程的切换可能会发生在每一个CPU指令中,而CPU的切换是依赖系统中断的。
单核场景下解决原子性问题
在单核场景中只要禁止CPU中断就可以解决原子性问题
多核场景下解决原子性问题
在多核场景下,同一时刻可能会存在多个线程同时执行的情况,系统中断只能禁止当前CPU的中断,没有办法阻止其他CPU执行,所以在多核CPU的情况下需要保证同一时刻只有一个线程执行代码,相当于多个线程串行执行,前一个线程执行的结果对于后一个线程是可以见的,保证了多个线程之间的互斥。
互斥:同一个时刻只有一个一个线程执行
如何实现互斥
在Java中使用锁来实现线程之间的互斥。通过使用锁的方式对于临界区代码进行加锁和解锁。锁是一种通用的技术方案,Java语言提供了synchronized关键字,就是一种锁的实现。
synchronized关键字
修饰静态方法
// 修饰静态方法 ,锁定的是当前类.class对象
synchronized static void bar() {
// 临界区
}
//相当于
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
}
修饰普通方法
// 修饰非静态方法,锁定的是当前实例对象this
synchronized void foo() {
// 临界区
}
修饰代码块
// 修饰代码块,锁定的是obj这个对象
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
在被synchronized修饰后无论是单核还是多核CPU,都能保证原子性操作,同时也可以保证了可见性,因为根据Java内存模型happens-before原则中对于管程的锁的规则,
管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
synchronized修饰的临界区具有互斥性,前一个线程的解锁对于后一个线程的加锁是可见的,而对于临界区代码的修改happens-before于解锁操作,根据传递性规则,临界区代码的修改对于后续进入的线程是可见的。
锁和受保护资源的关系
锁和受保护资源的一个合理的关系应该是:受保护资源和锁之间的关联关系是 N:1 的关系,
如何使用锁来保护多个资源?
当我们需要保护多个资源的时候,首先要区分多个资源之间是否存在关联关系。
保护没有关联关系的多个资源
对于没有关联关系的多个资源,我们可以使用多把锁来进行保护,每一个资源都用一把锁来进行保护,也可以使用同一把互斥锁来保护多个对象,但是存在一个问题,就是性能太差,会导致所有的操作都是串行执行。用不同的锁对受保护的资源进行精细化管理,能够提升性能,这种锁还有个名字,叫细粒度锁。
保护有关联关系的多个资源
关联关系指的是在都是在一个临界区里边的多个资源,如果在临界区存在多个资源,锁需要将所有的资源都要保护起来。
总结
”原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。