如何正确开各种锁,常见的锁总结超全

其实我们真正用到的锁也就那么两三种,只不过依据设计方案和性质对其进行了大量的划分,接下来我们就来聊聊关于如何正确开各种锁,常见的锁总结超全?以下内容大家不妨参考一二希望能帮到您!

如何正确开各种锁,常见的锁总结超全

如何正确开各种锁,常见的锁总结超全

各种锁总结

  1. 锁是什么?干什么用? 锁主要用来实现资源共享的同步。只有获取到了锁才能访问该同步代码,否则等待其他线程使用结束释放锁。一句话:限制多线程资源竞争
  2. 知道下面这些锁吗? 自旋锁、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、偏向锁、对象锁、线程锁、锁粗化、锁消除、轻量级锁、重量级锁、信号量、独享锁、共享锁、分段锁 、闭锁

其实我们真正用到的锁也就那么两三种,只不过依据设计方案和性质对其进行了大量的划分。

1. 常见的锁

synchronized:它就是一个非公平,悲观,独享,互斥,可重入,重量级

Synchronized a (){ b(); // a已有锁,会使用a的锁 } Synchronized b (){ }

lock:以下两个锁都在JUC包下,是API层面上的实现

  • ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
  • ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

ReentrantLock与synchronized 的区别?

ReentrantLock(可重入锁):

  • 中断等待 ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定: 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
  • ReentrantLock获取锁定有三种方式 lock(),如果获取了锁立即返回,如果别的线程持有锁, 当前线程则一直处于休眠状态,直到获取锁 tryLock(),如果获取了锁立即返回true, 如果别的线程正持有锁,立即返回false tryLock(long timeout,TimeUnit unit),如果获取了锁定立即返回true, 如果别的线程正持有锁, 会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true, 如果等待超时,返回false; lockInterruptibly,如果获取了锁定立即返回, 如果没有获取锁定,当前线程处于休眠状态, 直到获取锁定,或者当前线程被别的线程中断
  • 可实现公平锁 对于ReentrantLock而言, 通过构造函数指定该锁是否是公平锁, 默认是非公平锁。非公平锁的优点在于吞 吐量比公平锁大。
  • 锁绑定多个条件 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
  • 公平锁 AbstractQueuedSynchronizer 简称AQS;抽象的队列同步器;所有抢锁的线程都去queue里面

synchronized(也可重入):

  • 优势:synchronized是在JVM层面上实现的, 不但可以通过一些监控工具监控synchronized的锁定, 而且在代码执行时出现异常,JVM会自动释放锁定, 但是使用Lock则不行,lock是通过代码实现的, 要保证锁一定会被释放,就必须将unLock()放到finally{}中
  • 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock, 但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态; 实际上,我推荐大家以压力测试为准

2. 按照性质分类

公平锁/非公平锁

公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序, 有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平 非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁。 ReentrantLock是通过AQS的来实现线程调度,实现公平锁(AbstractQueuedSynchronizer)

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度

悲观锁:悲观锁认为对于同一个数据的并发操作, 一定是会发生修改的,哪怕没有修改,也会认为修改。 因此对于同一个数据的并发操作,悲观锁采取加锁的形式

悲观锁适合写操作非常多的场景 悲观锁在Java中的使用,就是利用各种锁。

乐观锁:乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。 在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。 乐观的认为,不加锁的并发操作是没有事情的

乐观锁适合读操作非常多的场景 不加锁会带来大量的性能提升 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法, 典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

补充 -- 乐观锁的实现方式

版本号(version)

版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。

时间戳(使用数据库服务器的时间戳)

时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。

待更新字段

待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。

所有字段

所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。

乐观锁几种方式的区别

新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。

独享锁/共享锁

独享锁:独享锁是指该锁一次只能被一个线程所持有。

ReentrantLock是独享锁。 Synchronized是独享锁

共享锁:共享锁是指该锁可被多个线程所持有

ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的, 通过实现不同的方法,来实现独享或者共享。

互斥锁/读写锁

独享锁/共享锁是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁:ReentrantLock

读写锁:读写锁在Java中的具体实现就是ReentrantReadWriteLock

更新锁

U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。

因为当使用共享锁时,修改数据的操作分为两步:

1. 首先获得一个共享锁,读取数据,

2. 然后将共享锁升级为排他锁,再执行修改操作。

这样如果有两个或多个事务同时对一个事务申请了共享锁,在修改数据时,这些事务都要将共享锁升级为排他锁。这时,这些事务都不会释放共享锁,而是一直等待对方释放,这样就造成了死锁。

如果一个数据在修改前直接申请更新锁,在数据修改时再升级为排他锁,就可以避免死锁。

性质

1. 用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;

2. 当被读取的页要被更新时,则升级为X锁;

3. U锁一直到事务结束时才能被释放。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层加锁方法会自动获取锁。

ReentrantLock、Synchronized都是可重入锁,可重入锁的一个好处是可一定程度避免死锁。

如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。

所以所有的锁都应该被设计成可重入的

3. 按照设计分类

自旋锁/自适应自旋锁

自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 非阻塞方式获取锁

自适应自旋锁:如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。

在JDK1.6中引入了自适应的自旋锁。 自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁粗化/锁消除

锁消除:是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized

偏向锁:是指一段同步代码一直被一个线程所访问, 那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问, 偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去, 当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。

重量级锁会让其他申请的线程进入阻塞,性能降低。

分段锁

分段锁是一种锁的设计,并不是具体的一种锁

ConcurrentHashMap并发的实现就是通过分段锁的形式来实现高效的并发操作

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

4. 数据库锁

表锁、行锁、间隙锁、悲观锁、乐观锁、共享锁(读锁)、排他锁(写锁)

行锁

锁的作用范围是行级别。

表锁

锁的作用范围是整张表。

数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

举个例子,一个用户表user,有主键id和用户生日birthday。

当你使用update … where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁;

当你使用update … where birthday=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁。

5. 并发控制会造成两种锁

并发控制会造成活锁和死锁,就像操作系统那样,会因为互相等待而导致。

活锁

定义:指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去。

解决方法:采用“先来先服务”策略可以避免。

死锁

定义:就是我等你,你又等我,双方就会一直等待下去。比如:T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁,死锁这种没有完全解决的方法,只能尽量预防。

预防方法:

一次封锁法,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度;顺序封锁法,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。

系统判定死锁的方法:

超时法:如果某个事物的等待时间超过指定时限,则判定为出现死锁;

等待图法:如果事务等待图中出现了回路,则判断出现了死锁。

对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。

6. 线程阻塞

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)

