又是三星期的生活。感觉自从有了这个分享之后,会无形多了一份动力,逼着自己不能落后,必须要去不停的学习,这其实是我想要的,各位少年团中的成员也都是有共鸣的,在此很感动,省去一万字。。。。。这一次会总结对象的安全发布、不变性,这几点,在我们工程实践中,同样也是非常具有参考与思考价值的基础知识点。看书枯燥,理解生涩,可是当你看过,理解一点,再平时业务代码中就会比别人多思考一分,就会比别人在更“恶劣”的网络环境中,更稳定一分。这几天想起《三傻》中,那句很经典的话:追求卓越,成功将会悄悄的靠近你。

## 一、发布与溢出
“发布(Publish)”一个对象的意思是指,使对象能够在当前作用于之外的代码中使用。这个“之外”,尤为关键,各种出问题的地方,都是因为这个“之外”所引起的。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为“溢出”。下面使用简单的例子进行说明:

### 1. 日常非常不注意的行为

class Status {
private String[] states = new String[]{"AA","BB","CC"}; public String[] getStates(){
return states;
}
}

思考:很多人会不服的来争吵:这特么哪里有问题,跑了这么久的线上了,一直没出问题啊!好,那么问题了来:是不是线上一直没问题的代码,就是好代码?就是正确的代码?

类似的代码还有:

class Cache {
private static HashMap<String,Object> cache = new HashMap<>(); public static Object getCacheValue(String key){
return return cache.get(key);
} public static HashMap<String,Object> getCache(String key){
return cache;
} public static void addCache(String key, Object object){
cache.push(key, object);
}
}

P.S.:以上代码是我去年年底,再项目工程中看到的代码,而且在线上运行着,千真万确!

### 2. 分析问题所在

你问我:这错在哪?如果我要回答,我会说:没错,你都没错。个人原因,我不喜欢程序员当面怼,因为我知道,大家都不容易,并且还知道:真的有问题那天,你知道痛了,你会主动改的,根本不用我说啥。当然,更严重的是,代码中(恩,线上代码),有人将states命名成了s,cache命名成了c,这我也说不了啥,什么叫做“追求卓越”,可能每人心中都会有自己的诠释吧。如果是下面代码出现在一个神不知鬼不觉的地方,请看:

class Controller {
public void cache(){//1
Status status = new Status();
String[] allStatus = status.getStates();
Cache.addCache("ALL_STATUS",allStatus);
} public void modify(){//2
String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
allStatus[0] = null;// 也许变成了其他值,null是一种比较极端的情况
} public String getFirstUpcaseStatus(){//3
String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
return allStatus[0].toUpperCase(); // oh no! NPE!
} public void remove(){//4
Cache.getCache().remove("ALL_STATUS");
}
}
  • 1、2、3、4四个方法我们并不知道是什么时候触发的
  • 就是说时间顺序上,有可能是4号方法首先被触发,那1、2、3都将有问题
  • 即使4不被触发,先1、2,后3,也是出问题的
  • 也许我们代码写的很复杂,例如在2号程序中调用了非常多的service,用了非常多的设计模式,最终我们将修改数组中的值
  • 也许我们知道问题所在不去修改数组中的状态值,可是你能保证你能维护这个代码一辈子吗?
  • 以后交给两个人维护,两个人由于没啥子追求,别人代码不看,一个人在一边修改了数组,而另一个人在另一边使用了数组中的状态值,后果不堪设想

### 3. 更加隐蔽式的危险发布

下面这种,新学到的一种危险性行为发布:

public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(e->doSomething(e));
}
}

心得:请大家尽量使用Java8语法,整洁、大方、可撸(这是什么鬼!)

思考:注意doSomething方法,会有什么问题呢?

### 4. 构造器与构造者

  • 作为构造者不要在构造器里面添加过多的逻辑,出错之后,这个锅你背不起!
  • 即使在一个构造器的最后一行,这个对象也是没有没初始化完成的!
  • this指针被发布出去,后果不堪设想,对象没初始化完成,而使用this指针。
  • 上面代码,可以在doSomething方法内部使用ThisEscape.this来访问父类
  • 如果父类没有初始化完,而访问父类,那将报错,这就是问题所在

### 5.针对这种隐蔽式的情况,我们怎么做

public class SafeListener {
private final EventListener listener; private SafeListener() {
listener = e->doSomething(e);
} public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return source;
}
}

### 6. 再举例一些不安全发布的例子


class Holder{
// 曝露类属性,大忌~
public Holder holder; public void initialize(){
holder = new Holder();
}
}
//由于未被正确发布,因此这个类可能出现故障
public class Holder{
private int n; public Holder(int n){
this.n = n;
} public void assertSanity(){
if(n != n){
throw new Exception("initial erroe");
}
}
}

