java多线程的15种锁

1 java锁分类

下面我们依照序号依次的介绍每一种锁

java多线程的15种锁

2 悲观锁和乐观锁

悲观锁和乐观锁是一种广义的概念,体现的是看待线程同步的不同的角度

悲观锁认为自己在使用数据的时候,一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。

锁实现:关键字synchronized、接口Lock的实现类

使用的场景:写操作较多,先加锁可以保证写操作是数据正确

乐观锁认为自己在使用数据的时候不会有其他的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
锁实现:CAS算法
使用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅提升

3 阻塞和自旋锁

是指当一个线程在获取锁的饿时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断是否能够被成功获取,自旋知道获取到锁才会退出循环。自旋是通过CAS算法进行的。何为CAS算法呢?

CAS(compare and swap):比较和交换,顾名思义就是先进行比较然后在进行交换。这里比较和交换是线程中的数据和内存中的数据之间的操作。

如下图所示:1 线程1和线程2都读取内存中的数据V赋值给A 2 线程1把V的值由0改为了1,并想把修改后的值写回到内存 3 线程1将A的值和V的值进行比较 4 两者相等,说明没有线程对V的值进行修改,直接把修改后的值(B=1)写入内存,此时,V=1。3 线程2进行将A的值和V的值进行比较 5 两者不相等,说明有线程对V的值进行修改,此时线程2不能够把修改后的值写入内存,因为它获得的A的值不是最新的,由A得到的B的值也可能是错误的。线程2会读取A的值,重新计算出B的值,再尝试重新写入,如果还是不相等在继续尝试,不断的自旋。

java多线程的15种锁

我们发现CAS算法存在一个非常明显的缺陷,那就是ABA问题。何为ABA问题呢?

如下图所示:线程1线程2 线程3 都获取A=V=0  1 线程1修改V的值为1 写入内存 2 线程2 把v的值改为2,但是没来的及写入,线程3 就开始运行 3 线程3 将V的值改为0 写入内存 4 线程2 比较A和V的值发现A=V,他自认为没有其他的线程对V进行修改,因而忽略了A->B->A的过程,形成了ABA问题。ABA的问题解决方法很简单:AtomicStampedReference在变量前面添加版本号,每次变量更新的时候都把版本号加一。

 

java多线程的15种锁

我们知道自旋是会消耗CPU资源的(不断循环),为什么不用阻塞的方式使等待的线程停止工作呢?原因有两个

(1)同步代码块逻辑简单情况下,自旋消耗的资源时很少的(在同步代码块逻辑简单的情况下,用自旋是比较合适的)

(2)最为关键的原因是阻塞与唤醒线程需要操作系统切换CPU状态,需要消耗一定的时间(CPU上下文切换)

那么为什么阻塞和唤醒线程会消耗大量时间呢?

因为线程的阻塞唤醒涉及大量的步骤,我们以线程阻塞为例进行说明

java多线程的15种锁

步骤1:线程1获取CPU时间片执行

步骤2:线程1被阻塞,上下文切换

步骤3:线程2抢占CPU时间片执行

步骤2涉及两个子步骤:

步骤2.1 从用户态切换到内核态

为什么要进行这种切换呢?

用户是没有权限对内存进行操作,我们要切换到内核态才有权对内存进行操作。

步骤2.2 把线程1的状态保存到PCB(内存)

我们到底要保存线程的哪些信息呢?

一个线程在运行时的内存模型主要有5个部分组成:(1)程序计数器:记录下一条指令的地址(2)虚拟机栈:保存函数的信息,例如,局部的变量,函数返回地址,操作数等(3)本地方法栈:和虚拟机栈类似,不过其保存的函数的信息是native函数(4)方法区:保存类的信息,静态变量等(5)堆:实例化的对象(堆是用户申请的(C语言中malloc函数),而栈是系统自动分配的)。很明显:程序计数器、虚拟机栈、方法区、堆等信息都会被保存到内存中。

可见上下文切换是非常耗时的。

4 无锁、偏向锁、轻量级锁、重量级锁

java多线程的15种锁

随着竞争不断的加剧,锁要不断的升级。

(1)无锁:适用于单线程(2)偏向锁:适用于只有一个线程访问同步块的情况,因为多个线程同时访问同步块,给某一个线程特权是不合理的(3)轻量级锁:竞争不是太多,循环等待消耗CPU资源的线程的数量在可接受的范围(4)重量级锁:多个线程同时竞争资源,只让一个线程运行,其余的线程都阻塞

5 可重入锁和不可重入锁

可重入锁:一个线程可以多次获得同一把锁并通过state记录加了多少锁

6 公平锁和非公平锁

公平锁和非公平锁主要是关于等待的线程的排队的问题,这个排队要利用AQS(AbstractQueuedSynchronizer)

我们以重入锁(ReentrantLock)为例解释AQS。AQS由三个部分组成,State:当前线程锁的个数、exclusiveOwerThread:当前占有锁的线程 、CLH队列等待运行的线程。

1 线程1 CAS算法A=V(state)=0,修改state的值为1 2 线程1又想获取锁,此时A=V(state)=1,state再加1,无论A想获得多少次,只是state+1 3 线程2 进行CAS比较,发现A不等于V,并且发现state不等于0,直接到CLH列队中等待。线程3和线程4也一样到CLH队列中等待。如果先来的线程先排队,获取锁的优先权,则为公平锁。如果,无视等待队列,直接尝试获取锁,则为非公平锁。

java多线程的15种锁

队列如果已经满了,该怎么办呢?

无法进入队列的线程,进入ArrayBlockingQueue,等队列有空位再进入队列

7 互斥锁和共享锁

互斥锁:在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

共享锁:共享锁从字面来看也即是允许多个线程共同访问资源。

互斥锁很容易理解,上锁之后,其他线程都阻塞了。

共享锁一个典型的例子就是读者写者模式,一个人写多个人去读,写者写出的东西,多个读者是可以一块去读的,这就是多个读者共享一个同步资源。

共享锁--用到共享锁的有Semapore信号量和ReadLock读锁