Java锁机制

什么是锁?

Java中的锁指的是实现资源同步时的一种机制,在多线程环境中强制资源的访问限制。

为什么要加锁?

        当一个线程对一个资源进行操作时,一步操作可以分为三步指令,读、加、写(具体就是,读取操作数到临时变量,临时变量进行操作,操作完后写回内存),多个线程运行的时候,共享了同一块资源,在访问这块资源的时候就称为临界资源,多线程访问同一资源时由于三步指令,可能会出现预期之外的问题。为了解决这个问题,我们可以为这块资源加上一把锁,只允许一个线程访问这块资源。

有哪些锁?

独享锁

独享锁是指该锁一次只能给一个线程(ReentrantLock、 Synchronized)

共享锁

共享锁是指该所一次能给多个线程,比如ReadWriteLock

乐观锁

        乐观锁是指一种很乐观的锁,每次操作时都认为别的线程不会修改,所以不加锁,但是在更新的时候会判断这个数据是否被修改过,可以使用CAS去实现(CAS算法:CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作),以及版本号的机制(数据版本机制:实现数据版本机制一般有两种,第一种是使用版本号,第二种是使用时间戳。版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功)解决ABA问题(ABA问题就是拿到的数据看似没有被修改过,实际上已经修改过,但是最后又修改了回去)。

        乐观锁适合于读操作比较多的情景。

悲观锁

        悲观锁是指一种很悲观的锁,认为每次去拿数据的时候别人都会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。

        悲观锁适合于写操作比较多的场景。

自旋锁

        自旋锁是指尝试获取锁的线程不会进入阻塞态,而是一直循环去“敲门”尝试获取锁,这样做减少了IO的次数减少了线程上下文切换的消耗但是加大了CPU的消耗。

可重入锁

        可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁。

分段锁

        分段锁其实是一种锁的设计,并不是具体的一种锁,其并发的实现就是通过将整体数据分割成一段一段的数据,然后在对具体的某一段数据进行加锁。只要多个线程不是同时操作同一段数据,那么多个线程就可以同时操作这个数据,从而实现高效的并发操作。ConcurrentHashMap中的分段锁称为Segment。

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

        这四种锁是专门针对synchronized底层锁升级的,指的是锁的状态,在了解这四种锁之前,需要直到Java虚拟机的对象头结构,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

        1)Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
  (2)Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  Monitor:可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

无锁
        无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
(1)无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
        偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
(1)在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
(2)偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
(3)偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁
        是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
(1) 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
(2)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
(3)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
(4)如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
(5)若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
        是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
        综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

锁升级机制

JVM中默认偏向锁是不打开的,打开偏向锁要对对象都加上一个“名牌”,效率低,所以默认不开启,后续4s后(可修改时间),当new对象时,开始加锁。

总结

以上就是Java的各种锁以及锁的升级过程,学习阶段,有问题请及时指正,感谢支持!

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值