说明:抛出异常这个类是很玄乎的,因为线程可见性的原因,线程初次读取n的时候是老的值,可是这之后n值被其他线程更新,这个线程再次读取的时候,读取到一个失效的值,这就是抛出异常的原因。可以见得普普通通的自身与自身的比较,在多线程的环境下,都是很有问题的!!

### 6-线程封闭

  • 常见的封闭模式:栈封闭。就是在局部方法中使用一个变量,而不把他暴露出去。另外我自己的理解,每次方法返回一个新对象,也是一种使用方式。
public int loadThe Ark(Collection<Animal> candidates){
// 将animals封闭在方法内部
SortedSet<Animal> animals;
int numberParies = 0;
Animal candidate = null; //针对animals容器进行各种统计 return numberParise; }
class Status {
public String[] getStates(){
//每次都返回新的对象数组
return new String[]{"AA","BB","CC"};
}
}
  • 另一种封闭模式:ThreadLocal模式。这种模式也比较常用,每次在web项目中保存session的时候,常常使用这种模式,来标记当前访问线程的登陆情况。不过这个要注意的是,再web中使用TreadLocal容易导致溢出,具体的分析,请期待到springMVC系列。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}

### 7-给出写安全发布的模式

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

## 二、不变性

满足线程安全的另外一种方式,就是使用不可变对象。如果想要创建不可变对象的话,要满足以下条件:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型
  • 对象是正确创建的(在对象的创建期间,this引用没有溢出)

### 1. 基础的不可能变模型

这种方式,有点像我在《CC》观后感那篇文章中,讲到的一个观点:尽量对原始工具包中的类进行封装,有节制的使用其中的功能。下面代码就展示了,再可变对象的基础上构建不可变类


public final class ThreeStooges{
//注意,这个stooges变量是可变的!
private final Set<String> stooges = new HashSet<>(); public ThreeStooges(){
stooges.add("1");
stooges.add("2");
stooges.add("3");
} public boolean isStooges(String name){
return stooges.contains(name);
}
}

### 2. 有点高端的货:使用不可变对象与volatile保证线程同步

这里使用了三个内在的基本功点:对象不可变、对象读写分离、对象可见性。上代码:

class Value{
private final BigInteger lastNumber;
private final BigInteger[] lastFactors; public Value(BigInteger lastNumber, BigInteger[] factors){
this.lastNumber = lastNumber;
this.lastFactors = Arrays.copyOf(factors,factors.length);//这里进行写复制
}
public BigInteger[] getFactors(BigInteger i){
if(lastNumber = null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors,lastFactors.length);//这里进行读复制
}
}

说明:由于每次初始化时候都进行类属性的初始化,并与外界分离,因为factors数组每次都是复制一个副本进行初始化的!并且每次读的时候,也是讲数组对象进行复制分离。这样,只要一初始化对象之后,实际上,类对象里面的两个类属性都是不可变的了,因为全部与外界隔离了

下面我们看看怎么使用:

public class VolatileCacheFactorizer implements Servlet{
private volatile Value cache = new Value(null,new BigInteger[0]); public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if(factors == null){
factors = factor(i);
cache = new Value(i,factors);
}
encodeIntoResponse(resp,factors);
}
}

说明:这里cache类属性使用volatile,保证多线程写入的时候,都能够同步到主内存中,在这种情况下,多线程即是可见的,而通过Value对象的不变性又保证了对cache对象访问的安全性,那这样,整个service就是线程安全的了!

## 三、设计线程安全的类

这一部分,我看书中涉及到很多名词,需要上网搜搜资料看看解释,否则读这一部分会很懵逼。我下面从一些名词解释入手来说说这一章。

### 1. 什么叫做监视器模式

乍看之下还以为这是一种设计模式,的确是一种设计模式!不过还想不起来是什么样子的。我一google才发现是非常简单的,其实就是一段互斥访问的代码段(管程):

class SynClass{
private long value = 0; public synchronized long getValue(){
return value;
} public synchronized long increment(){
if(value == Long.MAX_VALUE)
throw new IllegalStateException("counter overflow");
return ++value;
}
}

说明:加了synchronized关键字的代码段,就相当一个屋子,每次只允许一个线程访问,如果访问有需求了,还可能进行挂起工作,那监视器是谁能?监视器就是对象本身,synchronized是加锁操作,这个锁也是这个对象持有的一个内部锁,如果要挂起代码,可是使用对象本身就天然继承自Object的wait方法,这就是监视器的作用。我看网上解释说:监视器(其实就是每个对象自己,因为每个对象都继承了Object)就像一个屋子的管理者,然后把对象这个“屋子”分成了三个地方:互斥访问区域、准备访问的区域和等待区域。

