编辑
2021-09-09
学习
00
请注意,本文编写于 1221 天前,最后修改于 53 天前,其中某些信息可能已经过时。

目录

聊聊java中的21种锁
1.概括
2.详解
2.1公平锁/非公平锁
2.2可重入锁/不可重入锁
2.3排它锁/共享锁
2.4互斥锁/读写锁
2.5乐观锁/悲观锁
2.6分段锁
2.7锁升级(无锁/偏向锁/轻量级锁/重量级锁)
2.7.1无锁
2.7.2偏向锁
2.7.3轻量级锁
2.7.4重量级锁
2.8自旋锁
2.8.1CAS算法
2.8.2自旋锁
2.8.3Java实现自旋锁
2.8.4自旋锁存在的问题
2.8.5自旋锁的优点
2.8.6自旋锁与互斥锁的区别
2.8.7自旋锁总结
2.9锁优化技术(锁粗化、锁消除)
2.9.1锁粗化
2.9.2锁消除
2.10 同步锁
2.11死锁
2.12synchronized
2.13Lock和synchronized的区别
2.14ReentrantLock 和synchronized的区别
3.常问总结

聊聊java中的21种锁

1.概括

  1. 公平锁/非公平锁
  2. 可重入锁/不可重入锁
  3. 悲观锁/乐观锁
  4. 自旋锁
  5. 分段锁
  6. 互斥锁/读写锁
  7. 独享锁/共享锁
  8. 偏向锁/轻量级锁/重量级锁

以上锁分类并不全指锁的状态,有些是锁的特性、设计。

公平锁:按线程申请锁的先后顺序获得锁 非公平锁:线程不是按先后顺序获得锁,效率比公平锁效率高

可重入锁:已获得的锁在内层可重复使用 不可重入锁:已获得的锁在内层不可重复使用

悲观锁:每次都假定访问时会有多个线程,执行代码前都要加锁,加锁成功后,其它线程处于阻塞状态 乐观锁:其实就是无锁,CAS就是无锁,每次都假定访问时只有自己一个线程

自旋锁:当一个线程在获得锁的时候,有其它线程获得锁,那么该线程就循环判断是否能获得锁,直至其它线程释放锁,然后获得锁。

分段锁:它是一种锁的设计,ConcurrentHashMap就是将hash数据分成多段,持有多把锁,其实就是分成多个hashmap,只有当线程访问同一把锁时才会阻塞,提高了效率。

互斥锁:加锁的代码块只能允许一个线程访问,其它线程则会阻塞 读写锁:它的read模式是共享锁、write模式是互斥锁。读锁和读锁能共存,读锁和写锁、写锁和写锁均不能共存

独享锁:就是互斥锁,该锁每次只能有一个线程访问 共享锁:该锁可以有多个线程访问

偏向锁:同一代码块一直被同一线程获得锁,那么下次将自动获得锁,降低获得锁的代价

轻量级锁:当锁是偏向锁时,被其它线程访问,偏向锁就会升级为轻量级锁,通过自旋转来获取锁,不会阻塞,提高性能

重量级锁:当轻量级锁时,线程一直自旋,当自旋次数达到一定次数时,将提升为重量级锁,让其它申请锁的线程进入阻塞状态,降低cpu的负载。synchronized是重量级锁。

2.详解

2.1公平锁/非公平锁

  • 公平锁:公平锁是指多个线程申请锁的顺序来获取锁,类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。

    image-20220909143227215

  • 非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁。在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

    image-20220909143503292

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大

对于Synchronized而言,是一种非公平锁。由于其不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁

2.2可重入锁/不可重入锁

  • 重入锁:也称递归锁。重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提是同一对象或class),简单来说,就是任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,这样的锁就叫做可重入锁。ReentrantLock和Synchronized都是可重入锁。

可重入锁的原理: 通过组合自定义同步器来实现锁的获取与释放。

再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,

