mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
427 lines
31 KiB
HTML
427 lines
31 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>58 Java 中的原子操作有哪些注意事项?.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/00 由点及面,搭建你的 Java 并发知识网.md.html">00 由点及面,搭建你的 Java 并发知识网</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/01 为何说只有 1 种实现线程的方法?.md.html">01 为何说只有 1 种实现线程的方法?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/02 如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?.md.html">02 如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/03 线程是如何在 6 种状态之间转换的?.md.html">03 线程是如何在 6 种状态之间转换的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/04 waitnotifynotifyAll 方法的使用注意事项?.md.html">04 waitnotifynotifyAll 方法的使用注意事项?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/05 有哪几种实现生产者消费者模式的方法?.md.html">05 有哪几种实现生产者消费者模式的方法?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/06 一共有哪 3 类线程安全问题?.md.html">06 一共有哪 3 类线程安全问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/07 哪些场景需要额外注意线程安全问题?.md.html">07 哪些场景需要额外注意线程安全问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/08 为什么多线程会带来性能问题?.md.html">08 为什么多线程会带来性能问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/09 使用线程池比手动创建线程好在哪里?.md.html">09 使用线程池比手动创建线程好在哪里?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/10 线程池的各个参数的含义?.md.html">10 线程池的各个参数的含义?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/11 线程池有哪 4 种拒绝策略?.md.html">11 线程池有哪 4 种拒绝策略?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/12 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?.md.html">12 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/13 线程池常用的阻塞队列有哪些?.md.html">13 线程池常用的阻塞队列有哪些?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/14 为什么不应该自动创建线程池?.md.html">14 为什么不应该自动创建线程池?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/15 合适的线程数量是多少?CPU 核心数和线程数的关系?.md.html">15 合适的线程数量是多少?CPU 核心数和线程数的关系?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/16 如何根据实际需要,定制自己的线程池?.md.html">16 如何根据实际需要,定制自己的线程池?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/17 如何正确关闭线程池?shutdown 和 shutdownNow 的区别?.md.html">17 如何正确关闭线程池?shutdown 和 shutdownNow 的区别?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/18 线程池实现“线程复用”的原理?.md.html">18 线程池实现“线程复用”的原理?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/19 你知道哪几种锁?分别有什么特点?.md.html">19 你知道哪几种锁?分别有什么特点?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/20 悲观锁和乐观锁的本质是什么?.md.html">20 悲观锁和乐观锁的本质是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/21 如何看到 synchronized 背后的“monitor 锁”?.md.html">21 如何看到 synchronized 背后的“monitor 锁”?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/22 synchronized 和 Lock 孰优孰劣,如何选择?.md.html">22 synchronized 和 Lock 孰优孰劣,如何选择?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/23 Lock 有哪几个常用方法?分别有什么用?.md.html">23 Lock 有哪几个常用方法?分别有什么用?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/24 讲一讲公平锁和非公平锁,为什么要“非公平”?.md.html">24 讲一讲公平锁和非公平锁,为什么要“非公平”?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/25 读写锁 ReadWriteLock 获取锁有哪些规则?.md.html">25 读写锁 ReadWriteLock 获取锁有哪些规则?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/26 读锁应该插队吗?什么是读写锁的升降级?.md.html">26 读锁应该插队吗?什么是读写锁的升降级?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/27 什么是自旋锁?自旋的好处和后果是什么呢?.md.html">27 什么是自旋锁?自旋的好处和后果是什么呢?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/28 JVM 对锁进行了哪些优化?.md.html">28 JVM 对锁进行了哪些优化?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/29 HashMap 为什么是线程不安全的?.md.html">29 HashMap 为什么是线程不安全的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/30 ConcurrentHashMap 在 Java7 和 8 有何不同?.md.html">30 ConcurrentHashMap 在 Java7 和 8 有何不同?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/31 为什么 Map 桶中超过 8 个才转为红黑树?.md.html">31 为什么 Map 桶中超过 8 个才转为红黑树?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/32 同样是线程安全,ConcurrentHashMap 和 Hashtable 的区别.md.html">32 同样是线程安全,ConcurrentHashMap 和 Hashtable 的区别</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/33 CopyOnWriteArrayList 有什么特点?.md.html">33 CopyOnWriteArrayList 有什么特点?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/34 什么是阻塞队列?.md.html">34 什么是阻塞队列?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/35 阻塞队列包含哪些常用的方法?add、offer、put 等方法的区别?.md.html">35 阻塞队列包含哪些常用的方法?add、offer、put 等方法的区别?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/36 有哪几种常见的阻塞队列?.md.html">36 有哪几种常见的阻塞队列?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/37 阻塞和非阻塞队列的并发安全原理是什么?.md.html">37 阻塞和非阻塞队列的并发安全原理是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/38 如何选择适合自己的阻塞队列?.md.html">38 如何选择适合自己的阻塞队列?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/39 原子类是如何利用 CAS 保证线程安全的?.md.html">39 原子类是如何利用 CAS 保证线程安全的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/40 AtomicInteger 在高并发下性能不好,如何解决?为什么?.md.html">40 AtomicInteger 在高并发下性能不好,如何解决?为什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/41 原子类和 volatile 有什么异同?.md.html">41 原子类和 volatile 有什么异同?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/42 AtomicInteger 和 synchronized 的异同点?.md.html">42 AtomicInteger 和 synchronized 的异同点?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/43 Java 8 中 Adder 和 Accumulator 有什么区别?.md.html">43 Java 8 中 Adder 和 Accumulator 有什么区别?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/44 ThreadLocal 适合用在哪些实际生产的场景中?.md.html">44 ThreadLocal 适合用在哪些实际生产的场景中?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/45 ThreadLocal 是用来解决共享资源的多线程访问的问题吗?.md.html">45 ThreadLocal 是用来解决共享资源的多线程访问的问题吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/46 多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的?.md.html">46 多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/47 内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()?.md.html">47 内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/48 Callable 和 Runnable 的不同?.md.html">48 Callable 和 Runnable 的不同?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/49 Future 的主要功能是什么?.md.html">49 Future 的主要功能是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/50 使用 Future 有哪些注意点?Future 产生新的线程了吗?.md.html">50 使用 Future 有哪些注意点?Future 产生新的线程了吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/51 如何利用 CompletableFuture 实现“旅游平台”问题?.md.html">51 如何利用 CompletableFuture 实现“旅游平台”问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/52 信号量能被 FixedThreadPool 替代吗?.md.html">52 信号量能被 FixedThreadPool 替代吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/53 CountDownLatch 是如何安排线程执行顺序的?.md.html">53 CountDownLatch 是如何安排线程执行顺序的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/54 CyclicBarrier 和 CountdownLatch 有什么异同?.md.html">54 CyclicBarrier 和 CountdownLatch 有什么异同?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/55 Condition、object.wait() 和 notify() 的关系?.md.html">55 Condition、object.wait() 和 notify() 的关系?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/56 讲一讲什么是 Java 内存模型?.md.html">56 讲一讲什么是 Java 内存模型?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/57 什么是指令重排序?为什么要重排序?.md.html">57 什么是指令重排序?为什么要重排序?</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/Java 并发编程 78 讲-完/58 Java 中的原子操作有哪些注意事项?.md.html">58 Java 中的原子操作有哪些注意事项?</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/59 什么是“内存可见性”问题?.md.html">59 什么是“内存可见性”问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/60 主内存和工作内存的关系?.md.html">60 主内存和工作内存的关系?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/61 什么是 happens-before 规则?.md.html">61 什么是 happens-before 规则?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/62 volatile 的作用是什么?与 synchronized 有什么异同?.md.html">62 volatile 的作用是什么?与 synchronized 有什么异同?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/63 单例模式的双重检查锁模式为什么必须加 volatile?.md.html">63 单例模式的双重检查锁模式为什么必须加 volatile?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/64 你知道什么是 CAS 吗?.md.html">64 你知道什么是 CAS 吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/65 CAS 和乐观锁的关系,什么时候会用到 CAS?.md.html">65 CAS 和乐观锁的关系,什么时候会用到 CAS?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/66 CAS 有什么缺点?.md.html">66 CAS 有什么缺点?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/67 如何写一个必然死锁的例子?.md.html">67 如何写一个必然死锁的例子?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/68 发生死锁必须满足哪 4 个条件?.md.html">68 发生死锁必须满足哪 4 个条件?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/69 如何用命令行和代码定位死锁?.md.html">69 如何用命令行和代码定位死锁?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/70 有哪些解决死锁问题的策略?.md.html">70 有哪些解决死锁问题的策略?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/71 讲一讲经典的哲学家就餐问题.md.html">71 讲一讲经典的哲学家就餐问题</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/72 final 的三种用法是什么?.md.html">72 final 的三种用法是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/73 为什么加了 final 却依然无法拥有“不变性”?.md.html">73 为什么加了 final 却依然无法拥有“不变性”?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/74 为什么 String 被设计为是不可变的?.md.html">74 为什么 String 被设计为是不可变的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/75 为什么需要 AQS?AQS 的作用和重要性是什么?.md.html">75 为什么需要 AQS?AQS 的作用和重要性是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/76 AQS 的内部原理是什么样的?.md.html">76 AQS 的内部原理是什么样的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/77 AQS 在 CountDownLatch 等类中的应用原理是什么?.md.html">77 AQS 在 CountDownLatch 等类中的应用原理是什么?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 并发编程 78 讲-完/78 一份独家的 Java 并发工具图谱.md.html">78 一份独家的 Java 并发工具图谱</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>58 Java 中的原子操作有哪些注意事项?</h1>
|
||
<p>本课时我们主要讲解 Java 中的原子性和原子操作。</p>
|
||
<h3>什么是原子性和原子操作</h3>
|
||
<p>在编程中,具备原子性的操作被称为原子操作。原子操作是指一系列的操作,要么全部发生,要么全部不发生,不会出现执行一半就终止的情况。</p>
|
||
<p>比如转账行为就是一个原子操作,该过程包含扣除余额、银行系统生成转账记录、对方余额增加等一系列操作。虽然整个过程包含多个操作,但由于这一系列操作被合并成一个原子操作,所以它们要么全部执行成功,要么全部不执行,不会出现执行一半的情况。比如我的余额已经扣除,但是对方的余额却不增加,这种情况是不会出现的,所以说转账行为是具备原子性的。而具有原子性的原子操作,天然具备线程安全的特性。</p>
|
||
<p>下面我们举一个不具备原子性的例子,比如 i++ 这一行代码在 CPU 中执行时,可能会从一行代码变为以下的 3 个指令:</p>
|
||
<ul>
|
||
<li>第一个步骤是读取;</li>
|
||
<li>第二个步骤是增加;</li>
|
||
<li>第三个步骤是保存。</li>
|
||
</ul>
|
||
<p>这就说明 i++ 是不具备原子性的,同时也证明了 i++ 不是线程安全的,正如第 06 课时中所介绍的那样。下面我们简单的复习一下,如何发生的线程不安全问题,如下所示:</p>
|
||
<p><img src="assets/CgpOIF5xzWKAb71oAAA8JO4TxM0494.png" alt="img" /></p>
|
||
<p>我们根据箭头指向依次看,线程 1 首先拿到 i=1 的结果,然后进行 i+1 操作,但假设此时 i+1 的结果还没有来得及被保存下来,线程 1 就被切换走了,于是 CPU 开始执行线程 2,它所做的事情和线程 1 是一样的 i++ 操作,但此时我们想一下,它拿到的 i 是多少?实际上和线程 1 拿到的 i 结果一样,同样是 1,为什么呢?因为线程 1 虽然对 i 进行了 +1 操作,但结果没有保存,所以线程 2 看不到修改后的结果。</p>
|
||
<p>然后假设等线程 2 对 i 进行 +1 操作后,又切换到线程 1,让线程 1 完成未完成的操作,即将 i+1 的结果 2 保存下来,然后又切换到线程 2 完成 i=2 的保存操作,虽然两个线程都执行了对 i 进行 +1 的操作,但结果却最终保存了 i=2,而不是我们期望的 i=3,这样就发生了线程安全问题,导致数据结果错误,这也是最典型的线程安全问题。</p>
|
||
<h3><strong>Java 中的原子操作有哪些</strong></h3>
|
||
<p>在了解了原子操作的特性之后,让我们来看一下 Java 中有哪些操作是具备原子性的。Java 中的以下几种操作是具备原子性的,属于原子操作:</p>
|
||
<ul>
|
||
<li>除了 long 和 double 之外的基本类型(int、byte、boolean、short、char、float)的读/写操作,都天然的具备原子性;</li>
|
||
<li>所有引用 reference 的读/写操作;</li>
|
||
<li>加了 volatile 后,所有变量的读/写操作(包含 long 和 double)。这也就意味着 long 和 double 加了 volatile 关键字之后,对它们的读写操作同样具备原子性;</li>
|
||
<li>在 java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的,比如 AtomicInteger 的 incrementAndGet 方法。</li>
|
||
</ul>
|
||
<h3><strong>long 和 double 的原子性</strong></h3>
|
||
<p>在前面,我们讲述了 long 和 double 和其他的基本类型不太一样,好像不具备原子性,这是什么原因造成的呢? 官方文档对于上述问题的描述,如下所示:</p>
|
||
<p><strong>Non-Atomic Treatment of double and long</strong></p>
|
||
<p>For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.</p>
|
||
<p>Writes and reads of volatile long and double values are always atomic.</p>
|
||
<p>Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.</p>
|
||
<p>Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.</p>
|
||
<p>Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.</p>
|
||
<p>从刚才的 JVM 规范中我们可以知道,long 和 double 的值需要占用 64 位的内存空间,而对于 64 位值的写入,可以分为两个 32 位的操作来进行。</p>
|
||
<p>这样一来,本来是一个整体的赋值操作,就可能被拆分为低 32 位和高 32 位的两个操作。如果在这两个操作之间发生了其他线程对这个值的读操作,就可能会读到一个错误、不完整的值。</p>
|
||
<p>JVM 的开发者可以自由选择是否把 64 位的 long 和 double 的读写操作作为原子操作去实现,并且规范推荐 JVM 将其实现为原子操作。当然,JVM 的开发者也有权利不这么做,这同样是符合规范的。</p>
|
||
<p>规范同样规定,如果使用 volatile 修饰了 long 和 double,那么其读写操作就必须具备原子性了。同时,规范鼓励程序员使用 volatile 关键字对这个问题加以控制,由于规范规定了对于 volatile long 和 volatile double 而言,JVM 必须保证其读写操作的原子性,所以加了 volatile 之后,对于程序员而言,就可以确保程序正确。</p>
|
||
<h3><strong>实际开发中</strong></h3>
|
||
<p>此时,你可能会有疑问,比如,如果之前对于上述问题不是很了解,在开发过程中没有给 long 和 double 加 volatile,好像也没有出现过问题?而且,在以后的开发过程中,是不是必须给 long 和 double 加 volatile 才是安全的?</p>
|
||
<p>其实在实际开发中,读取到“半个变量”的情况非常罕见,这个情况在目前主流的 Java 虚拟机中不会出现。因为 JVM 规范虽然不强制虚拟机把 long 和 double 的变量写操作实现为原子操作,但它其实是“强烈建议”虚拟机去把该操作作为原子操作来实现的。</p>
|
||
<p>而在目前各种平台下的主流虚拟机的实现中,几乎都会把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要为了避免读到“半个变量”而把 long 和 double 声明为 volatile 的。</p>
|
||
<h3><strong>原子操作</strong> <strong>+</strong> <strong>原子操作</strong> <strong>!=</strong> <strong>原子操作</strong></h3>
|
||
<p>值得注意的是,简单地把原子操作组合在一起,并不能保证整体依然具备原子性。比如连续转账两次的操作行为,显然不能合并当做一个原子操作,虽然每一次转账操作都是具备原子性的,但是将两次转账合为一次的操作,这个组合就不具备原子性了,因为在两次转账之间可能会插入一些其他的操作,例如系统自动扣费等,导致第二次转账失败,而且第二次转账失败并不会影响第一次转账成功。</p>
|
||
<p>以上就是本课时的内容,我们介绍了什么是原子性,Java 中的原子操作有哪些,并且还对 long 和 double 这一具有特殊性的情况进行了详细说明,最后我们还介绍了简单地把原子操作组合在一起,并不能保证整体依然具备原子性。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/Java 并发编程 78 讲-完/57 什么是指令重排序?为什么要重排序?.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/Java 并发编程 78 讲-完/59 什么是“内存可见性”问题?.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709970eead5c3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|