java锁

一、基础概念

1.1 锁的类型

锁从宏观上分类,分为悲观锁与乐观锁。

1.1.1 乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

乐观锁的特点:不会使用系统内置的锁。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

1.1.2 悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。

java中的悲观锁有Synchronized。

AQS框架下的锁都是悲观锁,但是AQS会先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

使用场景选择

1.并发度低的情况下,使用乐观锁。
2.并发度高的情况下有两种选择
1)先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁
2)直接使用悲观锁

1.1.3 自旋锁

自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态,然后由操作系统介入,来换取阻塞的线程。

自旋锁的优缺点

优点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

缺点
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。

自旋锁时间阈值

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化


1.如果平均负载小于CPUs则一直自旋

2.如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

3.如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

4.如果CPU处于节电模式则停止自旋

5.自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

6.自旋时会适当放弃线程优先级之间的差异

自旋锁的开启
JDK1.6中-XX:+UseSpinning开启;
JDK1.7后,去掉此参数,由jvm控制;

1.1.4 可重入锁

可重入锁: 如果锁具备可重入性,则称作为可重入锁。
像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程对象的分配,而不是基于方法调用的分配。

即当线程获取了某个对象的锁之后,再去调用这个对象其他加锁的方法时,无需再去申请锁。如果不具备可重入性,此时线程A需要重新申请锁,而线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

1.1.5 公平锁和非公平锁

公平锁:公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

优缺点对比

优:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。

缺:整体吞吐效率相对非公平锁要低,等待队列中除一个线程以外的所有线程都会阻塞,CPU唤醒线程的开销比非公平锁要大。

非公平锁:即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
优缺点对比

优:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。

缺:在高并发环境下可能造成线程优先级反转和饿死现象。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

1.公平锁和非公平锁介绍https://blog.csdn.net/qq_33404773/article/details/117328587

1.1.6 读写锁

读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

读锁:多个线程的读操作互不影响,不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

写锁:如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

1.2 线程用户态与内核态切换的代价

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

1.如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
2.如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。

用户态与内核态的更多知识请参考:
@see 用户态与内核态的切换 https://www.cnblogs.com/lirong21/p/4213028.html

synchronized的不足

Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

1.3 锁的粒度

被封锁的对象的粒度。例如数据项、记录、文件或整个数据库,锁粒度越小事务的并行度越高。

二、synchronized

先来看下利用synchronized实现同步的基础:
Java中的每一个对象都可以作为锁。具体表现 为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

2.1 Java对象头

synchronized用的锁是存在Java对象头里的。

Java的对象头通常由两个部分组成,一个是Mark Word存储对象的hashCode或者锁信息,另一个是Class Metadata Address用于存储对象类型数据的指针,如果对象是数组,还会有一个部分存储的是数据的长度

在这里插入图片描述

对象头中Mark Word布局

在这里插入图片描述

2.2 锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率

2.2.1偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发获取锁的,这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
图中的线 程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

在这里插入图片描述

2.2.1.1偏向锁获取流程

1.访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态,如果不是可偏向状态,则设为可偏向状态,且Mark Word中的线程ID为当前线程ID。

2.如果为可偏向状态,则查看Mark Word中线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

3.如果线程ID并未指向当前线程,则通过CAS操作竞争锁并替换Mark Word中,如果竞争成功(说明没有锁占用),则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败(说明有锁占用),执行4。

4.如果CAS获取偏向锁失败,则表示有竞争。
当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

5.执行同步代码。

2.2.1.2 偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,且竞争失败,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着, 如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈 会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁(即升级成轻量级锁),最后唤醒暂停的线程。

2.2.1.3 偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;

在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

偏向锁升级成轻量级锁条件:存在锁竞争,且拥有偏向锁的线程还未执行完。即另一个线程在进行cas替换Mark Word时失败了。

2.2.2 轻量级锁

如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:

在这里插入图片描述

2.2.2.1轻量级锁加锁和解锁流程

1.线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的Mark Word复制到该锁记录中,官方称之为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word 替换为指向当前锁记录的指针。
如果成功,则获得锁,进入步骤3。如果失败执行步骤2

2.线程自旋,自旋成功则获得锁,进入步骤3。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)

3.锁的持有线程执行同步代码,执行完后开始CAS替换Mark Word,即将Displaced Mark Word替换回到对象头。如果成功则表示没有竞争发生,释放锁,CAS替换失败执行步骤4

4.CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程

轻量级锁升级成重量级锁条件:

1.未获取到锁的线程自旋一次cas替换Mark Word失败,升级为重量级锁。
2.获取到锁的线程解锁时,将Displaced Mark Word替换回到对象头失败,则升级为重量级锁。

2.3 重量级锁Synchronized

重量级锁,就是让争抢锁的线程从用户态转换成内核态,释放时又由内核态转为用户态。让cpu借助操作系统进行线程协调。

2.3.1 重量级锁的加锁、解锁简要流程

重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,其他线程执行完释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。

2.3.2 底层实现流程

底层是基于每个对象的监视器(monitor)来实现的。被synchronized修饰的代码,在被编译器编译后在被修饰的代码前后加上了一组字节指令。

在代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问。

在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念并实现了锁的可重入。

当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获取该对象锁。

当执行monitorexit时,锁计数器-1,当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁。

Synchronized的实现原理

Synchronized设计到的队列
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

在这里插入图片描述

Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;

Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;

OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;

Owner:当前已经获取到所资源的线程被称为Owner;

!Owner:当前释放锁的线程。

锁竞争的流程

JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

Synchronized是非公平锁的原因

Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

思考

1.使用 synchronized 修饰静态方法和非静态方法有什么区别

实质就是对象锁和类锁

A: synchronized static是某个类的范围,synchronized static cSync{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

B: synchronized 是某实例的范围,synchronized isSync(){}防止多个线程同时访问这个实例中的synchronized 方法。

2. lock锁和synchronized对比

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;

而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;但正是因为lock可以手动的去释放锁,故在编程中更加的灵活

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
synchronized的等待锁的时间没有限制,而lock可以设置等待的超时时间,超过此时间可以不必等待

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。因此lock的灵活性更高,便于实现定制化的逻辑

5)Lock可以提高多个线程进行读操作的效率。使用ReentrantReadWriteLock的readLock()加读锁而使多个线程可以同时进行读操作

Lock和synchronized的选择

1.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
synchronized的性能要比ReentrantLock差个20%-30%

2.针对应锁里面的代码执行时间较长,则推荐使用synchronized。这样可以减少很多的CAS机制方式的获取和释放锁,减少cpu的性能消耗。

参考资料

1.Java并发编程:Lock https://www.cnblogs.com/dolphin0520/p/3923167.html
2.Java – 偏向锁、轻量级锁、自旋锁、重量级锁https://www.cnblogs.com/lzh-blogs/p/7477157.html
3.书籍《Java并发编程的艺术》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值