1.原子性问题如何解决
原子性的源头是线程切换。而前程切换是基于CPU中断实现的,那是不是让CPU无法中断,就能避免线程切换并保证原子性呢?
答案是不一定,在单CPU场景下或许可以这样,但是在多CPU场景下就行不通了,因为可能一个代码操作会多个线程在多个CPU同时执行,此时就无法保证CPU的原子性,此时多个线程同时执行,还是可能会产生竞态条件
那么该如何保证原子性呢?
这时候可以采取互斥机制:保证同一时刻,只有一个线程执行这一条件。也就是说保证对共享变量的修改时互斥的
2.互斥
2.1.简易锁模型

临界区:一段需要互斥的代码
简易锁模型:线程进入临界区之前首先请求锁(尝试加锁)lock(),成功则进入临界区,失败则等待或返回(阻塞or非阻塞),持有锁的线上执行完临界区的代码后,执行解锁unlock()
2.2.改进后的锁模型

这里我们将临界区的受保护的资源标注了出来,其次我们还将受保护的资源R与保护资源的锁LR相关联,这样就能有效避免锁别的临界区资源的情况
2.3.Java提供的锁机制:Synchronized
锁是一种通用解决方案,Java语言提供的Synchronized就是锁的一种实现。
其可以用来:
-
修饰非静态方法:
锁定的是当前实例对象this
class X {
// 修饰非静态方法
synchronized(this) void foo() {
// 临界区
}
}
-
修饰静态方法:
锁定的是当前类的Class对象
class X {
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
}
}
-
修饰代码块:
锁定的是指定的类对象实例
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock(),这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的,毕竟忘记解锁 unlock() 可是个致命的 Bug(意味着其他线程只能死等下去了)
2.4.锁和受保护资源的关系
受保护资源和锁的关系应该是N:1,也就是一把锁可以保护多个资源,也就是说对共享资源的读写应该是由一把锁来保护的,如果多把锁同时保护一个资源,会有什么问题呢?
例如如下:
在并发调用addOne()的时候,调用get()方法时就会导致value的可见性问题,因为此时value是被SafeCalc.class和this一起保护
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
synchronized是Java在语言层面的提供的互斥原语,其实Java还有很多其他类型的锁,但是作为互斥锁,原理都是相通的:锁,一定要有一个锁定的对象
3.如何用一个锁保护多个资源
3.1.保护没有关联的多个资源
前面也提到了,多个资源共用一把锁,当然是可以的,但是此时会导致针对没有关联的共享资源的访问都变为单线程,性能太差。
此时用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,细粒度锁
3.2.保护有关联关系的多个资源
如何定义资源存在关联关系:
简单来说,关联关系就是一种原子性特征:外在表现是不可分割,本质操作的中间状态对外不可见,面向CPU就是多核CPU执行互斥也不中断

如果这个时候仍然使用一把锁进行保护,则可能出现锁无法覆盖所有的受保护资源(具体也需要看场景),而正确的做法是选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源,总结如下:
加锁的原则:
不存在关联关系的资源,尽量用细粒度锁
存在关联关系的资源,需要使用能够覆盖所有受保护资源的锁,并且需要梳理出访问路径,所有访问路径都设置合适的锁。