本篇文章给大家带来了关于Java的相关知识,其中主要整理了并发编程的相关问题,包括了Java 内存模型、volatile 详解以及synchronized 的实现原理等等内容,下面一起来看一下,希望对大家有帮助。
本篇文章给大家带来了关于java的相关知识,其中主要整理了并发编程的相关问题,包括了Java 内存模型、volatile 详解以及synchronized 的实现原理等等内容,下面一起来看一下,希望对大家有帮助。 推荐学习:《java视频教程》 一、JMM 基础-计算机原理 Java 内存模型即 Java Memory Model,简称JMM。JMM 定义了Java 虚拟机 (JVM)在计算机内存(RAM)中的工作方式。JVM 是整个计算机虚拟模型,所以 JMM 是隶属于 JVM 的。Java1.5 版本对其进行了重构,现在的 Java 仍沿用了 Java1.5 的版本。Jmm 遇到的问题与现代计算机中遇到的问题是差不多的。 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的。
如果从内存中读取 1M 的 int 型数据由 CPU 进行累加,耗时要多久? 而且现实情况中绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个 I/O 操作是基本上是无法消除的(无法仅靠寄存器来完成所有运算任务)。早期计算机中 cpu 和内存的速度是差不多的,但在现代计算机中,cpu 的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所 以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样 处理器就无须等待缓慢的内存读写了。 在计算机系统中,寄存器是 L0 级缓存,接着依次是 L1,L2,L3(接下来是内存,本地磁盘,远程存储)。越往上的缓存存储空间越小,速度越快,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0 寄存器是 L1 一级缓存的缓存,L1 是 L2 的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一 层的数据的子集。 在现代 CPU 上,一般来说 L0, L1,L2,L3 都集成在 CPU 内部,而 L1 还分 为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache, I-Cache,L1i),分别用于存放数据和执行数据的指令解码。每个核心拥有独立 的运算处理单元、控制器、寄存器、L1、L2 缓存,然后一个 CPU 的多个核心共 享最后一层 CPU 缓存 L3。 二、Java 内存模型(JMM)从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 2.1、可见性 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值, 其他线程能够立即看得到修改的值。 2.2、原子性 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 三、volatile 详解3.1、volatile 特性可以把对 volatile 变量的单个 public class Volati { // 使用volatile 声明一个64位的long型变量 volatile long i = 0L;// 单个volatile 变量的读 public long getI() { return i; }// 单个volatile 变量的写 public void setI(long i) { this.i = i; }// 复合(多个)volatile 变量的 读/写 public void iCount(){ i ++; }}
public class VolaLikeSyn { // 使用 long 型变量 long i = 0L; public synchronized long getI() { return i; }// 对单个的普通变量的读用同一个锁同步 public synchronized void setI(long i) { this.i = i; }// 普通方法调用 public void iCount(){ long temp = getI(); // 调用已同步的读方法 temp = temp + 1L; // 普通写操作 setI(temp); // 调用已同步的写方法 }} 所以 volatile 变量自身具有下列特性:
volatile 虽然能保证执行完及时把变量刷到主内存中,但对于 count++这种非原子性、多指令的情况,由于线程切换,线程 A 刚把 count=0 加载到工作内存, 线程 B 就可以开始工作了,这样就会导致线程 A 和 B 执行完的结果都是 1,都写到主内存中,主内存的值还是 1 不是 2 3.2、volatile 的实现原理
四、synchronized 的实现原理 Synchronized 在 JVM 里的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
synchronized 使用的锁是存放在 Java 对象头里面,Java 对象的对象头由 mark word 和 klass pointer 两部分组成:
锁信息则是存在于对象的 mark word 中,MarkWord 里默认数据是存储对象的 HashCode 等信息。 但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式 4.1、锁的状态对照上面的图中,我们发现锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态, 它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和 释放锁的效率。 4.2、偏向锁引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的 CAS 操作。
步骤 1、 访问 Mark Word 中偏向锁的标识是否设置成 1,锁标志位是否为 01,确认为可偏向状态。
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致 stop the word 操作;
4.3、 轻量级锁轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
4.3.1、自旋锁原理 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。 4.3.2、自旋锁的优缺点 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。 4.3.3、自旋锁时间阈值 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。 但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋次数很重要。
4.3.4、不同锁的比较推荐学习:《java视频教程》 以上就是Java线程学习之并发编程知识点的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |