Java并发编程实战(chapter_1)原子性
混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书。有种要高登大堂的感觉,被各种网络上、朋友、同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感。2月28好开始看了之后,发现,其实完全没这个必要。除了翻译的烂之外(一大段中文下来,有时候你就会骂娘:这tm想说的是个shen me gui),所有的,多线程所必须掌握的知识点,深入点,全部涵盖其中,只能说,一书在手,万线程不愁那种!当然,你必须要全部读懂,并融汇贯通之后,才能有的效果。我推荐,看这本书的中文版本,不要和哪一段的冗长的字句在那过多的纠缠,尽量一段一段的读,然后获取这一段中最重要的那句话,否则你会陷入中文阅读理解的怪圈,而怀疑你的高中语文老师是不是体育老师客串的!!我举个例子:13页第八段,我整段读了三遍硬是没想明白前面那么多的文字,是干什么用的,就是最后一句话才是核心:告诉你,线程安全性,最正规的定义应该是什么!(情允许我,向上交的几个翻译此书的,所谓的“教授”致敬,在你们的引领下,使我们的意志与忍受力更上了一个台阶,人生更加完美!)
一、多线程开发所要平衡的几个点
看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点
- 安全性
- 活跃性
- 无限循环问题
- 死锁问题
- 饥饿问题
- 活锁问题(这个还没具体的了解到)
- 性能要求
- 吞吐量的问题
- 可伸缩性的问题
二、多线程开发所要关注的开发点
要想平很好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:
- 原子性
- 先检查后执行
- 原子类
- 加锁机制
- 可见性
- 重排
- 非64位写入问题
- 对象的发布
- 对象的封闭
- 不变性
在一本国人自己写的,介绍线程工具api的书中,看到了这么一句话:外练原子,内练可见。感觉这几点如果在多线程中尤为重要。我在有赞,去年还记得上线多门店的那天凌晨,最后项目启动报一个类加载的错误,一堆人过来看问题,基德大神站在攀哥的后面,最后淡淡的说了句:已经很明显是可见性问题了,加上volatile,不行的话,我把代码吃了!!可以见得,多线程这几个点,在“居家旅行”,生活工作中是多么的常见与重要!不出问题不要紧,只要一出,就会是头痛的大问题,因为你根本不好排查根本原因在这。所以我们需要平时就练好功底,尽量避免多线程问题的出现!而不是一味的用框架啊用框架、摞代码啊摞代码!
三、原子性下面的安全问题
1. 下面代码有什么问题呢?
public class UnsafeConuntingFactorizer implements Servlet{ private long count = ; 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值,而结果却相同
3. 使用原子类解决上面代码问题
public class UnsafeConuntingFactorizer implements Servlet{ private final AtomicLong count = new AtomicLong(); 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语句块
- 当然,只有一个线程执行的程序,请忽略(那还叫能用的程序吗?)
7. 性能的问题来了
//在复杂的场景下,使用多个原子类的对象 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); } }
在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。