网站首页 > 精选教程 正文
上节讲了线程安全和原子性,其实就是并发代码变成同步,意味这代码只有一个人在使用,这样就不会有问题。
(一)Java中的锁
1.自旋锁
为了不放弃CPU执行时间,循环的使用CAS技术对数据尝试进行更新,直至成功。(乐观锁的实现)
2.悲观锁
假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。(从读数据就开始上锁。)
3.乐观锁
假定没有冲突,在修改数据时如果发生数据和之前获取的不一致,则读最新数据,修改后重试修改。(假定能成功,如果不成功也是CAS的操作)
4.独享锁(写)
给资源加上写锁,线程可以修改资源,其他线程不能再加锁(单写)(互斥锁)(就 像谈恋爱,被女朋友锁定了,其他人无法获取你)
5.共享锁(读)
给资源加上读锁只能读不能改,其他线程只能加读锁,不能加写锁(多读)(就像信息已经放给婚介了,谁都可以获取我的信息)
6.可重入锁,不可重入锁
线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码
public class ObjectSyncDemo2 { public synchronized void test1(Object arg) { System.out.println(Thread.currentThread() + " 我开始执行 " + arg); if (arg == null) { test1(new Object()); } System.out.println(Thread.currentThread() + " 我执行结束" + arg); } public static void main(String[] args) throws InterruptedException { new ObjectSyncDemo2().test1(null); } }
7.公平锁,非公平锁
争抢锁的顺序,如果按先来后到,则为公平。
公平锁:线程1拿到锁,线程2在等待,线程3也在等待,线程1执行完释放锁,线程2就拿到这个锁。
非公平锁:线程1拿到锁,线程2在等待,线程3也在等待,线程1执行完释放锁,线程3就拿到这个锁,本来应该线程2拿到锁的,线程3先拿到了。就是不公平。
(二)同步关键字synchronized
- ① 介绍
最基本的线程通信机制,基于对象监视器实现的。
JAVA中的每个对象都与一个监视器相关联,个线程可以锁定或解锁。
一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,知道它们可以获得该监视器上的锁定为止。
- ② 特性
可重入,独享,悲观锁。
- ③ 锁的范围
类锁,对象锁,锁消除,锁粗化
同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)
- ④ 锁消除
发生在编译器级别的一种锁优化方式。完全不需要加锁,却执行了加锁操作。
// 锁消除(jit) public class ObjectSyncDemo { public void test3(Object arg) { StringBuilder builder = new StringBuilder(); builder.append("a"); builder.append(arg); builder.append("c"); System.out.println(arg.toString()); } public void test2(Object arg) { String a = "a"; String c = "c"; System.out.println(a + arg + c); } public void test1(Object arg) { // jit 优化, 消除了锁 StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("a"); stringBuffer.append(arg); stringBuffer.append("c"); // System.out.println(stringBuffer.toString()); } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000000; i++) { new ObjectSyncDemo().test1("123"); } } }
test1()方法中的StringBuffer数以函数内部的局部变量,进作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作
StringBuffer 的源码
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
- ④ 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
// 锁粗化(运行时 jit 编译优化) // jit 编译后的汇编内容, jitwatch可视化工具进行查看 public class ObjectSyncDemo { int i; public void test1(Object arg) { synchronized (this) { i++; } synchronized (this) { i++; } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10000000; i++) { new ObjectSyncDemo().test1("a"); } } }
在test1() 同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下
前提中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也不合理。
public void test1(Object arg) { synchronized (this) { i++; i++; } }
(三)同步关键字加锁原理
- ① JAVA对象头
锁的实现机制与java对象头息息相关,锁的所有信息,都记录在java的对象头中。用2字(32位JVM中1字=32bit=4baye)存储对象头,如果是数组类型使用3字存储(还需存储数组长度)。对象头中记录了hash值、GC年龄、锁的状态、线程拥有者、类元数据的指针。
- ② 几种锁类型
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
- ③ 偏向锁(A线程独占锁,不用上下文切换。对象头标识)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier)
偏向锁的获取
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
偏向锁的设置
关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。
- ④自旋锁(一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。一定要有次数限制)
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。
所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。
自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。
- ④ 轻量级锁(A线程拥有锁,B获取,竞争,自旋)
- 加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
解锁
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。
- ⑤ 重量级锁(B线程自旋获取不到锁,膨胀重量锁,阻塞A线程。直到B执行完。)
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
PS:有数据表明,除去大型互联网公司,80%的系统不存在多线程的竞争的情况,一定要熟悉这几种锁,对以后面试镀金(面试)真的很有用。
- 上一篇: 这应该是把Java中的“锁”讲解的最清楚的一篇文章了
- 下一篇: Java锁机制之五大特性
猜你喜欢
- 2024-11-21 Java中的重重“锁”事
- 2024-11-21 线程进阶:多任务处理——Java中的锁(Unsafe基础)
- 2024-11-21 深入理解MySQL锁机制原理
- 2024-11-21 Java并发锁的原理,你所不知道的Java“锁”事
- 2024-11-21 阿里二面:你知道Java中的同步与锁机制详解?
- 2024-11-21 知识点深度解读系列-JAVA锁
- 2024-11-21 图解Java中的锁:什么是死锁?怎么排查死锁?怎么避免死锁?
- 2024-11-21 Java锁与线程的那些“不可描述”的事儿
- 2024-11-21 让人闻风丧胆的 Mysql 锁机制
- 2024-11-21 Java中各种锁的理解
你 发表评论:
欢迎- 04-11Java面试“字符串三兄弟”String、StringBuilder、StringBuffer
- 04-11Java中你知道几种从字符串中找指定的字符的数量
- 04-11探秘Java面试中问的最多的String、StringBuffer、StringBuilder
- 04-11Python字符串详解与示例(python字符串的常见操作)
- 04-11java正则-取出指定字符串之间的内容
- 04-11String s1 = new String("abc");这句话创建了几个字符串对象?
- 04-11java判断字符串中是否包含某个字符
- 04-11关于java开发中正确的发牌逻辑编写规范
- 最近发表
-
- Java面试“字符串三兄弟”String、StringBuilder、StringBuffer
- Java中你知道几种从字符串中找指定的字符的数量
- 探秘Java面试中问的最多的String、StringBuffer、StringBuilder
- Python字符串详解与示例(python字符串的常见操作)
- java正则-取出指定字符串之间的内容
- String s1 = new String("abc");这句话创建了几个字符串对象?
- java判断字符串中是否包含某个字符
- 关于java开发中正确的发牌逻辑编写规范
- windows、linux如何后台运行jar(并且显示进程名)
- 腾讯大佬私人收藏,GitHub上最受欢迎的100个JAVA库,值得学习
- 标签列表
-
- nginx反向代理 (57)
- nginx日志 (56)
- nginx限制ip访问 (62)
- mac安装nginx (55)
- java和mysql (59)
- java中final (62)
- win10安装java (72)
- java启动参数 (64)
- java链表反转 (64)
- 字符串反转java (72)
- java逻辑运算符 (59)
- java 请求url (65)
- java信号量 (57)
- java定义枚举 (59)
- java字符串压缩 (56)
- java中的反射 (59)
- java 三维数组 (55)
- java插入排序 (68)
- java线程的状态 (62)
- java异步调用 (55)
- java中的异常处理 (62)
- java锁机制 (54)
- java静态内部类 (55)
- java怎么添加图片 (60)
- java 权限框架 (55)
本文暂时没有评论,来添加一个吧(●'◡'●)