### 2. 什么叫做先验条件和后验条件

  • 先验条件(precondition):针对方法(method),它规定了在调用该方法之前必须为真的条件。
  • 后验条件(postcondition):也是针对方法,它规定了方法顺利执行完毕之后必须为真的条件。

### 3. 设计线程安全的类的三要素

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问管理策略

### 4. 什么叫做不变性条件

这个也是要做一定解释:程序在一系列的操作之后,还能够满足自己的先验条件和后验条件的,就叫做不变性条件(这个理解有点困难,大致我自己的想法是这样)

### 5. 收集同步的需求

class SafeClass{
private long value = 0; public synchronized long increment(){
if(value == Long.MAX_VALUE)
throw new IllegalStateException("counter overflow");
return ++value;
}
}
  • 我们要做的,确定本类中的那些状态,会再多线程的操作下影响对象的不变性
  • 如果一个状态转变是依赖于前一个状态的话,那就会复合操作,需要同步机制
  • 当然,有些状态转变不依赖之前,例如温度
  • 上例中increment加上了synchronized就是一种保护程序不变性与后验条件的机制

### 6. 注意状态的所有权

举个简单的例子

class Owner{
private SunChild sub;
}

其实这个sub对象就是Owner所拥有的一个子对象,所有权归Owner。但是如果加上如下代码

class Owner{
private SunChild sub; SubChild getSub(){
return sub;
} }

这种情况下,所有权就被发布了出去,这样的情况就要考虑同步机制进行保护。

注意:交出所有权的时候一定要多加思考程序的运行情景,以防不备!

### 7. 实例封闭

如果某个对象不是线程安全的,我们可以将其进行封装,或者通过单一锁进行保护。下面是使用实例封闭模式进行的一种样例:

public class PersonSet{
private final Set<Person> mySet = new HashSet<>();//mySet本身并非线程安全 public synchronized void addPerson(Person p){
mySet.add(p)
}
public synchronized boolean contains(Person p){
return mySet.contain(p);
}
}

说明:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程再访问数据时总能持有正确的锁

### 8. 线程安全的又一方式:委托

委托,其实就是将对象的涉及到的影响可变性条件的状态,放到JDK提供的一些线程安全的容器中去,进行统一管理。同样也是一个简单的例子:


public class Tracher{
private final ConcurrentMap<String,Object> localMap; public Tracher(){
localMap = new ConcurrentHashMap<>();
} public Map<String,Object> getLocations(){
return localMap;
}
public Object getLocation(String key){
return lcoalMap.get(key);
}
}

上面讲统一使用ConcurrentMap进行管理。如果想要获取一个不变的状态的话,可以进行读复制:

public Map<String,Object> getLocations(){
return new ConcurrentHashMap<>(localMap);
}

### 9. 委托不是万能的

过分依赖原子类所造成的“残局”:

public class NumberRange {
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i){
if(i>upper.get()){//注意这里
throw new Exception("error");
}
lower.set(i)
} public AtomicInteger getUpper(){
return upper;
}
}

说明:由于upper被暴露了出去,可是setLower方法内部进行了“先检查后执行”的步骤,依赖于upper值,这样,lower属性的值就出现了不可预估性,原子操作没达成,原子类失效了。可以使用加锁来修改上述代码

### 10. 特别需要注意的由委托引起的非线程安全

这种模式属于一种叫做“客户端加锁”,其实就是写程序中很不注意的,将内置锁和属性对象的锁混淆所致,下面是问题代码:

public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E e){
//注意这里synchronized使用的是内置锁
boolean absent = !list.contains(e);
if(absent){
//这里add使用的是list对象里面的同步锁
list.add(e);
}
return absent;
}
}

两种锁并不一样,导致并没有对“先判断再执行”进行同步操作,还是会存在不安全性问题。下面是解决的方式:

public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public boolean putIfAbsent(E e){
synchronized (list){//统一使用属性对象的锁
boolean absent = !list.contains(e);
if(absent){
list.add(e);
}
return absent;
}
}
}

## 四、总结

本次主要讲了三个方面:

  • 对象的发布
  • 不变性
  • 设计线程安全的类

相对来说比较枯燥,尽量都是用简洁明了的例子来混合讲解了,望给位看官多多包涵~哈哈哈。接下来要分享的东西,就会实用很多,涉及到JDK线程工具的良好实用(如闭锁、FutureTask等),并且我在接下来的线程分享文章中,会每次安排一个大章节,逐步进行生活必备品之一的java.util.concurrent.ThreadPoolExecutor源码分析,敬请期待!

