乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则读取最新的值。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都其他线程会修改,所以每次在读写数据的时候都会上锁,采用的是一种先取锁再访问的策略,这样别人想读写这个数据就会阻塞直到拿到锁。java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁
-
为什么要有自旋锁? 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换(线程的阻塞和唤醒需要CPU从用户态转换成内核态,频繁的阻塞和唤醒对CPU来说是一种负担),而是进入阻塞挂起状态,这样它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
-
自旋锁是指当一个线程尝试获取某个锁而获取不到的时候(锁被其他线程占用着),那么该线程会循环检测该锁是否被释放,而不是立即进入线程挂起或者睡眠状态。自旋锁适用于锁保护的临界区很小,因为临界小了,那么线程占用锁的时间就少,这样其他线程就可以经过短暂的自旋后获取到该锁,从而避免线程状态切换带来的消耗。
锁消除
在多线程中,为保证数据的完整性,在多线程下对共享数据的操作需要进行同步控制。但是在某些情况下,Java虚拟机会检测到多线程不可能存在共享数据的竞争,这是就不需要对共享资源进行加锁。所以锁消除可以节省无意义的请求锁时间,提高程序运行效率。
锁粗化
在使用锁的时候,我们一般认为都应该将被锁保护的临界区要尽可能小使需要进行同步的操作数尽可能小,在竞争锁的情况下,其他等待锁的线程就可以比较快地获取到锁。但是!但是后面很重要,如果在一个程序中存在一系列连续的加锁、解锁操作,那么这些加锁和解锁的过程就会导致不必要的性能消耗。因此,我们就可以将多个连续的小范围内的加锁和解锁操作连接在一起,扩展成为一个范围更大的锁,这就是锁粗化。
锁的四种状态
1. 无锁 无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
2. 偏向锁 在执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销。*偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
3. 轻量级锁 轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问时偏向锁就会升级为轻量级锁(一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁),其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
4. 重量级锁 如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
Java中的锁池和等待池
在前面的文章中,我们很多次讲到了锁池、等待池,那么等待池、锁池到底是用来干什么的?
- 在Java中,每个对象都有两个池,锁(monitor)池和等待池
等待池: 设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。
锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
Lock锁
- Lock是一个接口(以下为其三个实现类)
- ReentrantLock (可重入锁)
- ReentrantReadWriteLock.ReadLock(读锁)
- ReentrantReadWriteLock.WriteLock (写锁)
- Lock接口的主要方法
-
void lock(): 阻塞式加锁方法, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有,调用该方法的线程将会被阻塞, 直到当前线程获取到锁.
-
void unlock(): 执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
-
boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果获取不到, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
-
tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
-
isLock(): 判断此锁是否已经被其他线程占有
-
Condition newCondition(): 该方法将创建一个绑定在当前Lock对象上的Condition对象,一个Lock对象可以绑定多个Condition对象。Condition实现的就是当线程挂起时指明在什么样的条件上挂起,同时在等待的事件发生后,只唤醒在等待该事件上的线程。
-
getHoldCount() : 查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
-
getQueueLength(): 返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
-
getWaitQueueLength:(Condition condition) 返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
-
hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
-
hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁
-
hasQueuedThreads(): 是否有线程等待此锁
-
isFair(): 该锁是否公平锁
-
isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程执行lock()方法的前后分别是 false 和 true
-
tryLock(): 尝试获得锁,仅在调用时锁未被线程占用,获得锁
Condition的使用(唤醒指定线程)
/**
* @Author GJY
* @Date 2021/3/9 15:48
* @Version 1.0
*/
public class JucProCum {
public static void main(String[] args) {
Date3 date3 = new Date3();
new Thread(()->{
for (int i = 0; i < 5; i++) {
date3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
date3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
date3.printC();
}
},"C").start();
}
}
class Date3{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number=1;
public void printA(){
lock.lock();
try{
while(number!=1){
//使当前获取lock的线程进入等待状态,需要被通知才能继续运行下面的代码
//只有调用该Condition对应的signal()方法才能被唤醒继续向下执行
condition1.await();
System.out.println("-收到通知-");
}
System.out.println(Thread.currentThread().getName()+"AA");
number=2;
//唤醒持有condition2监视器的线程
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try{
while(number!=2){
condition2.await();
System.out.println("-收到通知-");
}
System.out.println(Thread.currentThread().getName()+"BB");
number=3;
//唤醒持有condition3监视器的线程
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try{
while(number!=3){
condition3.await();
System.out.println("-收到通知-");
}
System.out.println(Thread.currentThread().getName()+"CC");
number=1;
//唤醒持有condition1监视器的线程
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
- 程序输出:
AAA
BBB
CCC
-收到通知-
AAA
BBB
CCC
-收到通知-
AAA
BBB
CCC
-收到通知-
AAA
BBB
CCC
-收到通知-
AAA
BBB
CCC
ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
- ReentrantLock应用(经典卖票问题)
/**
* @Author GJY
* @Date 2021/7/1 11:45
* @Version 1.0
*/
public class Juc2 {
public static void main(String[] args) throws InterruptedException {
Tick tick2 = new Tick();
new Thread(() -> {for (int i = 0; i < 40; i++) {tick2.selltick();}}, "A").start();
new Thread(() -> {for (int i = 0; i < 40; i++) {tick2.selltick();}}, "B").start();
new Thread(() -> {for (int i = 0; i < 40; i++) {tick2.selltick();}}, "C").start();
}
}
class Tick2 {
private int number = 30;
Lock reentrantLock = new ReentrantLock();
public void selltick() {
reentrantLock.lock();
try{
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票" + ",剩余" + number + "张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
Lock锁和synchronize的区别
1、两者所处层面不同 synchronized是Java中的一个关键字,当我们调用它时会从在虚拟机指令层面加锁,关键字为monitorenter和monitorexit。 Lock是Java中的一个接口,它有许多的实现类来为它提供各种功能,将同步代码放在lock()加锁、unlock()解锁中间。
2、获锁方式 synchronized可对实例方法、静态方法和代码块加锁,相对应的,加锁前需要获得实例对象的锁或类对象的锁或指定对象的锁。说到底就是要先获得对象的监视器(即对象的锁)然后才能够进行相关操作。 Lock的使用离不开它的实现类AQS,而它的加锁并不是针对对象的,而是针对当前线程的,并且AQS中有一个原子类state来进行加锁次数的计数。
3、获锁失败 使用关键字synchronized加锁的程序中,获锁失败的对象会被加入到一个虚拟的等待队列中被阻塞,直到锁被释放;1.6以后加入了自旋操作,当自旋到一定次数并且获锁操作未成功,则synchronized会升级为重量级锁。 使用Lock加锁的程序中,获锁失败的线程会被自动加入到AQS的等待队列中进行自旋,自旋的同时再尝试去获取锁,等到自旋到一定次数并且获锁操作未成功,线程就会被阻塞。
4、Lock可以获取锁的状态而synchronized无法获取锁的状态。
5、解锁操作 synchronized:不能指定解锁操作,执行完代码块的对象会自动释放锁 Lock:可调用ulock方法去释放锁比synchronized更灵活