Java并发编程的艺术(二)

简介

本系列为《Java并发编程的艺术》读书笔记。在原本的内容的基础上加入了自己的理解和笔记,欢迎交流!

chapter 2:Java并发机制的底层实现

之前学习Java多线程的时候,使用过synchronized,这种锁称为重锁,而volatile被称为轻量锁,具有可见性,可见性是指一个线程修改一个共享变量时,其他线程可以读取修改后的值 。
书中给出了一些术语,之后会频繁出现,作者介绍详尽,不再补充:
在这里插入图片描述

volatile关键字

在Java中,如果一个变量被该关键字修饰,那么所有线程看到这个变量的值一定是相同的(共享)。

//在类中声明
volatile instance = new Singleton();


// 使用工具查看生成的汇编代码,会发现在声明变量的后面多一句内容:
Lock addl $0x0, (%esp);

Lock 指令会引发CPU将指定位置的数据写回到内存中,一旦对应的数据发生了改变,所有cache中的对应的记录都会变成脏数据因为计算机中的高速缓存必须维持数据的版本一致。这种协议称为缓存一致协议
通常 来说,每个CPU有自己对应的cache,而且cache通常有多级。当一个Lock信号在总线上广播时,别的CPU会拦截写回内存的地址,并去自己对应的缓存中检测是否命中,如果命中则将对应的缓存行的脏读位置为1。总的来说,volatile实现的原则有两点:

  1. Lock指令触发处理器缓存写回到内存中;
  2. 一致性协议使得别的处理器的缓存中对应的缓存行失效,所以就必须去内存中读取值。

就目前而言,处理器的操作过程是:所有持有该数据的处理器的cache会将对应的缓存行锁定(因为数据是被修改了,所有不允许别的处理器来访问这些脏数据),通过缓存一致性协议保证修改被写回到内存中。该操作称为缓存锁定

Synchronized关键字

Java中每个对象都是可以作为锁,具体情况可以划分成以下几种:

  1. 普通对象的锁是当前实例对象;
  2. 静态同步方法,锁是当前类的Class对象;
  3. 同步方法块,锁是Synchronized括号内的对象,即1,2。

JVM中的线程想要访问同步代码块必须获取锁,基于的是Monitor对象。代码的块的同步是使用monitorentermonitorexit指令实现的,这两个指令会在Java编译成字节码的时候被插入在指定的位置

同步代码块在编译时会在开头插入monitorenter,在结束处和异常处插入monitorexit。

Java对象头

每个对象都会有对象头,用来存储一些元数据synchronized锁的相关信息就存储在对象头。

之所以每个对象都可以作为一个锁,是因为在java对象头中保存了对应的信息。前面提到的CAS算法其实就用来修改Java对象头的。

数组对象的对象头包括3个字长,比非数组对象多一字长用于表示数组的长度。
在这里插入图片描述
Mark word的内容如下:

  1. 对象的HashCode
  2. 分代年龄
  3. 锁标记位

锁的升级与比较

在Java 1.6之后,锁一共有四种状态,对应的优先级从低到高:

1. 无锁

即没有加锁。
在这里插入图片描述

2. 偏向锁:只有一个线程访问锁

在这里插入图片描述

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录存储锁偏向的线程ID以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。如果发生了线程对数据的争用,那么偏向锁就会撤销
注意:偏向锁只是加速了一个线程多次访问一个同步块,且没有发生争用,适用于线程对锁的竞争度不高的情况。

偏向锁的作用主要是加速线程争用情况不严重的场景,锁标记指示当前对象持有的偏向锁,此时线程会检查markword中的线程ID是否是自己,如果不是自己那么去检查是否设置了偏向锁标志:

  1. 设置为1:使用CAS将当前线程id修改到线程ID:
    1. 成功:获取偏向锁; 此时 : TID|Epoch|对象分代年龄|1|01
    2. 失败:撤销偏向锁;此时: null|Epoch|对象分代年龄|0|01 。此后偏向锁失效,因为发生了锁争用。
  2. 设置为0:那么就去竞争锁,此时偏向锁失效,偏向锁的撤销必须是在安全点。
    在这里插入图片描述

3. 轻量级锁:适用于争用情况不严重的场景

加锁 在这里插入图片描述

线程在访问同步代码块之前,会在当前的线程的系统栈中创建存储锁记录的空间(多个锁)

每访问一个同步代码块就会将这个锁对象(A对象)的mark word(理解为对象唯一标识)复制到该空间中,并使用CAS(原子性)操作将A对象的mark word中的对应的字段修改成当前的线程锁记录空间的地址这里的理解就是,一旦发现当前的mark word被修改了,那么别的线程使用CAS操作就无法修改,会进入自旋或者锁膨胀

这个过程就是将对象A的mark word(唯一标识)拿走,放到自己的私有区域(线程栈),并告诉别人现在这个锁被获取了(因为原本存储mark word的位置存储到了一个锁空间的地址)。

如果成功则获取锁,如果失败则使用自旋的方式不断尝试获取锁;

解锁

解锁就很好理解了。和加锁的过程相反,就是把原本拿走的mark word还回去,表示我用完了,当别的线程再次访问这个对象时,会发现这个位置的值确实是对象本身的mark word,此时就会获取成功。
如果此时有线程在争用这个锁,那么轻量级锁就会失效,升级成重量锁,那么拿到锁的线程在使用CAS解锁就会失败,此时释放锁的线程需要唤醒因为该锁而阻塞的线程
在这里插入图片描述

4. 重量级锁:多个线程同时访问一把锁

由于锁的争用且自旋一段时间无法获取锁,此时线程就会将当前的锁修改成重量级锁。重锁会让当前线程进入阻塞状态,会被加入到同步队列中。

原子性操作

先明确一些术语:
在这里插入图片描述

首先我们来看一下处理器(32位的IA-32处理器)是如何实现原子性操作的:

  1. 对总线进行加锁:处理器提供一个LOCK信号,当有一个LOCK信号被输出到总线上时,其他处理器的请求将被阻塞。但是CPU和内存之间的通信也会被阻塞
  2. 对缓存机进行加锁总线加锁使得其他的处理器不可以操作其他内存地址的数据。如果一个内存地址的数据被缓存到了cache中,那么在执行锁操作期间,当发生了写回动作时会去更新缓存行中的数据,利用缓存一致性来保证数据的可靠性。
  3. 缓存一致性: 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中的缓存该变量的缓存行是无效的,那么它就会重新从内存中读取。

作者详尽的表述了Java如何实现原子性操作的,总结起来就是锁和循环的CAS操作。不断地使用CAS操作尝试去修改状态,如果成功则代表获取到了锁。
CAS存在的问题和解决办法:

  1. ABA问题:引入版本号;
  2. 循环时间过长:使用pause指令
    1. 延迟流水线的执行;
    2. 避免在退出循环时因为内存的冲突引发流水线清空;
  3. 无法保证多个变量的共享操作
    1. 使用锁;
    2. 将多个共享变量
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值