Java的锁机制分析

一、前言

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。

二、锁的分类

锁的分类目录:
在这里插入图片描述
一、乐观锁VS悲观锁

乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,最常使用的是CAS算法(CAS算法原理);
悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候回显加锁,确保数据不会被别的线程修改。Java中,Synchronized关键字和Lock的实现类都是悲观锁;

在这里插入图片描述

根据上面的概念秒速我们可以发现:
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确;
乐观锁适合读操作多的场景,不加锁的特点就能够使其读操作的性能大幅提升;

二、自旋锁VS适应性自旋锁

2.1 首先,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态的转换需要耗费处理器时间,如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

2.2 自旋锁:在许多的场景中,同步资源的锁定时间很短,也就是线程执行完成的时间很短(执行完后释放锁),为了这一小段时间去切换线程,线程的挂起和恢复可能会让系统得不偿失,因为我们可以让当前线程进行自旋,如果在自旋完成后前面获取锁的线程已经释放锁,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销;

在这里插入图片描述
2.1.1自旋锁的缺点:

自旋锁本身是有缺点的,它不能代替阻塞,自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用时间很长,那么自旋的线程只会拜拜浪费处理器资源。因为,自旋等待的时间必须要有一定的限定,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改),没有成功获得锁,就应当挂起线程。

2.1.2自旋锁的实现原理:

自旋锁的实现原理同样是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
JDK6中默认开启自旋锁,并引入自适应的自旋锁(适应性自旋锁)。
适应性自旋锁:自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源;

三、无锁VS偏向锁VS轻量级锁VS重量级锁

这四种锁是指锁的状态,专门针对Synchronized的。 首先为什么Synchronized能实现线程同步?
在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”

3.1Java对象头

Synchronized是悲观锁,在操作同步资源之前需要给同步资源加锁,这把锁是存在Java对象头的里;

3.1.1以HotsPot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段),Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息,这些信息都是与对象自身定义无关的数据,所有Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储更多的数据,它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化;
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

3.2Monitor

Monitor可以理解为一个同步工具或一个同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁;
Monitor是线程私有的数据结构,每一个线程都有一个可用的Monitor record列表,同时还有一个全局的可用列表,每一个被锁住的对象都会和一个Monitor关联,同时Monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用;

Synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步;

如同我们在自旋锁中提到的 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”,这种方式就是Synchronized最初实现同步的方式,这就是JDK 6之前Synchronized效率低的原因。这种依赖于操作系统Mutex Lock实现的锁我们称之为"重量级锁",JDK 6中为了减少获得锁和释放锁带来的性能消防,引入了"偏向锁"和"轻量级锁";

3.3锁的状态

锁一共有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级;
在这里插入图片描述
3.3.1无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能够修改成功;
特点:修改操作在循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试,CAS原理及应用即是无锁的实现;

3.3.2偏向锁

3.3.2.1偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价;
3.3.2.2当一个线程访问同步代码块并获取锁时,会在Mark Word存储锁偏向的线程ID,在线程进入或退出同步代码块时,不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可;
3.3.2.3 锁的释放:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被 锁定状态,撤销偏向锁后恢复无锁(标志位为“01”)或轻量级锁(标志位为"00")的状态;
PS:偏向锁在JDK 6及以后的JVM里是默认启用的,可以通过JVM参数关闭偏向锁: XX:UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态;

3.3.3轻量级锁

3.3.3.1轻量级锁是指挡锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能;
3.3.3.2在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为"01"状态,是否为偏向锁为"0"),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)空间,用于存储对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中;
3.3.3.3拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并在Lock Record里的owner指针指向对象的Mark Word,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态;

3.3.3.4 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁;
3.3.3.5若当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程在只有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁;

3.3.4重量级锁

升级为重量级锁时,锁标志的状态值为"10",此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态;

整体的锁状态升级过程如图:
在这里插入图片描述