sleep():睡眠,阻塞而不释放锁

wait():等待,阻塞并释放锁

yield():礼让,暂停当前线程,主动让出自己的CPU时间。

Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。 调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程.

join():插队,当前线程等待join进来的执行完,再继续。

类似sleep,停止当前线程,让join线程先执行完毕,或执行指定的时间。//插队拼接

suspend()和resume():暂停/恢复,有死锁倾向。JDK1.5以后已经废除

两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复。 必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。

join:

public static void main(String[] args) throws InterruptedException { System.out.println("main start"); Thread t1 = new Thread(new Worker("thread-1")); t1.start(); t1.join(); System.out.println("main end"); }

main线程要等到t1线程运行结束后,才会输出“main end”。如果不加t1.join(),main线程和t1线程是并行的。而加上t1.join(),程序就变成是顺序执行了。

7. 线程类常用方法

sleep(): 强迫一个线程睡眠N毫秒。

isAlive(): 判断一个线程是否存活。

join(): 等待线程终止。

activeCount(): 程序中活跃的线程数。

enumerate(): 枚举程序中的线程。

currentThread(): 得到当前线程。

isDaemon(): 一个线程是否为守护线程。

setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)

setName(): 为线程设置一个名称。

wait(): 强迫一个线程等待。

notify(): 通知一个线程继续运行。

setPriority(): 设置一个线程的优先级。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页