Java中所有锁介绍

在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下:

1.公平锁 / 非公平锁

2.可重入锁 / 不可重入锁

3.独享锁 / 共享锁

4.互斥锁 / 读写锁

5.乐观锁 / 悲观锁

6.分段锁

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

8.自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

公平锁/非公平锁

公平锁

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

非公平锁

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

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


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

可重入锁/不可重入锁

可重入锁

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLocksynchronized都是可重入锁

synchronized void setA() throws Exception{Thread.sleep(1000);setB();
}
synchronized void setB() throws Exception{Thread.sleep(1000);
}

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

不可重入锁

不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下:

import java.util.concurrent.atomic.AtomicReference;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()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程不必每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。

把它变成一个可重入锁

import java.util.concurrent.atomic.AtomicReference;public class UnreentrantLock {private AtomicReference<Thread> owner = new AtomicReference<Thread>();private int state = 0;public void lock() {Thread current = Thread.currentThread();if (current == owner.get()) {state++;return;}//这句是很经典的“自旋”式语法,AtomicInteger中也有for (;;) {if (!owner.compareAndSet(null, current)) {return;}}}public void unlock() {Thread current = Thread.currentThread();if (current == owner.get()) {if (state != 0) {state--;} else {owner.compareAndSet(current, null);}}}
}

在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。

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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

独享锁/共享锁

独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,它俩一个是独享一个是共享锁。

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

共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。

另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

互斥锁/读写锁

互斥锁

在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源

读写锁

读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态

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

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。

乐观锁/悲观锁

悲观锁

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

乐观锁

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

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。

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

我们一般有三种方式降低锁的竞争程度: 
1、减少锁的持有时间 
2、降低锁的请求频率 
3、使用带有协调机制的独占锁,这些机制允许更高的并发性。

在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。

其实说的简单一点就是

容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。

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

锁的状态

1.无锁状态

2.偏向锁状态

3.轻量级锁状态

4.重量级锁状态

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

偏向锁

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

轻量级

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

重量级锁

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


自旋锁

我们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,所以这里给大家讲一下什么是自旋锁。

简单回顾一下CAS算法

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

1.需要读写的内存值 V

2.进行比较的值 A

3.拟写入的新值 B

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

什么是自旋锁?

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

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

Java如何实现自旋锁?

下面是个简单的例子:

public class SpinLock {private AtomicReference<Thread> cas = new AtomicReference<Thread>();public void lock() {Thread current = Thread.currentThread();// 利用CASwhile (!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方法释放了该锁。

自旋锁存在的问题

1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

可重入的自旋锁和不可重入的自旋锁

文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

public class ReentrantSpinLock {private AtomicReference<Thread> cas = new AtomicReference<Thread>();private int count;public void lock() {Thread current = Thread.currentThread();if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回count++;return;}// 如果没获取到锁,则通过CAS自旋while (!cas.compareAndSet(null, current)) {// DO nothing}}public void unlock() {Thread cur = Thread.currentThread();if (cur == cas.get()) {if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟count--;} else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。cas.compareAndSet(cur, null);}}}
}

自旋锁与互斥锁

1.自旋锁与互斥锁都是为了实现保护资源共享的机制。

2.无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。

3获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

自旋锁总结

1.自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

2.自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。

3.自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。

4.自旋锁本身无法保证公平性,同时也无法保证可重入性。

5.基于自旋锁,可以实现具备公平性和可重入性质的锁。

【End】

听说你对现在的工作不是特别满意?别担心,老王给你准备一份「Java最常见200+面试题全解析」,助力大家找到更好的工作,这份面试题包含的模块:

  • Java、JVM 最常见面试题解析

  • Spring、Spring MVC、MyBatis、Hibernate 面试题解析

  • MySQL、Redis 面试题解析

  • RabbitMQ、Kafka、Zookeeper 面试解析

  • 微服务 Spring Boot、Spring Cloud 面试解析

扫描下面二维码付费阅读

关注下方二维码,订阅更多精彩内容。

转发朋友圈,是对我最大的支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546644.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python深拷贝,浅拷贝,赋值引用

1.在python中&#xff0c;对象赋值实际上是对象的引用。当创建一个对象&#xff0c;然后把它赋给另一个变量的时候&#xff0c;python并没有拷贝这个对象&#xff0c;而只是拷贝了这个对象的引用 &#xff08;1&#xff09;直接赋值,默认浅拷贝传递对象的引用而已,原始列表改变…

pjtool用到的数据库----oracle范畴

PL/SQL Developer 专门面向Oracle数据库存储程序单元的开发 PL/SQL&#xff1a;过程化SQL语言转载于:https://www.cnblogs.com/ejllen/p/3684890.html

如何让mysql索引更快一点

后端开发&#xff0c;公众号内容包括但不限于 python、mysql、数据结构和算法、网络协议、Linux。技术人怎能只有技术和代码&#xff0c;如果你对投资理财、保险&#xff0c;英语学习、读书写作有兴趣&#xff0c;都欢迎来公众号【谭某人】与我交流&#xff0c;你总会有些收获。…

将本地项目上传到码云(gitee)远程仓库

前提条件&#xff1a; 1、本地电脑上已经安装了 git客户端&#xff0c;未安装的&#xff0c;具体安装过程可以参考此安装链接&#xff1a;https://blog.csdn.net/ezreal_tao/article/details/81609883 2、用户已经在gitee码云上注册完成 具体操作步骤&#xff1a; 1、登录码云…

单线程的Redis为什么却能支撑高并发?

作者&#xff1a;Draveness原文链接&#xff1a;draveness.me/redis-io-multiplexing最近在看 UNIX 网络编程并研究了一下 Redis 的实现&#xff0c;感觉 Redis 的源代码十分适合阅读和分析&#xff0c;其中 I/O 多路复用&#xff08;mutiplexing&#xff09;部分的实现非常干净…

在Windows10上安装WSL使用binwalk命令

Windows 10 推出的WSL 功能可以协助我们直接使用binwalk 分析Windows 内的文件 一、WSL 准备 1、打开控制面板→应用→程序和功能→启动或关闭Windows 功能&#xff0c;打开“适用于Linux 的Windows 子系统”和“虚拟机平台”&#xff0c;随后需要重启。 2、打开Windows 10 自…

最详细的 IDEA调试教程

Debug用来追踪代码的运行流程&#xff0c;通常在程序运行过程中出现异常&#xff0c;启用Debug模式可以分析定位异常发生的位置&#xff0c;以及在运行过程中参数的变化。通常我们也可以启用Debug模式来跟踪代码的运行流程去学习三方框架的源码。在Intellij IDEA中使用好Debug&…

centos 修改ip地址

为什么80%的码农都做不了架构师&#xff1f;>>> 查看IP地址 1 登陆连接centos系统&#xff0c;输入 ifconfig 可以查看到当前本机的IP地址信息&#xff0c;如下图 临时设置IP地址 1 如本机为例&#xff0c;上面查询IP为1.117&#xff0c;输入 ifconfig eth0 &…

经典面试题:如何保证缓存与数据库的双写一致性?

作者&#xff1a;你是我的海啸地址&#xff1a;http://t.cn/EK64FeP只要用缓存&#xff0c;就可能会涉及到缓存与数据库双存储双写&#xff0c;你只要是双写&#xff0c;就一定会有数据一致性的问题&#xff0c;那么你如何解决一致性问题&#xff1f;面试题剖析一般来说&#x…

四、华为鸿蒙HarmonyOS应用开发之Java开发下Page Ability生命周期

系统管理或用户操作等行为均会引起Page实例在其生命周期的不同状态之间进行转换。Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化(比如释放资源),这有助于提升应用的性能和稳健性。 Page生命周期回调 Page生命周期的不同状态转换及其对应的回…

写简历的十大误区

作者&#xff1a;Coody地址&#xff1a;my.oschina.net/hooker/blog/3014656在互联网极速膨胀的社会背景下&#xff0c;各行各业涌入互联网的IT民工日益增大。早在2016年&#xff0c;我司发布了Java、Ios工程师的招聘信息&#xff0c;就Java工程师单个岗位而言&#xff0c;日收…

面试官问你MySQL的优化,看这篇文章就够了

作者&#xff1a;zhangqhsegmentfault.com/a/1190000012155267一、EXPLAIN做MySQL优化&#xff0c;我们要善用 EXPLAIN 查看SQL执行计划。下面来个简单的示例&#xff0c;标注(1,2,3,4,5)我们要重点关注的数据type列&#xff0c;连接类型。一个好的sql语句至少要达到range级别。…

六、华为鸿蒙HarmonyOS应用开发之Java开发模式下的不同Page 间实现页面跳转

不同Page 间导航&#xff0c;实现页面跳转 AbilitySlice作为Page的内部单元&#xff0c;以Action的形式对外暴露&#xff0c;因此可以通过配置Intent的Action导航到目标AbilitySlice。Page间的导航可以使用startAbility()或startAbilityForResult()方法&#xff0c;获得返回结…

MySQL 快速创建千万级测试数据

作者&#xff1a;IKNOW本尊链接&#xff1a;http://t.cn/E96Shud背景在进行查询操作的性能测试或者 sql 优化时&#xff0c;我们经常需要在线下环境构建大量的基础数据供我们测试&#xff0c;模拟线上的真实环境。总不能让我去线上去测试吧&#xff0c;会被DBA砍死的创建测试数…

【面试收藏】Redis最全面试题

出自&#xff1a;https://github.com/CyC2018/CS-Notes程序员乔戈里整理一、概述二、数据类型STRINGLISTSETHASHZSET三、数据结构字典跳跃表四、使用场景计数器缓存查找表消息队列会话缓存分布式锁实现其它五、Redis 与 Memcached数据类型数据持久化分布式内存管理机制六、键的…

八、华为鸿蒙HarmonyOS应用开发之Java UI框架、常用TextField组件使用

一、TextField组件 二、TextField组件实例测试 1.新建基于empty ability(Java)手机应用程序的TextField工程。运行出现如下结果 2.在resources->zh.element文件夹下string.json文件修改代码如下: {"string": [{"name": "entry_MainAbility&qu…

面试题:彻底搞懂 Cookie 和 Session

我在做面试官的时候&#xff0c;曾经问过很多朋友这个问题&#xff1a; Cookie 和 Session 有什么区别呢&#xff1f;大部分的面试者应该都可以说上一两句&#xff0c;比如&#xff1a;什么是 Cookie&#xff1f;什么是 Session&#xff1f;两者的区别等。但如果再往深入探讨的…

局域网内Windows允许其他电脑指定IP访问本地mysql-8.0.23数据库

第一步:用ping命令测试两台电脑是否连通 如果两台电脑是连通的请转到第二步,如果是非连通的请进行如下操作: 1、进入控制面板,打开Windows Defender 防火墙,点击高级设置(本人用的是Windows10,其他Windows版本操作类似) 2、在新弹出的页面中点击入站规则,在入站规则中…

经典面试题:聊一聊垃圾回收算法

关于垃圾回收算法的这道面试题&#xff0c;几乎是所有 3 年以上的 Java 面试中必问的题目&#xff0c;甚至有些好一点的公司会在校招时问到面试者。那么本文就系统的讲一下垃圾回收的算法&#xff0c;和 Hotspot 虚拟机执行垃圾回收的一些实现细节&#xff0c;比如安全点和安全…

九、华为鸿蒙HarmonyOS应用开发之Java UI框架、常用Image组件使用

一、Image 二、Image组件实例测试 1.新建基于empty ability(Java)手机应用程序的ImageTest工程。运行出现如下结果 2.在Project窗口,打开“entry > src > main > resources > base > media”,添加4个图片至media文件夹下,以“pic1.jpg”、“pic2.jpg”、…