Java -- 锁

39 篇文章 1 订阅
2 篇文章 0 订阅

1. 乐观锁

        乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新数据,通常采用在写时先读出当前版本号然后加锁的方法

        具体过程:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作

        Java中的乐观锁大部分是通过CAS(比较和交换)操作实现的,CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态

2. 悲观锁

        悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁

        Java中的悲观锁大部分基于AQS(抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的SynchronizedReentrantLockSemaphoreCountDownLatch等。该框架下的锁会尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock

3. 自旋锁

        自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫做自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗

        线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,甚至有时线程永远无法获得锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁

3.1 自旋锁的优缺点

优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间

缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋锁过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁

3.2 自旋锁的时间阈值

        自旋锁用于让当前线程占有CPU的资源不释放,等到下次自旋锁获取锁资源后立即执行相关操作。但是如何选择自旋的执行时间呢?如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。因此,对自旋的周期选择将直接影响到系统的性能!

        JDK的不同版本所采取的自旋周期不同,JDK 1.5 为固定 DE 时间,JDK 1.6 引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个最佳时间

4. synchronized

        synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。synchronized属于独占式悲观锁,同时属于可重入锁。在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放资源后才能访问该对象或执行同步代码块

        Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorentermonitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的

  1. synchronized的作用范围
    🌰 synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象
    🌰 synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不是对象
    🌰 synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象
  2. synchronized的实现原理
    synchronized内部包括ContentionListEntryListWaitSetOnDeckOwner!Owner 这6个区域,每个区域的数据都代表锁的不同状态
    🌰 ContentionList :锁竞争队列,所有请求锁的线程都放在竞争队列中
    🌰 EntryList:竞争候选队列,在Contention List 中有资格成为候选者来竞争锁资源的线程被移动到了Entry List中
    🌰 WaitSet:等待集合,调用wait方法后被阻塞的线程将被放在WaitSet中
    🌰 OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck
    🌰 Owner:竞争到锁资源的线程被称为Owner状态线程
    🌰 !Owner:在Owner线程释放锁后,会从Owner的状态变为!Owner

        synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中

        为了防止锁竞争时ContentionList尾部的元素被大量的并发线程进行CAS访问而影响性能,Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中,并指定EntryList中的某个线程(一般是最先进入的线程)为OnDeck线程。Owner线程并没有直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,让OnDeck线程重新竞争锁。在Java中把该行为称为“竞争切换”,该行为牺牲了公平性,但提升了性能

        获取到锁资源的OnDeck线程会变成Owner线程,而未获取到锁资源的线程仍然停留在EntryList中

        Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,直到某个时刻被notify方法或者notifyAll方法唤醒,会再次进入EntryList中。ContentionList中、EntryList、WaitSet中的线程均为阻塞状态,该阻塞是由操作系统来完成的(在Linunx内核下是采用pthread_mutex_lock内核函数实现的)

        Owner线程在执行完毕后会释放锁的资源并变为!Owner状态

        在synchronized中,在线程进入ContentionList之前,等待的线程会先尝试以自旋的方式获得锁,如果获取不到就进入ContentionList,该做法对于已经进入队列的线程是不公平的,因此synchronized是非公平锁。另外,自旋获得锁的线程也可以直接抢占OnDeck线程的锁资源

        synchronized是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间

        JDK 1.6 对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量锁,在升级到重量级锁。这种升级过程叫作锁膨胀。在JDK 1.6 中默认开启了偏向锁和轻量级锁,可以通过-XX:UseBiasedLocking禁用偏向锁

5. ReentrantLock

        ReentranLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器AQS)来实现锁的获取与释放

        独占锁指该锁在同一时刻只能被一个线程获取,而没有获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作

        ReentrantLock支持公平锁和非公平锁的实现。公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的

        ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法

        ReentrantLock如何避免死锁:响应中断、可轮询锁、定时锁

  1. 响应中断: 在synchronized中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求
  2. 可轮询锁: 通过boolean tryLock() 获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false
  3. 定时所锁: 通过boolean tryLock(long time,TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态
    🌰当前线程获取到了可用锁并返回true
    🌰当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断的状态
    🌰当前线程获取锁的时间超过了指定的等待时间,则将返回false。如果设定的时间小于等于0,则该方法将完全不等待

5.1 公平锁与非公平锁

        ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制

        ReentrantLock通过在构造函数ReentrantLock(boolean fair) 中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但是执行效率明显高于公平锁。如果系统没有特殊的要求,一半情况下建议使用非公平锁

5.2 tryLock、lock和lockInterruptibly的区别

🍐tryLock 若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout, TimeUnit unit) 可以增加时间限制,如果超过了指定的时间还没获得锁,则返回fasle
🍐lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁
🍐在锁中断时lockInterruptibly会抛出异常,lock不会

