## 一、多线程开发所要平衡的几个点混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书。有种要高登大堂的感觉,被各种网络上、朋友、同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感。2月28好开始看了之后,发现,其实完全没这个必要。除了翻译的烂之外(一大段中文下来,有时候你就会骂娘:这tm想说的是个shen me gui),所有的,多线程所必须掌握的知识点,深入点,全部涵盖其中,只能说,一书在手,万线程不愁那种!当然,你必须要全部读懂,并融汇贯通之后,才能有的效果。我推荐,看这本书的中文版本,不要和哪一段的冗长的字句在那过多的纠缠,尽量一段一段的读,然后获取这一段中最重要的那句话,否则你会陷入中文阅读理解的怪圈,而怀疑你的高中语文老师是不是体育老师客串的!!我举个例子:13页第八段,我整段读了三遍硬是没想明白前面那么多的文字,是干什么用的,就是最后一句话才是核心:告诉你,线程安全性,最正规的定义应该是什么!(情允许我,向上交的几个翻译此书的,所谓的“教授”致敬,在你们的引领下,使我们的意志与忍受力更上了一个台阶,人生更加完美!)
看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点
- 安全性
- 活跃性
- 无限循环问题
- 死锁问题
- 饥饿问题
- 活锁问题(这个还没具体的了解到)
- 性能要求
- 吞吐量的问题
- 可伸缩性的问题
要想平衡好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:
- 原子性
- 先检查后执行
- 原子类
- 加锁机制
- 可见性
- 重排
- 非64位写入问题
- 对象的发布
- 对象的封闭
- 不变性
在一本国人自己写的,介绍线程工具api的书中,看到了这么一句话:外练原子,内练可见。感觉这几点如果在多线程中尤为重要。我在有赞,去年还记得上线多门店的那天凌晨,最后项目启动报一个类加载的错误,一堆人过来看问题,基德大神站在攀哥的后面,最后淡淡的说了句:已经很明显是可见性问题了,加上volatile,不行的话,我把代码吃了!!可以见得,多线程这几个点,在“居家旅行”,生活工作中是多么的常见与重要!不出问题不要紧,只要一出,就会是头痛的大问题,因为你根本不好排查根本原因在这。所以我们需要平时就练好功底,尽量避免多线程问题的出现!而不是一味的用框架啊用框架、摞代码啊摞代码!
## 三、原子性下面的安全问题 **1. 下面代码有什么问题呢?**public class UnsafeConuntingFactorizer implements Servlet{
private long count = 0;
private long getCount(){
return count;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp,factor);
}
}
**2. 上面代码分析**思考:如何让一个普普通通的类变得线程安全呢?一个类什么叫做有状态,而什么又叫做无状态呢?
- 一个请求的方法,实例都是一个,所以每次请求都会访问同一个对象
- 每个请求,使用一个线程,这就是典型的多线程模型
- count是一个对象状态属性,被多个线程共享
++count
并非一次原子操作(分成:复制count->对复制体修改->使用复制体回写count,三个步奏)- 多个线程有可能多次修改count值,而结果却相同
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
private long getCount(){
return count.get();
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();//使用了新的原子类的原子方法
encodeIntoResponse(resp,factor);
}
}
**4. 原子类也不是万能的**
//在复杂的场景下,使用多个原子类的对象
public class UnsafeConuntingFactorizer 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())){//先判断再处理,并没有进行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
**5. 先列举一个我们常见的复合型操作**思考:什么叫做复合型操作?
public class LazyInitRace {
private ExpensiveObject instace = null;
public ExpensiveObject getInstace(){
if(instace == null){
instace = new ExpensiveObject();
}
return instace;
}
}
**6. 提高“先判断再处理”的警觉性**看好了,这就是我们深恶痛绝的一段代码!如果这段代码还分析不了的,对不起,出门左转~
- 如果没有同步措施,直接对一个状态进行判断,然后设值的,都是不安全的
- if操作和下面代码快中的代码,远远不是原子的
- 如果if判断完之后,接下来线程挂起,其他线程进入判断流程,又是同样的状态,同样进入if语句块
- 当然,只有一个线程执行的程序,请忽略(那还叫能用的程序吗?)
//在复杂的场景下,使用多个原子类的对象
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
//这下子总算同步了!
public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//先判断再处理,并没有进行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
**8. 上诉代码解析** - 加上了``synchronized``关键字的确解决了多线程访问,类安全性问题 - 可是每次都是一个线程进行计算,所有请求变成了串行 - 请求量低于100/s其实都还能接受,可是再高的话,这就完全有问题的代码了 - 性能问题,再网络里面,是永痕的心病~ **9. 一段针对原子性、性能问题的解决方案**思考:有没有种“关公挥大刀,一砍一大片”的感觉?
//在复杂的场景下,使用多个原子类的对象
public class CacheFactorizer implements Servlet{
private BigInteger lastNumber;
private BigInteger[] lastFactors ;
private long hits;
private long cacheHits;
public synchronized long getHits(){
return hits;
}
public synchronized double getCacheHitRadio(){
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null){
factors = factor(i);
synchronized (this){
lastNumer = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
## 四、可见性下面的对象共享在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。
可见性这个话题,在多线程的环境下,是相当棘手的。可能多年之后,你成为万人心中的老鸟,也同样会对这个问题,怅然若失!我自己总结了几点,可见性问题的难处:
- 道理简单,真实场景代码错中复杂,你根本不知道是可见性导致的
- 小概率事件,往往可能只有百分之一,甚至千分之一的出事概率,容易被我们“得过且过”
- 容易直接扔到一个
synchronized
加锁块里面,进行“大刀”式的处理,而忽略了高效性 - 可见性+原子性的综合考虑
针对这些问题,我们只能先从基本功抓起,然后在日积月累的开发工作中,多多分析程序运行的场景,多多尝试,才能大有裨益。
**1. 可见性的发生的必要条件**插曲:昨天看了《恋爱回旋》这部日本电影,其中有个场景让我记忆深刻:女主是小时候被魔鬼母亲常年训练的乒乓球少年运动员,后来总总原因,放弃了乒乓球,当起了OL,这一别就是15年。当再次碰到男主的时候,男主向女主发起乒乓球挑战,以为女主是个菜逼,然后赌一些必须要让女主完成的事情。(女主本人也是觉得乒乓球对自己是一种心理的负担,并且放弃这么久了,所以没啥子自信)没想到,女主一拿球拍,在接发球的那一刹那。。。。。大家应该都懂了。我当时就在影院中说出声来:基本功太重要了。
可见性,无非就是再多线程环境下,对共享变量的读写导致的。可能一个线程修改了共享变量的值,而另一个线程读取的还是老的值,差不多就是这么大白话的解释了下来。其中发生的必要条件有:
- 多线程环境访问同一个共享变量
- 服务器模式下启动程序
- 共享变量并没有做什么处理,代码块也没有同步
当然,要分析为什么会有可见性的问题,要结合JVM虚拟机内存模型分析。以后会在《深入理解Java虚拟机》的学习中,做详细的分析,敬请期待。
**2. 不多说上代码**public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
**3. 上诉代码分析**思考:上面的打印number的值,有可能会有几种结果呢?什么情况下出现的这些个结果
- number最终打印结果有可能出现42、0,或者根本就不会打印
- 42:这种情况是运行正确的结果
- 0:这种情况发生了指令重排(五星级的问题)
- 不会打印:主线程对ReaderThread线程出现了共享变量不可见
之所以说是“愚钝”,原因是重排问题,是一个很底层很考验计算机基础能力的一个问题,小弟不才,当年分析计算机组成原理与指令结构的时候,枯燥极致,都睡过去了。现在回头,才知道其重要性。现阶段,对重排的分析,我只能举例个简单的例子,进行说明,更进一步的分析,同样是要结合JVM的机制(六大Happens-before)来分析,以后再做进一步,详尽的分析。下面就是那个简单的例子:
//简单例子
public class OrderConfuse{
public static void main(String[] args){
int a = 1;
int b = 2;
int c = a+b;
int d = c+a;
System.out.println(c);
System.out.println(d);
}
}
- 上面程序是正确的,也能正确输出
- 对a和b的赋值操作,并非先赋值a再赋值b的
- 原因是JVM底层会对指令进行优化,保证程序的快速执行,其实就是一种效率优化
- 变量c会用到a和b变量,所以a和b的操作必须要发生在c之前(happens-before)
- 有可能b进行了赋值,而a还是初始化的状态,就是值为0
所以结合前面的代码段:
public class NoVisibility{
......
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
- number和ready之后,并没有使用它们的变量了
- number和ready会被进行指令重排
- 结果就是:ready已经赋值变成了true,可是number还是0
这就是为啥会为零的原因所在!
**5. 针对可见性,还是要上JVM的内存模型进行简单分析**- 每个线程都会有自己的线程虚拟机栈
- 栈上面存储原始类型和对象类型的引用
- 每次启动一个线程,都会在对共享数据进行一次复制,复制到每个线程的虚拟机栈中
- 上面的number是在主线程中,同时在ReaderThread线程的虚拟机栈中有一个副本
- 各个虚拟机栈最终要进行同步,才能保持一致
- 所以每次修改一个共享变量(原始类型)其实是在本地线程空间里面修改
- number在主线程里面修改了,可是在ReaderThread线程里面并没有修改,因为两个线程访问的空间并不一样,一个线程对另一个线程空间并不可见。
volatile关键字的作用,主要有一下几点:
- 能把对变量的修改,马上同步到主存中
- 各个线程立马更新自己线程栈中的变量值
- 防止指令重排
- 无法保证原子性
对于最底层如何做到这些个点的,具体还可以分析,例如什么内存屏障、状态过期等等,完全可以聊一个专题,今天再次先不聊,同样放到《深入理解JVM虚拟机》的学习中来详尽分析。所以,上面程序可以改成下面这个样子:
public class Visibility{
private static volatile boolean ready;//注意这个类型
private static volatile int number;//注意这个类型
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
**7. synchronized关键字同样可以保证可见性**
public class Visibility{
private static boolean ready;
private static int number;
private static Object lock = new Object();
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
synchronized(lock){//这里进行了加锁
number = 42;
ready = true;
}
}
}
- 加锁可以同时保证可见性与原子性
- 加锁同样可以防止指令重排,内部代码都会照顺序执行
public class VisibleNotAtomic{
private static volatile int number = 1;
private static class ReadThread extends Thread{
public void run(){
if(number == 2){
System.out.println("correct!");
}else{
System.out.println("error!");
}
}
}
public static void main(String[] args){
number++;
}
}
- number是对主线程和ReadThread线程都可见的
- 可是number++不是原子操作
- 加加到了一半,主线程挂起,ReadThread线程运行,number的值还是1,输出error
我们主要讲了线程的原子性和可见性,结合代码,不知不觉就讲了一堆,而且感觉还可以在讲~~多线程的话题真的是太恐怖了!未来的可预见性的规划如下:
- 对象的安全发布
- 对象的不变性
- 对象的合理加锁
- 生产者消费者模型
- 构建高效可伸缩的缓存
恩,敬请期待~