读书笔记之《Java 并发编程的艺术》
一、多线程语义
即使是单核处理器也支持多线程执行代码,cpu 通过给每个线程分配 cpu 时间片来执行任务,当前任务执行一个时间片后会切换到下一个任务,所以 cpu 通过不停的切换线程执行。
并发执行如果没有达到一定的数量级,速度反而会比串行执行要慢。这是因为线程有创建和上下文切换的开销。
-
如何减少线程创建和上下文切换的开销?(“vmstat 1” 的 cs 参数,查看线程切换的次数)
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,比如 cas 算法、比如将数据的id按照 hash 算法取模分段,不同的线程处理不同段的数据。
- 使用最少线程。避免创建不需要的线程,比如通过线程池等方式。
- 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
-
如何避免死锁?
- 避免一个线程同时获取多个锁。
- 尝试使用定时锁,使用 lock.trylock(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
在java se 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁(也称互斥锁)状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种策略的目的是为了提高获得锁和释放锁的效率。有意思的是除了偏向锁,jvm 实现锁的方式都用了循环 cas,即当一个线程想进入同步块的时候使用循环 cas 的方式来获取锁,当它退出同步块的时候使用循环 cas 释放锁。
如果只有一个线程进入同步代码块,那么它首先会获得“偏向锁”;当存在线程间竞争的时候,“偏向锁”会撤销,从而使用“轻量级锁”;当线程通过自旋方式始终获取不到“轻量级锁”时(获取锁的线程执行时间过长等原因),那么“轻量级锁”会膨胀成“重量级锁”。
二、java 内存模型
jmm(java 内存模型)采用共享内存模型,通过控制主内存(main memory)与每个线程的本地内存(local memory)之间的交互,来为 java 程序员提供内存可见性保证。
-
从 java 源代码到最终实际执行的指令序列,会分别经历“编译器优化的重排序”、“指令级并行的重排序”、“内存系统的重排序”,前一个属于编译器重排序,后两个属于处理器重排序,这些重排序会导致多线程程序出现内存可见性问题。为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。jmm 把内存屏障分为四类:
- 对于会改变程序执行结果的重排序,jmm 要求编译器和处理器必须禁止这种重排序。
- 对于不会改变程序执行结果的重排序,jmm 对编译器和处理器不做要求。
- 因此,在程序员看来,程序执行的语义(执行结果)不会改变,happens-before 关系本质上和 as-if-serial 语义是一回事。
-
happens-before 是 jmm 最核心的概念。happens-before 规则对应于一个或多个编译器和处理器重排序规则,jmm 通过 happens-before 规则隐藏了复杂的重排序规则以及这些规则的具体实现,而程序员在 happens-before 规则上编程,从而保证内存可见性。
- 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。这个规则由 as-if-serial 语义保证。
- 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 a happens-before b,且 b happens-before c,那么 a happens-before c。
顺序一致性内存模型是一个理论参考模型,jmm 和处理器内存模型在设计时通常会以顺序一致性内存模型为参照。
顺序一致性模型、jmm、处理器内存模型,内存模型设计由强变弱,因为越是追求性能,内存模型就会设计的越弱,以此减少内存模型对它们的束缚。
三、synchronized、volatile和 final 的内存语义
-
理解 volatile 特性的一个好方法是把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。简而言之,volatile 变量自身具有下列特性:
- 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
- 原子性。对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
-
volatile 关键字如何保证可见性?volatile 的内存语义?
- 当对 volatile 变量写的时候,会将当前处理器缓存行的数据写回到系统内存。
- 当对 volatile 变量读的时候,会将当前处理器缓存行的数据置为无效,因此要从系统内存中读取变量值。
-
volatile 关键字如何保证有序性?
- 在每个 volatile 写操作的前面插入一个 storestore 屏障(禁止上面的普通写和下面的 volatile 写重排序)。
- 在每个 volatile 写操作的后面插入一个 storeload 屏障(防止上面的 volatile 写与下面可能有的 volatile 读/写重排序)。
- 在每个 volatile 读操作的后面插入一个 loadload 屏障(禁止下面所有的普通读操作和上面的 volatile 读重排序)。
- 在每个 volatile 读操作的后面插入一个 loadstore 屏障(禁止下面所有的普通写操作和上面的 volatile 读重排序)。
-
为什么 jdk 文档说 cas 同时具有 volatile 读和 volatile 写的内存语义?
- 编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序。
- 编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
- cas(compare and swap)操作意味着要对 volatile 变量先读后写,要同时具备 volatile 读和写的语义,因此编译器不能对 cas 与 cas 前面和后面的任意内存操作重排序。
synchonized 关键字的原理?jvm 基于进入和退出 monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在 jvm 规范里并没有详细说明。
-
锁的释放和获取的内存语义?
- 当线程释放锁时,jmm 会把该线程对应的本地内存中的共享变量刷新到主内存中。(和 volatile 写的内存语义相同)
- 当线程获取锁时,jmm 会把该线程对应的本地内存置为无效,因此要从系统内存中读取变量值。(和 volatile 读的内存语义相同)
-
对于 final 变量,编译器和处理器要遵守两个重排序规则(对象引用不在构造函数中“溢出”的情况下):
- 在构造函数内对一个 final 变量的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含 final 变量的对象引用,与随后初次读这个 final 变量,这两个操作之间不能重排序。
-
final 变量的内存语义?
- 写 final 变量的重排序规则会要求编译器在 final 变量的写之后,构造函数 return 之前插入一个 storestore 屏障。
- 读 final 变量的重排序规则会要求编译器在读 final 变量的操作前面插入一个 loadload 屏障。
在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“溢出”。因为 jmm 无法保证在构造函数中“对变量的写”和“被构造对象的引用” 这两者之间是否会被重排序。
四、其他
-
jps 和 jstack 命令?
- jps:查看 jvm 进程信息
- jstack:查看某个jvm进程的堆栈信息
- 用 jstack 命令dump线程信息,看看 pid 为 18023 的进程里的线程都在做什么。
jstack 18023 > /home/wwwroot/dump18023
- 统计线程分别处于什么状态
grep java.lang.thread.state dump18023 | awk '{print $2$3$4$5}' | sort | uniq -c
可以使用odps、hadoop 或者自己搭建服务器集群来解决硬件资源限制的问题。
一个对象的引用占 4 个字节。
上一篇: jQuery 命名空间的使用
下一篇: 什么铁锅好,用它炒出来的菜健康又美味
推荐阅读
-
Java并发之条件阻塞Condition的应用代码示例
-
java并发笔记四之synchronized 锁的膨胀过程(锁的升级过程)深入剖析
-
Java并发 CompletableFuture异步编程的实现
-
Java日期时间API系列5-----Jdk7及以前的日期时间类TimeUnit在并发编程中的应用
-
【并发编程】Java对并发编程的支持历史
-
《C#并发编程经典实例》读书笔记-关于并发编程的几个误解
-
荐 Java——数据库编程JDBC之数据库连接池技术(C3P0与Druid,提供了Druid的工具类)
-
Java并发编程之常用的辅助类详解
-
Java并发编程中的若干核心技术,向高手进阶!
-
[Java 并发编程实战] 设计线程安全的类的三个方式(含代码)