Java中的锁有不少种,常常会听到“锁”这个词。面试
犹如天天出门时,🔑就是一种“锁”,拿不到🔑,就进不去了。算法
Java那么多种类的锁,都是按不一样标准来分类的。就像商店里的各类商品,能够按式样,也能够按颜色或者尺寸。编程
其实它们都是一种思想。安全
一个进程能够包含多个线程,那么多个线程就会有竞争资源的问题出现,为了互相不打架,就有了锁的概念了。 一个线程也能够本身完成任务,但就像一个小组能够互相配合、共同完成任务,比一我的要快不少是否是?bash
整理个大图~多线程
其实这么多分类只是从特性、表现、实现方式等不一样的侧重点来讲的,不是绝对的分类,例如,不可重入锁和自旋锁,实际上是同一种锁。并发
要绕晕了是否是?下面就分别来讲说。函数
林妹妹比较悲观,宝玉比较乐观~高并发
看名字便知,它是悲观的,老是想到最坏的状况。 锁也会悲观,它并非难过,它只是很谨慎,怕作错。性能
每次要读data的时候,老是以为其余人会修改数据,因此先加个🔐,让其余人不能改数据,再慢慢读~
要是你在写一篇日记,怕别人会偷看了,就加了个打开密码,别人必须拿到密码才能打开这篇文章。这就是悲观锁了。
应用: synchronized关键字和Lock的实现类都是悲观锁。
它很乐观,老是想着最好的状况。 它比较大条,不会太担忧。若是要发生,总会发生,若是不会发生,那就不会。为何要担忧那么多?
每次读data时,老是乐观地想没有其余人会同时修改数据,不用加锁,放心地读data。 但在更新的时候会判断一下在此期间别人有没有去更新这个数据。
就像和别人共同编辑一篇文章,你在编辑的时候别人也能够编辑,并且你以为别人不会改动到你写的部分,那就是乐观锁了。
事事无绝对,悲观也好乐观也好,没有绝对的悲观,也没有绝对的乐观。只是在这个当时,相信,仍是不相信。
类型 | 实现 | 使用场景 | 缺点 |
---|---|---|---|
悲观锁 | synchronized关键字和Lock的实现类 | 适合写操做多的场景,能够保证写操做时数据正确 | 若是该事务执行时间很长,影响系统的吞吐量 |
乐观锁 | 无锁编程,CAS算法 | 适合读操做多的场景,可以大幅提高其读操做的性能 | 若是有外来事务插入,那么就可能发生错误 |
是乐观锁的一种实现方式。
简单来讲,有3个三个操做数:
没有绝对的公平,也没有绝对的不公平。
公平,就是按顺序排队嘛。 公平锁维护了一个队列。要获取锁的线程来了都排队。
非公平,上来就想抢到锁,好像一个不讲道理的,抢不到的话,只好再去乖乖排队了。 非公平锁没有维护队列的开销,没有上下文切换的开销,可能致使不公平,可是性能比fair的好不少。看这个性能是对谁有利了。
举个栗子
广义上的可重入锁,而不是单指JAVA下的ReentrantLock。
可重入锁,也叫作递归锁,指的是同一线程外层函数得到锁以后,内层递归函数仍然有获取该锁的代码,但不受影响。
这句话神马意思?
这种锁是能够反复进入的。
当一个线程执行到某个synchronized方法时,好比说method1,而在method1中会调用另一个synchronized方法method2,此时线程没必要从新去申请锁,而是能够直接执行方法method2。
class MyClass {
public synchronized void method1() {
enterNextRoom();
}
public synchronized void method2() {
// todo
}
}
复制代码
两个方法method1和method2都用synchronized修饰了。
假设某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而因为method2也是synchronized方法,假如synchronized不具有可重入性,此时线程A须要从新申请锁。可是这就会形成一个问题,由于线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
若是不是可重入锁的话,method2可能不会被当前线程执行,可能形成死锁。
可重入锁最大的做用是避免死锁。
实现类:
按上面的例子,线程A从method1执行到method2的时候,不能直接获取到锁,要执行下去,必须先解锁。
实现不可重入锁有什么方式呢?那就是自旋~
(什么是自旋锁?等下详细说,先有个概念,就是当一个线程在获取锁的时候,若是锁已经被其它线程获取,那么该线程将循环等待)
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
复制代码
同一线程两次调用lock()方法,若是不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁。
自旋锁就是,若是此时拿不到锁,它不立刻进入阻塞状态,而愿意等待一段时间。
相似于线程在那里作空循环,若是循环必定的次数还拿不到锁,那么它才会进入阻塞的状态,这个循环次数是能够人为指定的。
有一天去全家买咖啡,服务员说真不巧,前面咖啡机坏了,如今正在修,要等10分钟喔,刚好没什么急事,那就等吧,坐到一边休息区等10分钟(其它什么事都没作)。介就是自旋锁~
那是否是有点浪费?若是你等了15分钟,还没修好,那你可能不肯意继续等下去了(15分钟就是设定的自旋等待的最大时间)
上面说自旋锁循环的次数是人为指定的,而自适应旋转锁,厉害了,它不须要人为指定循环次数,它本身自己会判断要循环几回,并且每一个线程可能循环的次数也是不同的。
若是这个线程以前拿到过锁,或者常常拿到一个锁,那它本身判断下来再次拿到这个锁的几率很大,循环次数就大一些;若是这个线程以前没拿到过这个锁,那它就没把握了,怕消耗CPU,循环次数就小一点。
它解决的是“锁竞争时间不肯定”的问题,但也不必定它本身设定的必定合适。
仍是前面去全家等咖啡的栗子吧~ 要是等到5分钟,还没修好,你目测10分钟里也修很差,就再也不等下去了(循环次数小);要是等了10分钟了,服务员说很是抱歉,快了快了,再1分钟就能够用了,你也还不急,都已经等了10分钟了,就多等一下子嘛(循环次数大)
简单实现以下:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
复制代码
lock()方法利用CAS,当第一个线程A获取锁的时候,可以成功获取到,不会进入while循环;
若是此时线程A没有释放锁,另外一个线程B又来获取锁,此时因为不知足CAS,因此就会进入while循环,不断判断是否知足CAS,直到A线程调用unlock方法释放了该锁,线程B才能获取锁。
自旋锁是不阻塞锁,可是它也会等一段时间,那和阻塞锁有什么区别?
去一个热门饭店吃饭,到了门口一看,门口的座位坐满了人……这咋整……服务员说,您能够先拿个号~小票上扫个二维码,关注我们,轮到您了,服务号里就会有提示哒~(很熟悉是否是?) 而后你就先取了号去逛逛周围小店去了,等轮到你了,手机里收到一条服务提醒消息,到你啦~这时你再去,就能够进店了。
这就是阻塞的过程~
若是是自旋锁呢? 就是你本身其它事情都不作,等在那里,就像去超市排队结帐同样,你走开的话是没有人会通知你的,只能从新排队,须要本身时刻检查有没有排到(能不能访问到共享资源)。
插播一下:
阻塞或唤醒一个Java线程须要操做系统切换CPU状态来完成,这种状态转换须要耗费处理器时间。
复制代码
自旋锁 | 阻塞锁 | |
---|---|---|
改变线程状态? | 不改变线程运行状态,一直处于用户态,即线程一直都是active的 | 改变线程运行状态,让线程进入阻塞状态进行等待 |
占用CPU? | 占用CPU时间 | 不会占用CPU时间,不会致使 CPU占用率太高,但进入时间以及恢复时间都要比自旋锁略慢 |
适用场景 | 线程竞争不激烈,而且保持锁的时间段 | 竞争激烈的状况下 阻塞锁的性能要明显高于自旋锁 |
这四种状态都不是Java语言中的锁,而是Jvm为了提升锁的获取与释放效率而作的优化(使用synchronized时),它们会随着竞争的激烈而逐渐升级,而且是不可逆的升级。
P.S., 无锁,即没有锁~
若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性,所以会有一些代码天生就是线程安全的。
CAS算法 即compare and swap(比较与交换),就是有名的无锁算法。
状态 | 描述 | 优势 | 缺点 | 应用场景 | 适用场景 |
---|---|---|---|---|---|
偏向锁 | 无实际竞争,让一个线程一直持有锁,在其余线程须要竞争锁的时候,再释放锁 | 加锁解锁不须要额外消耗 | 若是线程间存在竞争,会有撤销锁的消耗 | 只有一个线程进入临界区 | 适用于只有一个线程访问同步块场景。 |
轻量级 | 无实际竞争,多个线程交替使用锁;容许短期的锁竞争 | 竞争的线程不会阻塞 | 若是线程一直得不到锁,会一直自旋,消耗CPU | 多个线程交替进入临界区 | 追求响应时间。同步块执行速度很是快。 |
重量级 | 有实际竞争,且锁竞争时间长 | 线程竞争不使用自旋,不消耗CPU | 线程阻塞,响应时间长 | 多个线程同时进入临界区 | 追求吞吐量。同步块执行速度较长。 |
你常常去一家店坐在同一个位置吃饭,老板已经记住你啦,每次你去的时候,只要店里客人很少,老板都会给你留着那个座位,这个座位就是你的“偏向锁”,每次只有你这一个线程用。
有一天你去的时候,店里已经坐满了,你的位置也被别人坐了,你只能等着(进入竞争状态),这时那个座位就升级到“轻量级”了。
要是那个座位特别好(临窗风景最佳,能隔江赏月~)每次你到的时候,都有其余好几我的也要去抢那个位置,没坐到那个位置就不吃饭了>_< 那时那个座位就升级到“重量级”了。
轻量级锁和重量级锁的重要区别是: 拿不到“锁”时,是否有线程调度和上下文切换的开销。
简单来讲:若是发现同步周期内都是不存在竞争,JVM会使用CAS操做来替代操做系统互斥量。这个优化就被叫作轻量级锁。
相比重量级锁,其加锁和解锁的开销会小不少。重量级锁的“重”,关键在于线程上下文切换的开销大。
共享 Vs 独享 图~ 是否是很形象? 😄
类型 | 描述 | 实现类 |
---|---|---|
共享(读锁) | 可被多个线程所持有,其余用户能够并发读取数据。若是事务T对数据A加上共享锁后,则其余事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。 | ReentrantReadWriteLock里的读锁 |
独享(排他锁,写锁) | 一次只能被一个线程所持有。若是事务T对数据A加上排它锁后,则其余事务不能再对A加任何类型的锁。得到排它锁的事务即能读数据又能修改数据。 | synchronized、ReentrantLock、ReentrantReadWriteLock里的写锁 |
排它锁是悲观锁的一种实现。
问题来了:共享锁为何要加个“读锁”?
防止数据在被读取的时候被别的线程加上写锁。
复制代码
而独占锁的原理是:
若是有线程获取到锁,那么其它线程只能是获取锁失败,而后进入等待队列中等待被唤醒。
复制代码
小组每一个礼拜都要各个成员共同填一份周报表格,要是每一个人打开的时候,能够加一个写锁,即你在写的时候,别人不能修改,这就是独享锁(写锁); 可是这份表格你们能够同时打开,看到表格内容(读取数据),正在改数据的人能够对这份表格加上共享锁,那这个锁就是共享锁。
共享锁的获取方法为acquireShared,源码为:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
复制代码
当返回值为大于等于0的时候方法结束说明得到成功获取锁,不然,代表获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
简单来讲 addWaiter(Node mode) 方法作了如下事情:
建立基于当前线程的独占式类型的节点; 利用 CAS 原子操做,将节点加入队尾。
ReentrantLock 是一个独占/排他锁。
提供了是否公平锁的初始化:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
复制代码
使用ReentrantLock必须在finally控制块中进行解锁操做。
在资源竞争不激烈的情形下,性能稍微比synchronized差点点。可是当同步很是激烈的时候,synchronized的性能一会儿能降低好几十倍,而ReentrantLock确还能维持常态。
高并发量状况下使用ReentrantLock。
优势: 可必定程度避免死锁。
对Java的各类锁概念作了下整理,写了些本身的理解, 还有不少基础方面,好比Java的对象头、对象模型(都比较基础)、锁的优化、各种锁代码实现等,后续再补充下。 有不少公号有不少高水平的文章,须要理解和练习的有太多。