Java并发编程实战(chapter_2)(对象发布、不变性、设计线程安全类)的更多相关文章

  1. 那些年读过的书《Java并发编程实战》二、如何设计线程安全类

    1.设计线程安全类的过程 设计线程安全类的过程就是设计对象状态并发访问下线程间的协同机制(在不破坏对象状态变量的不变性条件的前提下). (1)构建线程安全类的三个基本要素: 1)找出构成对象状态的所有 ...

  2. Java并发编程实战 之 对象的共享

    上一篇介绍了如何通过同步多个线程避免同一时刻访问相同数据,本篇介绍如何共享和发布对象,使它们被安全地由多个进程访问. 1.可见性 通常,我们无法保证执行读操作的线程能看到其他线程写入的值,因为每个线程 ...

  3. java并发编程实战笔记---(第二章)线程安全:正确性

    ThreadA__________     同步 ______________ 异步 ___________     异步 ThreadB__________         ____________ ...

  4. Java并发编程(十三)在现有的线程安全类中添加功能

    重用现有的类而不是创建新的类,可以降低工作量,开发风险以及维护成本. 有时候线程安全类可以支持我们所有的操作,但更多时候,现有的了类只能支持大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新 ...

  5. 【java并发编程实战】第六章:线程池

    1.线程池 众所周知创建大量线程时代价是非常大的: - 线程的生命周期开销非常大:创建需要时间,导致延迟处理请求,jvm需要分配空间. - 资源消耗:线程需要占用空间,如果线程数大于可用的处理器数量, ...

  6. 《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象

    Java Concurrency in Practice,一本完美的Java并发参考手册. 查看豆瓣读书 推荐:InfoQ迷你书<Java并发编程的艺术> 第一章 介绍 线程的优势:充分利 ...

  7. 《Java并发编程实战》/童云兰译【PDF】下载

    <Java并发编程实战>/童云兰译[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062521 内容简介 本书深入浅出地介绍了Jav ...

  8. 《java并发编程实战》笔记

    <java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为:  Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...

  9. Java并发编程实战——读后感

    未完待续. 阅读帮助 本文运用<如何阅读一本书>的学习方法进行学习. P15 表示对于书的第15页. Java并发编程实战简称为并发书或者该书之类的. 熟能生巧,不断地去理解,就像欣赏一部 ...

随机推荐

  1. 芝麻HTTP:Appium的安装

    Appium是移动端的自动化测试工具,类似于前面所说的Selenium,利用它可以驱动Android.iOS等设备完成自动化测试,比如模拟点击.滑动.输入等操作,其官方网站为:http://appiu ...

  2. OpenStack_I版 2.keystone部署

    生成keystone默认证书,指定用户 修改keystone主配置文件 第625行,修改数据库连接方式   修改完成同步数据库 同步完成可以查看数据库是否有表生成 为了以后调试keystone方便,现 ...

  3. Nethogs - 网络流量监控工具

    命令iftop来检查带宽使用情况.netstat用来查看接口统计报告.还有其他的一些工具Bandwidthd.Speedometer.Nethogs.Darkstat.jnettop.ifstat.i ...

  4. Crash CodeForces - 417B

    During the "Russian Code Cup" programming competition, the testing system stores all sent ...

  5. Redis入门必读,The Little Redis Book中文版

    csdn的博客都要搬到这里了 The Little Redis Book中文版 入门 The Little Redis Book中文版 第一章 - 基础知识 The Little Redis Book ...

  6. python基础—装饰器

    python基础-装饰器 定义:一个函数,可以接受一个函数作为参数,对该函数进行一些包装,不改变函数的本身. def foo(): return 123 a=foo(); b=foo; print(a ...

  7. 结合实例分析Android MVP的实现

    最近阅读项目的源码,发现项目中有MVP的痕迹,但是自己却不能很好地理解相关的代码实现逻辑.主要原因是自己对于MVP的理解过于概念话,还没有真正操作过.本文打算分析一个MVP的简单实例,帮助自己更好的理 ...

  8. [USACO12FEB]Nearby Cows

    题意 给出一棵n个点的无根树,每个点有权值,问每个点向外不重复经过k条边的点权和 题解 设f[i][j]表示所有离i节点距离为j的点权和,v为它周围相邻的点,t为v的个数,则 j > 2 f[i ...

  9. [SCOI2007]蜥蜴

    网络流 一个点拆成两个,注意要把某一类边连反过来 这样才能保证有限制 # include <bits/stdc++.h> # define IL inline # define RG re ...

  10. 极重要基础命令三剑客加find

    find  -type:以文件类型查找 -name:以文件名查找 ! 取反 sed命令实战: sed -n “2p” oldboy.txt 打印第二行 sed -n "1,2p" ...