图解Java中的18 把锁!

  • 乐观锁和悲观锁

  • 独占锁和共享锁

  • 互斥锁和读写锁

  • 公平锁和非公平锁

  • 可重入锁

  • 自旋锁

  • 分段锁

  • 锁升级(无锁|偏向锁|轻量级锁|重量级锁)

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

乐观锁和悲观锁

悲观锁

悲观锁对应于生活中悲观的人,悲观的人总是想着事情往坏的方向发展。

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

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

在 Java 语言中 synchronizedReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。

乐观锁

乐观锁 对应于生活中乐观的人,乐观的人总是想着事情往好的方向发展。

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

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

乐观锁可以使用版本号机制CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。

两种锁的使用场景

悲观锁和乐观锁没有孰优孰劣,有其各自适应的场景。

乐观锁适用于写比较少(冲突比较小)的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。

如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。

独占锁和共享锁

独占锁

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

JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的实现类就是独占锁。

共享锁

共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。

互斥锁和读写锁

互斥锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。

读写锁

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

读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

在 JDK 中定义了一个读写锁的接口:ReadWriteLock

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

ReentrantReadWriteLock 实现了ReadWriteLock接口,具体实现这里不展开,后续会深入源码解析。

公平锁和非公平锁

公平锁

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

在 java 中可以通过构造函数初始化公平锁

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

非公平锁

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

在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(false);

可重入锁

可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。

敲黑板:可重入锁的一个好处是可一定程度避免死锁。

以 synchronized 为例,看一下下面的代码:

public synchronized void mehtodA() throws Exception{// Do some magic tingsmehtodB();
}public synchronized void mehtodB() throws Exception{// Do some magic tings
}

上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。

自旋锁

自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:

public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));return v;
}

CAS 操作如果失败就会一直循环获取当前 value 值然后重试。

另外自适应自旋锁也需要了解一下。

在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。

分段锁

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

分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。

锁升级(无锁|偏向锁|轻量级锁|重量级锁)

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

无锁

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

偏向锁

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

偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

轻量级锁

当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

重量级锁

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

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

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。

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

锁粗化

锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。

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

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}
}

锁消除

锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。

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

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;
}

一张图总结:

前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下:

-- End --


往期推荐

synchronized 的超多干货!


ThreadLocal内存溢出代码演示和原因分析!


ThreadLocal不好用?那是你没用对!



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

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

相关文章

在CSS中使用not:first-child选择器

Introduction: 介绍&#xff1a; Well, selectors are a very common term to deal with while we are developing a website or web page. You might know quite a few of them and might as well be implementing them. You might also have noticed that all the selectors…

linux/unix 段错误捕获【续】

本文为“在C/C中捕获段错误&#xff0c;打印出错的具体位置”的续篇&#xff0c;进一步解决涉及动态链接库的情况。背景知识&#xff1a;linux/unix下动态链接库的基本原理/proc/pid/maps文件的基本格式动态链接库&#xff1a;在进程执行过程中动态加载&#xff0c;进程间可以共…

C#获取屏幕大小的“简单整理”。。

额…… 首先是有一个需求&#xff0c;两个显示器&#xff0c;程序自动获取自己所在的显示器大小&#xff0c;并显示到右下角。 我们都知道C#有一个Screen.PrimaryScreen.Bounds&#xff0c;可以获取屏幕的完整尺寸&#xff0c;但是只能获取主显示器的尺寸&#xff0c;后来我发…

Spring为什么建议构造器注入?

来源 | juejin.cn/post/6844904056230690824作者 | Richard_Yi本文的内容主要是想探讨我们在进行 Spring 开发过程当中&#xff0c;关于依赖注入的几个知识点&#xff0c;具体内容如下&#xff1a;Autowired, Resource, Inject 三个注解的区别当你在使用Autowired时&#xff0…

c#中的long类型示例_C#中带示例的带符号字节数组

c#中的long类型示例C&#xff03;中的有符号字节数组 (Signed Byte Array in C#) In C#.Net, we can create a signed byte array by using sbyte, sbyte is used to store both of the values (negative and positive) between the range of -128 to 127 (Signed 8 bits integ…

Shell中的while循环

while循环的格式while expressiondocommandcommanddone1、计数器控制的while循环主要用于已经准确知道要输入的数据和字符串的数目。举例1 #!/bin/sh2 int13 while(( $int<5 ))4 do5 echo $int6 let "int"7 done2、结束标记控制的while循环主要用于不知道读入数据…

一文玩转 EhCache 缓存框架!

Ehcache 介绍EhCache 从 Hibernate 发展而来&#xff0c;是一个纯Java的进程内缓存框架&#xff0c;具有快速、精干等特点。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存&#xff0c;Java EE和轻量级容器。它具有内存和磁盘存储&#xff0c;缓存加载器&#x…

