一、概念
JMM与java并发编程相关:
1、抽象了线程与主内存的关系,例如线程的共享变量需要放到内存中进行读取
2、规定了java源代码到CPU可执行指令这个转换过程中需要遵守的规范,例如防止指令重排序造成的并发问题
二、并发编程的三个特性
1、原子性
一次操作或者多次操作,要么所有的操作全部都得到执行,要么都不执行。
在 Java 中,可以借助synchronized
、各种 Lock
实现原子性。
2、可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
在 Java 中,可以借助synchronized
、volatile
以及各种 Lock
实现可见性。
3、有序性
在 Java 中,volatile
关键字可以禁止指令进行重排序优化
三、volatile
1、可以保证变量的可见性:volatile
关键字修饰变量则表明该变量是共享且不稳定的,每次使用它都要到主存中进行读取
2、可以防止指令重排序:将变量声明为 volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
双重效验锁代码如下:
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
四、synchronized
1、悲观锁:每次在获取共享资源的时候都会上锁
2、重量级锁:依赖于操作系统的监视器锁来实现,操作系统要想阻塞或者唤醒一个线程都需要从用户态转到内核态,需要较长的时间
3、可修饰方法、静态方法、代码块
4、可保证可见性、原子性
修饰代码块会对括号里指定的对象/类加锁:
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
五、乐观锁与悲观锁
1、乐观锁:总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
2、悲观锁:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁。例如synchronized和各种Lock 六、版本号机制 一般是在数据表中加上一个数据版本号
version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当某个线程对数据进行更新时,读取到的version值与当前数据库中的version相等才更新
七、CAS算法
Compare And Swap(比较与交换):用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
但是存在ABA问题:
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,但是不能说明它的值没有被其他线程修改过。因此可以再加上版本号和时间戳进一步验证
八、ReentrantLock
1、实现了Lock接口,是可重入的悲观锁
2、比synchronized强大,具有等待可中断、轮询、可实现公平锁等功能
3、ReentrantLock
的底层就是由 AQS (抽象队列同步器)来实现的。
AQS 使用 int 成员变量 state
表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作。
以 ReentrantLock
为例,state
初始值为 0,表示未锁定状态。A 线程 lock()
时,会调用 tryAcquire()
独占该锁并将 state+1
。此后,其他线程再 tryAcquire()
时就会失败,直到 A 线程 unlock()
到 state=
0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state
会累加),这就是可重入的概念。