Java并发编程:底层实现机制

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79600749 一、volatile的应用1.
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79600749

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转换为汇编指令在CPU上执行。

Java中所使用的并发机制依赖于JVM的实现CPU的指令

一、volatile的应用

如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

1. volatile的定义与实现原理

CPU术语表如下所示:

cpu_concepts_table.jpg

那么,volatile是如何保证可见性的呢?

现在,我们看示例代码,如下所示:

instance = new Singleton(); //instance是volatile变量

转变为汇编代码,如下所示:

0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);

有volatile修饰的共享变量进行写操作时会多出第二行汇编代码。

lock前缀的指令在多核处理器下会引发两件事:

  • 将当前处理器缓存行的数据写回到主内存;
  • 写回主内存操作会使其他CPU里缓存了该内存地址的数据失效。

在详细介绍lock指令之前,我们需要对计算机存储层次结构有一个简单的认识:

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile的两条实现原则:

  • lock指令会引起处理器缓存回写到内存;
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效。

2. volatile的使用优化

使用追加字节的方式来优化队列出队和入队的性能!

为什么追加字节能够提高并发编程的效率呢?

因为对于Intel Core、Atom和Pentium M处理器,其L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行。这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。

是不是在使用volatile变量时都应该追加到64字节?不是,两种场景下不能:

  • 缓存行非64字节宽的处理器;
  • 共享变量不会被频繁地读写。

二、synchronized的应用

1. 锁的实现原理

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。

具体表现为:

  • 对于普通同步方法,锁是当前实例对象;
  • 对于静态同步方法,锁是当前类的Class对象;
  • 对于同步方法块,锁是synchronized括号中配置的对象。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

synchronized所用的锁是存在Java对象头里的。

2. 锁的对比

Java中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁的状态会随着竞争情况逐渐升级,锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

2.1 偏向锁

背景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程得到锁的代价更低,故引入了偏向锁。

方法:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该进程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

biased_lock_procedure.jpg

2.2 轻量级锁

  • 轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,该过程称之为Displaced MarkWord。然后,线程尝试使用CAS将对象头的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  • 轻量级锁解锁

轻量级锁解锁时,会使用原子CAS操作将Displaced MarkWord替换回对象头,如果成功,则表示竞争没有发生。如果失败,则表示当前锁存在竞争,锁就会膨胀成为重量级锁。

lightweight_lock_procedure.jpg

2.3 锁的对比

jvm_lock_comparation.png

三、原子操作的实现原理

原子操作(Atomic Operation):不可被中断的一个或一系列操作。

1. 术语

atomic_operation_concepts_table.jpg

2. 处理器如何实现原子操作

处理器通过两种方式来实现原子操作:

使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,则该处理器可以独占共享内存。

使用缓存锁保证原子性

第二个机制是通过缓存锁来保证原子性。总线锁开销大,缓存锁开销小。

两种情况下,处理器不会使用缓存锁:

  1. 当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行时,处理器会调用总线锁。
  2. 有些处理其不支持缓存锁。

3. Java如何实现原子操作

Java通过两种方式实现原子操作:

使用循环CAS实现原子操作

自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止

例如:

// 使用CAS实现线程安全计数器
private void safeCount() {
    for (;;) {
        int i = atomicI.get();
        boolean suc = atomicI.compareAndSet(i, ++i);
        if (suc)
            break;
    }
}

CAS实现原子操作的三大问题:

(1)ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

解决思路:使用版本号!

(2)循环时间开销大

(3)只能保证一个共享变量的原子操作

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。

除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想要进入同步块的时候,其使用循环CAS的方式来获取锁,当它退出同步块时,其使用循环CAS的方式来释放锁。

四、小结

在本文,我们一起研究了volatile、synchronized和原子操作的实现原理。

目录
相关文章
|
3天前
|
IDE Java 物联网
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
8 0
|
4天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
5天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
5天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
7天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
|
存储 Java 安全
Java并发机制底层实现原理-synchronized
章节目录 synchronized的实现原理与应用 synchronized 重量级锁 1.6版本之前 synchronized 被称之为 重量级锁 1.6版本对 synchronized 进行了优化,主要优化的点在于 减少 获得锁和释放锁带 来的性能消耗,为实现这个目的引入了偏向锁、与轻量级锁。
1488 0
|
11天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
4天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。

热门文章

最新文章