avr uart打印_AVR | 在16x2 LCD上打印HELLO WORLD

avr uart打印We would learn the connection to the LCD first as the connections is a bit complex and here we are using an 8-bit LCD. 我们将首先学习到LCD的连接&#xff0c;因为连接有点复杂&#xff0c;这里我们使用的是8位LCD 。 Simulation 模拟 Explanation 说明…

SQLite CodeFirst、Migration 的趟坑过程 [附源码]

负二、配置说明 最近想做个东西&#xff0c;用到了SQLite&#xff0c;按照之前的方法步骤搭建的结果失败了&#xff0c;因为SQLite的版本升级了&#xff0c;导致Migration自动迁移各种报错&#xff0c;而且我查了一下自动迁移的包貌是不再更新了。——2018年1月24日 能正常使用…

linux中lvm的缩减

问题提出&#xff1a;服务器硬盘做成了lvm&#xff0c;但是/home目录空间较大&#xff0c;于是想缩减一下&#xff0c;分配给其他目录。实验环境&#xff1a;操作系统&#xff1a;redhat企业版&#xff0c;硬盘已经做成了lvm。问题解决&#xff1a;操作前的注意事项&#xff1a…

SpringBoot 过滤器、拦截器、监听器对比及使用场景!

来源 | blog.csdn.net/qq_38020915/article/details/116431612作者 | dingwen_blog一、关系图理解二、区别1.过滤器过滤器是在web应用启动的时候初始化一次, 在web应用停止的时候销毁可以对请求的URL进行过滤, 对敏感词过滤挡在拦截器的外层实现的是 javax.servlet.Filter 接口…

Java StringBuilder length()方法与示例

StringBuilder类的length()方法 (StringBuilder Class length() method) length() method is available in java.lang package. length()方法在java.lang包中可用。 length() method is used to return the length of this sequence (i.e. it counts the number of characters …

进程通信:匿名管道和命名管道

一、进程间通信方式 管道( pipe )&#xff1a;管道是一种半双工的通信方式&#xff0c;数据只能单向流动&#xff0c;而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。有名管道 (named pipe) &#xff1a; 有名管道也是半双工的通信方式&#xff0c…

Jenkins Build Radiators(构建发射源)

为什么80%的码农都做不了架构师&#xff1f;>>> information radiators&#xff08;信息发射源&#xff09;的概念通常被用在敏捷的圈子里。 据敏捷专家Alistair Cockburn所说&#xff1a; 一个信息发射源是一个贴在一个地方的显示器&#xff0c;当人们工作或路过时…

线程池是如何重复利用空闲的线程来执行任务的?

来源&#xff1a;blog.csdn.net/anhenzhufeng/article/details/88870374在Java开发中&#xff0c;经常需要创建线程去执行一些任务&#xff0c;实现起来也非常方便&#xff0c;但如果并发的线程数量很多&#xff0c;并且每个线程都是执行一个时间很短的任务就结束了&#xff0c…

strictmath_Java StrictMath nextAfter()方法与示例

strictmathStrictMath类的nextAfter()方法 (StrictMath Class nextAfter() method) Syntax: 句法&#xff1a; public static double nextAfter(double starts , double directions);public static float nextAfter(float starts , double directions);nextAfter() method is …

C# 将程序添加开机启动的三种方式

前言 最近在研究程序随系统启动&#xff0c;发现在 win7 上因为权限的问题&#xff0c;写注册表的时候总是会出现问题&#xff0c;写不进去导致的不能自动启动&#xff0c;随后决定仔细的看一看这方面的问题。 查资料过程中主要发现有三种方式可以添加到启动&#xff0c;分别…

SpringBoot 中的 3 种条件装配!

一、介绍在实际的项目开发中&#xff0c;我们往往需要根据不同的环境做出不同的配置&#xff0c;例如&#xff1a;在开发环境下&#xff0c;我们会使用内存数据库以便快速启动服务并进行开发调试&#xff0c;在test环境、生产环境&#xff0c;会使用对应环境的数据库。如果我们…

java中intvalue_Java Short类intValue()方法及示例

java中intvalue短类intValue()方法 (Short class intValue() method) intValue() method is available in java.lang package. intValue()方法在java.lang包中可用。 intValue() method is used to return the value denoted by this Short object converted to type int (by c…

C# Winform 窗体美化(目录)

最近在看 C# Winform 的窗体美化&#xff0c;发现一些很有用的美化皮肤库&#xff0c;学习过后也把一些资料整理一下。 一、IrisSkin 换肤库&#xff08;IrisSkin4&#xff09; 二、LayeredSkin 界面库&#xff08;LayeredSkinDemo&#xff09; 三、不规则窗体&#xff08;G…