# java 高并发实战读书笔记 #
java concurrency in practice
**声明**:最近硬着头皮去读这本java 高并发实战,在读此书前久闻此书是java并发编程里程碑式的一本书,同时对其中文翻译版的晦涩难懂也是如雷贯耳(听很多人说其2到5章翻译尤其难以理解,为此读到2到5章时笔者特地费力去看了英文版)。针对本书相信如果操作系统学的好的人,尤其是对信号量机制理解透彻的人应该能轻松一些。
在阅读本书时,能让人清晰的回忆起很多曾经学到过但是一直不明其用途的知识,尤其可以加深对许多java设计模式的理解,例如,装饰着模式、监视器模式。还有一些容易被人遗忘知识,例如什么是发布与溢出,如何防止对象溢出?什么是内置锁,对象锁和私有锁有什么区别?下面我们就开始详细记录我从书中梳理出的一些知识总结。
# 第一章讲解了并发史、线程的优势、线程带来的风险,此处略去总结 #
**特别说明:**关于这一章其实,《java concurrency in practice》中只是浅尝辄止的作了介绍,并没有另一本书中介绍的更加全面和详细,那就是汤小丹等人编辑的那本《计算机操作系统》(这本书相信大部分计算机系出身的学子都研读过,笔者当年大四考研时很详细的把这本书读了3遍,其实算读的少的,当时我邻桌的兄弟读了七遍,到如今我已经工作了,这本书所学到的知识一直让我很受用)。
# 2.线程安全性 #
> Perhaps surprisingly, concurrent programming isn’t so much about threads or locks, any more than civil engineering is about rivets and I-beams. Of course,building bridges that don’t fall down requires the correct use of a lot of rivets and I-beams, just as building concurrent programs require the correct use of threads and locks. But these are just mechanisms—means to an end. Writing thread-safe code is, at its core, about managing access to state, and in particular to shared,mutable state
在本书中有这样一段介绍,他把写线程安全的程序比作土木工程中给大桥上铆钉和衡梁。在读这本书之前,你可能会认为要构建稳定安全的并发程序就要学会合理的使用线程和锁。但这本书的作者却认为,这终归只是一些机制,究其本质其核心在于对状态访问操作的管理,尤其是,共享和可变状态。
**对象的状态:**存储在状态变量(实例域或静态域)中的数据。书中是这样描述的,这里的实力域和静态域即是指类的成员变量,成员变量可以分作静态变量和实例变量两种,关于对象的状态书中没有详细展开,这里做出补充:
有状态对象:有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
> 1. /**
> 1. * 有状态bean,有state,user等属性,并且user有存偖功能,是可变的。
> 1. */
> 1. public class StatefulBean {
> 1.
> 1. public int state;
> 1. // 由于多线程环境下,user是引用对象,是非线程安全的
> 1. public User user;
> 1.
> 1. public int getState() {
> 1. return state;
> 1. }
> 1.
> 1. public void setState( int state) {
> 1. this .state = state;
> 1. }
> 1.
> 1. public User getUser() {
> 1. return user;
> 1. }
> 1.
> 1. public void setUser(User user) {
> 1. this .user = user;
> 1. }
> 1. }
**无状态对象:**无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类,是线程安全的。
> 1. public class StatefulBean {
> 1.
> 1. // 虽然有userDao 属性,但Dao只有一些简单的增上改查方法,userDao 是没有属性的也就没有状态信息,是Stateless Bean。
> 1. public UserDao userDao ;
> 1.
> 1. public int getState() {
> 1. return state;
> 1. }
> 1.
> 1. public void setState( int state) {
> 1. this .state = state;
> 1. }
> 1.
> 1. public User getUser() {
> 1. return user;
> 1. }
> 1.
> 1. public void setUser(User user) {
> 1. this .user = user;
> 1. }
> 1. }
如果在设计代码时没有考虑到线程安全性,使用上面的方法可能要面临大改,也即是说一面三个方面虽然正确,但知易行难,我们更要探究的是,如果设计线程安全的类。
## 2.1线程安全性 ##
这章节除了及介绍线程安全性,更多的是在介绍servlet这种无状态对象的便利。通常线程安全性的需求并非来源于对线程的直接使用,而是使用servlet这样的框架。下面详细介绍
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
无状态对象(stateless )一定是线程安全的。就像上例,因为stateless 没有任何状态域,也不引用任何域。他仅含有一些方法,算然在方法service执行过程会产生一些临时的状态,但是这些临时状态(这里的临时状态就是指i,factors这些临时计算结果)会缓存在线程自带的栈中,当另一个线程同时访问service时,他们不会共享那些临时状态,而是会在执行过程中分别把临时状态缓存在自己的栈中,因此可以互不干扰的执行。
> **Stateless s are always thread-safe**
由于大多数servlet都是无状态的,所以使用servlet可以极大的减轻设计线程安全性所带来的负担。
## 2.2原子性 ##
如果我们想给servlet加一个属性怎么办,看看下面这个方法
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count
encodeIntoResponse(resp, factors);
}
}
这是一个老生常谈的例子,这个看似无懈可击的程序实际是非线程安全的,++count看起来是一个操作,实际上在执行时包含会先读取count的值,修改count的值,写入修改结果三步操作。由于在多线程时,cpu是分时复用的,当多个线程同时访问count时中断可能发生在以上三步操作的任何时刻(有可能是时间片用完,也有可能接收到),这程序的执行结果也就变得不确定了。
> 出现复合操作,必须要保证操作的原子性,例如以下两种复合操作:
> 1. 读取——修改——写入
> 2. 先坚持后执行
> 这里除了加锁意外,要尽可能地使用现有的线程安全类,去管理状态,例如使用,java.util.concurrent.atomic包里的AotmicLong来代替long类型的计数器,由于AtomicLong是线程安全的,所以这个类仍是线程安全的。
@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count.get();
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
## 2.3加锁机制 ##
当在servlet中添加一个属性,则使用线程安全的类去管理状态,但是如果加入多个状态是否可以使用多个线程安全对类去管理多个状态呢?
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
很遗憾这个看似无懈可击的方法是非线程安全的,这是一个因式分解的servlet,lastNumber表示最近执行因式分解的数值,lastFactors数组表示最近的分解结果。要保证上述类的线程安全性,就要时刻保证lastFactors数组中的数值乘积和lastNumber保持一致。所以即便lastNumber和lastFactors分开来都是线程安全的,但是UnsafeCachingFactorizer并不是线程安全类。由此我们得出以下结论:
> 要保证原子性,就必须在单个操作中保证所有操作的更新所有相关状态变量
## 2.4用锁来保护状态 ##
> 要注意的是,虽然大多数类都把内置锁作为一种有效的加锁机制,但是对象的内置锁和对象状态之间并没有内在关联,之所以每个对象都有一个内置锁,是为了避免显示的构造内置锁,你需要自行构造加锁协议和同步策略来。常见的加锁约定是对属性进行封装,并通过内置锁控制对状态的访问,使得在该对象上不会发生并放访问。
> 对于每个包含多个变量的不变性条件,其中涉及的变量都要由同一个锁保护
# 3.对象的共享 #
第二章讲了使用同步使多个线程访问同一变量。本章讲解如何共享和发布对象。
## 3.1 可见性 ##
> 同步除了可以使代码以原子性执行还可以同于确保可见性。(关于可见性问题笔者以前阅读《java并发编程之美》时有过很深刻的印象,所以这里只做简单介绍。推荐阅读这个帖子:https://gitbook.cn/books/5ab3a4b59802af4892b426fb/index.html)
### 3.1.1失效数据 ###
> 当读线程read值时,取的是未同步的数据。
### 3.1.2非原子性64位操作 ###
> JVM允许将64位读、写操作分为两个32位的操作。当读取非volatile的long变量时,如果读写在不同的线程,在不考虑失效数据时,多线程使用共享可变变量Long时不安全的。除非用volatile声明,或用锁保护。
### 3.1.4 volatile ###
> 确保变量的更新操作通知到其他线程。Volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量总返回最新写入的值。
## 3.2 发布与逸出 ##
> 下面就到了本书的重点基础知识发布与逸出。这里翻译写的晦涩难懂,几乎让人摸不清头脑。我也是颇费一番功夫才理解。
> **发布:**Publishing an means making it available to code outside of its current scope,
> such as by storing a reference to it where other code can find it, returning it
> from a nonprivate method, or passing it to a method in another class.
> **逸出:**An that is published when it should not have been is said to
> have escaped.
> 简而言之,发布就是使对象能在当前作用域以外使用(比如把对象的属性设置为static行,这样任何线程都可以通过【类名.属性名】使用这个属性,也就是说这个属性被发布了。再比如说,通过我们长写的get方法,返回对象的摸某个属性的引用)。而逸出就是在不应该发布时不小心发布了对象。比如下面这个例子:
>
@Getter
@Setter
public class Test{
//一个你不希望被发布的状态集合
private String[] states={\"A\",\"B\",\"C\"}
}
> @Getter和@Setter为lombok提供的标签,可以自动为类添加setter和getter方法。
> 上述例子,有一个你不希望被更改的状态,但一旦使用getter方法将其引用发布,调用他的对象就可以轻易地修改数组内容。这种不安全的发布就叫做逸出。
发布对象有以下几种常见的形式:
1. 将指向对象的引用保存到其他代码能够访问的地方
2. 在非私有的方法中返回该对象的引用
3. 将对象引用传递到其他类的方法中
4. 在别的已发布对象中的非私有域中引用对象
5. 发布一个匿名内部类,因为匿名内部类包含了当前对象的隐含引用,发布匿名内部类时也发布了自己
针对第5条可能不好理解,这里作出解释,如下示例是一个第5种情况引起的逸出:
//隐式地使this引用逸出(不要这么做)
public class ThisEscape {
public ThisEscape(EventSource source){
source.registerListener(new EventListener() {
public void onEvent(Event e){
//一个外部方法
doSomething(e);
}
});
}
}
> 内部类、匿名内部类都可以访问外部类的对象的域,为什么会这样,实际上是因为内部类构造的时候,会把外部类的对象this隐式的作为一个参数传递给内部类的构造方法,这个工作是编译器做的,他会给你内部类所有的构造方法添加这个参数。该例因为你在外部对象的构造方法内部发布内部类对象的而导致发布了this,但是对于外部对象来说当前对象还没有构造完成就提前被发布了,这样的发布容易导致错误。
> 不要在构造函数中使用this
上述例子之所以逸出,是因为其他线程可能会在构造函数未完成前使用了this,换句话说只要其他线程不会再构造没完成前使用this,就可以避免逸出,顺着这个思路,可以将上述构造方法私有化,再使用工厂方法返回实例。
## 3.3 线程封闭 ##
当访问共享变量时,通常要使用某种同步机制,换句话说如果有某种方法可以避免共享数据,就可以不使用同步。线程封闭就是其中一种方法,即:只在单线程中访问数据(一个常见的例子就是JDBC的Connection对象)
### 3.3.1 ad-hoc线程封闭
指:维护线程封闭性的职责完全由程序实现来承担。
脆弱,不建议使用。应该使用更强的线程封闭技术(如:栈封闭或ThreadLocal类)
### 3.3.2栈封闭
只能通过局部变量才能访问对象。
### 3.3.3 ThreadLocal类(规范方法)
使线程中的某个值与保存值的对象关联起来。ThreadLocal对象通常用于防止对可变的单实例变量或僵尸变量进行共享。
继续阅读与本文标签相同的文章
17个新手常见Python运行时错误
云体验无国界:阿里云聆听国际站前来报道
-
开发者必读 · 周报 | 002期
2026-05-19栏目: 教程
-
斩获2019中国金融科技创新大赛金奖,蚂蚁金服mPaaS助力打造超级App生态
2026-05-19栏目: 教程
-
有呀!互联网icp许可证除申请以外有转让的吗?飞起
2026-05-19栏目: 教程
-
Apache Zepplin使用Hive Interpreter查询
2026-05-19栏目: 教程
-
大宗货运如何实现“重去重回”?
2026-05-19栏目: 教程