综上,偏向锁通过对比Mark Word解决加锁问题,避免多次执行CAS操作。而轻量级锁通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将出了拥有锁的线程以外的线程都阻塞,需要频繁切换CPU,来进行加锁和释放锁;

四、公平锁VS非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁;
优点:等待锁的线程不会饿死;
缺点:整体吞吐效率相对非公平锁要低,等待列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大;

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好够用,那这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现申请锁的线程先获取锁的场景;
优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程;
缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁;

五、可重入锁VS非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法自动获取锁(前提锁对象得是同一个对象或者Class),不会因为之前获取过还没释放而阻塞;
优点:可一定程度避免死锁;
PS:Java中的ReentrantLock和Synchronized都是可重入锁;

代码示例:
在这里插入图片描述

在上面的代码中,类中的两个方法都是被内置锁Synchronized修饰的,doSomething()方法调用doOthers()方法,当执行doSomething()方法时,由于锁是可重入的,因为此方法在调用doOthers()方法时,不必释放锁,也是直接获取doOthers()方法的锁;
如果是不可重入锁,当前线程获取锁,执行doSomething()方法,在调用doOthers()方法之前必须释放掉doSomething()方法的锁,但是此锁已经被当前线程所持有,在方法完成之前都不会释放锁,所以此时会出现死锁;

六、独享锁VS共享锁

独享锁也叫排它锁,是指该锁一次只能被一个线程所持有,如果线程A对数据1加上排它锁后,则其它线程不能再对数据1加任何类型的锁,获得排它锁的线程即能读数据又能修改数据;
PS:JDK中的Synchronized和JUC中Lock的实现类就是互斥锁;

共享锁是指该锁可被多个线程所持有,如果线程A对数据1加上共享锁后,则其它线程只能对A再加共享锁,不能加排它锁,获得共享锁的线程只能读数据,不能修改数据;

ReetrantReadWriterLock有两把锁:ReadLock(读锁)和WriteLock(写锁),两个锁是靠内部类Syn实现的锁,Syn是AQS的一个子类,这种结构在CountDownLatch、ReetrantLock、Semaphore里面也存在;
在ReetrantReadWriterLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样,读锁是共享锁,写锁是独享锁,读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升;

三、Synchronized和Lock

一、Java中每一个对象都可以作为锁,这是Synchronized实现同步的基础;

在不同方法上加Synchronized,被当成锁的对象的不同:
1.普通同步方法,把当前实例对象当成锁;
2.静态同步方法,把当前类的Class对象当成锁;
3.同步方法块,把括号里的对象当成锁;

二、Synchronized和Lock的区别
在这里插入图片描述

1.来源:
Lcok是一个接口,Synchronized是Java的一个关键字,Synchronized是内置的语言实现;
2.异常是否释放锁:
Synchronized在发生异常时候回自动释放占有的锁,因此不会出现死锁;而Lock发生异常的时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发送;
3.是否响应中断:
Lock等待锁过程中可以用interrupt来中断等待,而Synchronized只能等待锁的释放,不能响应中断;
4.是否知道获取锁:
Lock可以通过trylock()来知道有没有获取锁,而Synchronized不能;
5.Lock可以提高多个线程进行读操作的效率:(可以通过ReadWriteLock实现读写分离)
6.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于Synchronized。所以说,在具体使用时要根据适当情况选择;
7.Synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度;

锁机制的区别:

Synchronized原始采用的是CPU悲观锁机制,即线程获得是独占锁,独占锁意味着其它线程只能依靠阻塞来等待线程释放锁,而在CPU转换多线程阻塞时会引起上下文去切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低;
Lock采用的是乐观锁机制,所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,乐观锁实现的机制就是CAS操作(Compare
and Swap);

四、Synchronized和Volatile

可参考文章:Java中的Volatile

五、死锁

可参考文章:死锁的四个必要条件和解决办法

六、最后!!

此文档参考:Java中的各种机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值