6. synchronized和ReentrantLock的比较

共同点
🍊都是用于控制多线程对共享对象的访问
🍊都是可重入锁
🍊都保证了可见性和互斥性

不同点
🦏ReentrantLock显示获取和释放锁;synchronized隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时必须在finally控制块中进行解锁操作
🦏ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性
🦏ReentrantLock是API级别的,synchronized是JVM级别的
🦏ReentrantLock可以定义公平锁
🦏ReentrantLock通过Condition可以绑定多个条件
🦏二者底层实现不一样:synchronized是同步阻塞,曹勇的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略
🦏Lock是一个接口,而synchronized是Java中的关键字,synchronized是由内置的语言实现的
🦏我们通过Lock可以知道有没有成功获取锁,通过synchronized却无法做到
🦏Lock可以通过分别定义读写锁提高多个线程读操作的效率

7. Semaphore

        Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,知道有其他许可信号被释放

        Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphore.acquire方法默认和ReentrantLock.lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.Interrupt方法中断而取消对许可信号的申请

        此外,Semaphore也实现了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的机制。对公平锁与非公平锁的定义在构造函数中设定

        Semaphore的所释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成

        Semaphore也可以用于时限爱你一些对象池、资源池的构建,比如静态全局对象池、数据库连接池。此外。我们也可以创建计数为1的Semaphore,将其作为一种互斥锁的机制(也叫二元信号量,表示两种互斥状态,表示两种互斥状态),同一时刻只有一个线程获取该锁

8. AtomicInteger

        多线程程序中,注入i++或++i等运算不具有原子性,因此不是安全的线程操作。我们可以通过synchronized或ReentrantLock将该操作变成一个原子操作,但是synchronized和ReentrantLock均属于重量级锁。因此JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger

        AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,它们的实现原理相同,区别在于运算对象的类型不同。还可以通过AtomicReference<V>将一个对象的所有操作都转化成原子操作。AtomicInteger的性能通常是synchronized和ReentrantLock的好几倍

9. 可重入锁

        可重入锁也叫做递归锁,指同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁

10. 公平锁与非公平锁

🐳公平锁(Fair Lock) 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程
🐳非公平锁(Nonfair Lock) 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待

        因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁

11. 读写锁:ReadWriteLock

        在Java中通过Lock接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫做普通锁。为了提高性能,Java提供了读写锁。读写锁分为读锁和写锁两种,多个读锁不互斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的

        如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取该共享数据,则需要使用写锁

12. 共享锁和独占锁

        Java并发包提供的加锁模式分为独占锁和共享锁

独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现
共享锁:允许多个线程同时获取该锁,并发访问共享资源ReentrantReadWriteLock中的读锁为共享锁的实现

        ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对下你给通过集成AQS进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别表示AQS队列中等待线程的锁获取模式

        独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性;因为并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时共享资源

13. 重量级锁和轻量级锁

        重量级锁是基于操作系统的互斥量实现的锁,会导致进程在用户态与内核态之间交换,相对开销较大

        synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高

        JDK 在1.6 版本之后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁

        轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁

14. 偏向锁

        除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况、偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)

        偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率

        在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时

        综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能

        锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级

15. 分段锁

        分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的

16. 同步锁与死锁

        在有多个线程同时被阻塞时,它们之间若相互等待对方释放锁资源,就会出现死锁。为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁

17. 如何进行锁优化

  1. 减少锁持有的时间
    减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间
  2. 减小锁粒度
    减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁
  3. 锁分离
    锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁,他根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据
  4. 锁粗化
    锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分的太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率
  5. 锁消除
    在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值