释放锁:释放锁时,进行计数自减。

synchronized void setA() throws InterruptedException { TimeUnit.SECONDS.sleep(1); setB(); } synchronized void setB() throws InterruptedException { TimeUnit.SECONDS.sleep(1); }

上面的代码就是可重入锁的一个特点,如果不是可重入锁的话,setB()可能不会被当前线程执行,可能造成死锁。

面试题1: 可重入锁如果加了两把,但是只释放了一把会出现什么问题? 答:程序卡死,线程不能出来,也就是说我们申请了几把锁,就需要释放几把锁。 面试题2: 如果只加了一把锁,释放两次会出现什么问题? 答:会报错,java.lang.IllegalMonitorStateException。
  • 不可重入锁:不可重入锁不可递归调用,递归调用就发生死锁

    // 使用自旋锁来模拟一个不可重入锁 public class UnreentrantLock { private AtomicReference owner = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); for (;;) { if (owner.compareAndSet(null, current)) { return; } } }

    public void unlock() { Thread current = Thread.currentThread(); owner.compareAndSet(current, null); }

    }

ReentrantLock中可重入锁实现:

非公平锁的锁获取方法:

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

在AQS中维护了一个volatile修饰的state成员变量来计数重入次数,避免了频繁的持有释放操作,这样提升了效率,又避免了死锁

补充: 什么是AQS?

AQS(AbstractQueuedSynchronizer)使用一个int成员变量来表示 同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作,状态信息通过protected类型的getState,setState,compareAndSetState来进行操作

AQS支持独占式和共享式这两种同步方法,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式如ReentrantReadWriteLock。

同步器的设计基于模板方法模式,使用方法如下:

  • 继承AbstractQueuedSynchronizer并重写指定方法(重写方法simple,就是对共享资源state的获取和释放)

  • 将AQS组合在自定义同步组件的实现中,并调用模板方法,这些模板方法会调用使用者重写的方法。

2.3排它锁/共享锁

  • 排它锁:排它锁在同一时刻只允许一个线程进行访问

  • 共享锁:共享锁在同一时刻可以允许多个线程访问,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确实每次只能被独占。

    image-20220909143806779

排它锁和共享锁也是通过AQS来实现的

2.4互斥锁/读写锁

  • 独占锁:是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

    image-20220909144257494

  • 互斥锁(排它锁):互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性,互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。

    • 读-读互斥
    • 读-写互斥
    • 写-读互斥
    • 写-写互斥

    互斥锁

  • 读写锁:既是互斥锁,又是共享锁,read模式是共享,write模式是互斥的

读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。

读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。

image-20220909142753739

写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。在Java中的具体实现就是ReadWriteLock。

public interface ReadWriteLock { /** * 获取读锁 */ Lock readLock(); /** * 获取写锁 */ Lock writeLock(); }

2.5乐观锁/悲观锁

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。

回到代码世界中,乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。

image-20220909135353539

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程)。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。

回到代码世界中,一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。

image-20220909135702121

2.6分段锁

分段锁是一种锁的设计,并不是具体的一种锁。在某些情况下我们将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这称为分段锁。容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,因此,分段锁也是提升多并发程序性能的重要手段之一。这就是JDK1.7中ConcurrentHashMap使用的锁分段技术。

ConcurrentHashMap原理:

它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 是基于粒度更小的分段锁,被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,就仅仅针对数组中的一项进行加锁操作,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。

线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

分段锁

在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会降低性能。使用独占锁时保护受限资源的时候,基本上是采用串行方式——每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。

一般有三种方式降低锁的竞争程序:

  • 减少锁的持有时间

  • 降低锁的请求频率

  • 使用带有协调机制的独占锁,这些机制允许更高的并发性

2.7锁升级(无锁/偏向锁/轻量级锁/重量级锁)

JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

锁的状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。这四种状态都不是Java语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化(使用synchronized时)

2.7.1无锁

