第一部分:并发理论基础03->互斥锁(上),解决原子性问题
1.原子性
一个或多个操作在cpu执行的过程中不被中断的特性,称为原子性
2.如何解决原子性问题
源头是执行一半线程切换,禁止线程切换是不是就可以了?
操作系统的线程切换,是操作系统自己控制cpu进行的,所以禁止操作系统的cpu发生中断就可以禁止线程切换
单核cpu场景,同一时刻只有一个线程执行,禁止cpu中断,操作系统就不会重新调度线程,禁止了线程切换,获取cpu使用权的线程就可以不间断执行。两次写操作,要么都被执行,要么都没有被执行,具有原子性
多核cpu场景,同一时刻,可能2个线程同时运行,一个线程在cpu1核上,一个线程在cpu2核上,禁止cpu中断,只能保证cpu上的线程连续执行不被线程切换走,不能保证同一时刻只有一个线程执行。
同一时刻,只有一个线程执行,这个条件非常重要,我们成为互斥。我们对共享变量的修改,是互斥的,无论是单核还是多核cpu,都能保证原子性
3.简易锁模型
互斥,锁。
需要互斥执行的代码成为临界区。线程进入临界区之前,尝试加锁,如果成功,进入临界区。线程持有锁,否则就等待,直到持有锁的线程解锁。持有锁的线程执行完临界区代码后,解锁unlock
类比进坑锁门,出坑开门,如厕就是临界区。
4.改建的锁模型
锁和资源的关系
创建和资源关联的锁
5.java语言提供的同步锁synchronized
synchronized就是锁的一种实现,synchronized关键字可以修饰方法,修饰代码库,
代码范例
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
synchronized 默认加上了加锁,解锁操作。
synchronized修饰代码库的时候,锁定了obj对象
synchronized修饰方法时候,锁定的是当前实例对象this
synchronized修饰静态方法时候,锁定的是当前类的class对象
修饰静态方法的具体含义
class X {
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
}
}
修饰非静态方法的具体含义
class X {
// 修饰非静态方法
synchronized(this) void foo() {
// 临界区
}
}
6.多线程共享数据修改问题
synchronized
class SafeCalc {
long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
long get() { return value; }
}
synchronized修饰addOne方法后,一定能保证原子操作,
可见性呢?
6个happens-before中的管程中锁规则说明了,对锁的解锁happens-before与后续对这个锁的加锁操作
管程就是synchronized,synchronized修饰的临界区是互斥的,同一时刻只有一个线程执行临界区的代码
而解锁操作happens-before后续对这个锁的加锁操作。
多线程同时之同时addOne方法,可见性是保证的,A线程修改完后,解锁happens-before与B线程的加锁,数据的可见性有保证
get方法可见不?
答案是不可见,管程中只保证后续对这个锁的加锁的可见性,get方法没有加锁,所以没法保证可见性,优化成synchronized修饰就可以解决这个问题了
get和addOne用的是一把锁this
7.锁和受保护资源的关系
受保护资源和缩之间的关系是N:1,一把锁保护多个资源可以,多把锁保护一个资源不可行
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
修改数据用的是SafeCalc.class锁,而get()用的是this锁,
修改数据的synchronized可见性只保证向同锁的可见性
这两个临界区没有互斥关系,所以有并发问题了
8.总结
互斥锁,需要深知锁的对象和缩之间的关系,才能用好互斥锁。