无锁状态其实就是上面讲的乐观锁,不再赘述。

2.7.2偏向锁

偏向锁是指是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁,以降低获取锁的代价。

image-20220909145703703

偏向锁是JDK6时加入的一种锁优化机制:在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。

缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。

2.7.3轻量级锁

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁(等待上一个线程释放锁)。轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。

image-20220909145609731

轻量级锁优缺点:

优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。

缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

2.7.4重量级锁

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁,简单来说就是一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞,性能降低。

image-20220909144702422

升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。

重量级锁是一种称谓:synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的Mutex Lock来实现。操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。为了优化synchonized,引入了轻量级锁,偏向锁。

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。

2.8自旋锁

CAS算法是乐观锁的一种实现方式,CAS算法中有涉及到自旋锁

2.8.1CAS算法

CAS是英文单词Compare and Swap(比较并交换),是一种无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS算法涉及到3个操作数

  • 需要读写的内存值V

  • 进行比较的值A

  • 写入的新值B

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断地重试

2.8.2自旋锁

自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断锁是否能够被成功获取,直到获取到锁才会退出循环。

image-20220909135929702

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就是说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

自旋锁的优点:避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。

自旋锁的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

自旋锁次数默认值:10次,可以使用参数-XX

来自行更改。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。

2.8.3Java实现自旋锁

public class SpinLock { private AtomicReference<Thread> cas = new AtomicReference<>(); 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()方法释放了该锁

2.8.4自旋锁存在的问题

  • 如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高

  • 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在线程饥饿问题

2.8.5自旋锁的优点

  • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

  • 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

2.8.6自旋锁与互斥锁的区别

自旋锁与互斥锁都是为了实现保护资源共享的机制 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放

2.8.7自旋锁总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁
  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)
  • 自旋锁如果持有锁的时间太长,则会导致其他等待获取锁的线程耗尽CPU
  • 自旋锁本身无法保证公平性,同时也无法保证可重入性
  • 基于自旋锁,可以实现具备公平性和可重入性的锁

2.9锁优化技术(锁粗化、锁消除)

2.9.1锁粗化

锁粗化是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。简单来说就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。

锁粗化

举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。

private static final Object LOCK = new Object(); for(int i = 0;i < 100; i++) { synchronized(LOCK){ // do some magic things } }

经过锁粗化后就变成下面这个样子了:

synchronized(LOCK){ for(int i = 0;i < 100; i++) { // do some magic things } }

2.9.2锁消除

锁消除是一种优化技术:就是把锁干掉。指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。

那如何判断共享数据不会被线程竞争?

利用逃逸分析技术:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。

在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了。

锁消除

举个例子让大家更好理解。

public String test(String s1, String s2){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); return stringBuffer.toString(); }

上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。

test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。

我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除。

StringBuffer.class // append 是同步方法 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }

2.10 同步锁

同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。

同步锁

2.11死锁

**死锁是一种现象:**如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。

Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁。

死锁

2.12synchronized

synchronized是Java中的关键字:用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。

  • 1.作用于实例方法时,锁住的是对象的实例(this);
  • 2.当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁, 会锁所有调用该方法的线程;
  • 3.synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

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

synchronized

2.13Lock和synchronized的区别

Lock 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  • 1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
  • 2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
  • 3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  • 4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  • 5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  • 6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

synchronized的优势:

  • 足够清晰简单,只需要基础的同步功能时,用synchronized。
  • Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
  • 使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。

2.14ReentrantLock 和synchronized的区别

ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

image-20220909155231820

相同点:

  • 1.主要解决共享变量如何安全访问的问题
  • 2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
  • 3.保证了线程安全的两大特性:可见性、原子性。

不同点:

  • 1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
  • 2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
  • 3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
  • 4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
  • 5.ReentrantLock 通过 Condition 可以绑定多个条件

3.常问总结

锁图

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:wjc

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 ‌CC BY-NC 许可协议。转载请注明出处!