首页 技术 正文
技术 2022年11月15日
0 收藏 906 点赞 4,770 浏览 8162 个字

混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书。有种要高登大堂的感觉,被各种网络上、朋友、同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感。2月28好开始看了之后,发现,其实完全没这个必要。除了翻译的烂之外(一大段中文下来,有时候你就会骂娘:这tm想说的是个shen me gui),所有的,多线程所必须掌握的知识点,深入点,全部涵盖其中,只能说,一书在手,万线程不愁那种!当然,你必须要全部读懂,并融汇贯通之后,才能有的效果。我推荐,看这本书的中文版本,不要和哪一段的冗长的字句在那过多的纠缠,尽量一段一段的读,然后获取这一段中最重要的那句话,否则你会陷入中文阅读理解的怪圈,而怀疑你的高中语文老师是不是体育老师客串的!!我举个例子:13页第八段,我整段读了三遍硬是没想明白前面那么多的文字,是干什么用的,就是最后一句话才是核心:告诉你,线程安全性,最正规的定义应该是什么!(情允许我,向上交的几个翻译此书的,所谓的“教授”致敬,在你们的引领下,使我们的意志与忍受力更上了一个台阶,人生更加完美!)

<!– vscode-markdown-toc-config
numbering=true
autoSave=true
/vscode-markdown-toc-config –>

## 一、多线程开发所要平衡的几个点

看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点

  • 安全性
  • 活跃性
    • 无限循环问题
    • 死锁问题
    • 饥饿问题
    • 活锁问题(这个还没具体的了解到)
  • 性能要求
    • 吞吐量的问题
    • 可伸缩性的问题

## 二、多线程开发所要关注的开发点

要想平衡好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:

  • 原子性

    • 先检查后执行
    • 原子类
    • 加锁机制
  • 可见性
    • 重排
    • 非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值,而结果却相同

**3. 使用原子类解决上面代码问题**

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语句块
  • 当然,只有一个线程执行的程序,请忽略(那还叫能用的程序吗?)

**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);
}
}

在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。

## 四、可见性下面的对象共享

可见性这个话题,在多线程的环境下,是相当棘手的。可能多年之后,你成为万人心中的老鸟,也同样会对这个问题,怅然若失!我自己总结了几点,可见性问题的难处:

  • 道理简单,真实场景代码错中复杂,你根本不知道是可见性导致的
  • 小概率事件,往往可能只有百分之一,甚至千分之一的出事概率,容易被我们“得过且过”
  • 容易直接扔到一个synchronized加锁块里面,进行“大刀”式的处理,而忽略了高效性
  • 可见性+原子性的综合考虑

针对这些问题,我们只能先从基本功抓起,然后在日积月累的开发工作中,多多分析程序运行的场景,多多尝试,才能大有裨益。

插曲:昨天看了《恋爱回旋》这部日本电影,其中有个场景让我记忆深刻:女主是小时候被魔鬼母亲常年训练的乒乓球少年运动员,后来总总原因,放弃了乒乓球,当起了OL,这一别就是15年。当再次碰到男主的时候,男主向女主发起乒乓球挑战,以为女主是个菜逼,然后赌一些必须要让女主完成的事情。(女主本人也是觉得乒乓球对自己是一种心理的负担,并且放弃这么久了,所以没啥子自信)没想到,女主一拿球拍,在接发球的那一刹那。。。。。大家应该都懂了。我当时就在影院中说出声来:基本功太重要了。

**1. 可见性的发生的必要条件**

可见性,无非就是再多线程环境下,对共享变量的读写导致的。可能一个线程修改了共享变量的值,而另一个线程读取的还是老的值,差不多就是这么大白话的解释了下来。其中发生的必要条件有:

  • 多线程环境访问同一个共享变量
  • 服务器模式下启动程序
  • 共享变量并没有做什么处理,代码块也没有同步

当然,要分析为什么会有可见性的问题,要结合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;
}
}

思考:上面的打印number的值,有可能会有几种结果呢?什么情况下出现的这些个结果

**3. 上诉代码分析**

  • number最终打印结果有可能出现42、0,或者根本就不会打印
  • 42:这种情况是运行正确的结果
  • 0:这种情况发生了指令重排(五星级的问题)
  • 不会打印:主线程对ReaderThread线程出现了共享变量不可见

**4. “愚钝”的聊聊指令重排**

之所以说是“愚钝”,原因是重排问题,是一个很底层很考验计算机基础能力的一个问题,小弟不才,当年分析计算机组成原理与指令结构的时候,枯燥极致,都睡过去了。现在回头,才知道其重要性。现阶段,对重排的分析,我只能举例个简单的例子,进行说明,更进一步的分析,同样是要结合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线程里面并没有修改,因为两个线程访问的空间并不一样,一个线程对另一个线程空间并不可见。

**6. volatile关键字横空出世**

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;
}
}
}
  • 加锁可以同时保证可见性与原子性
  • 加锁同样可以防止指令重排,内部代码都会照顺序执行

**8. volatile不是万能的**

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

## 五、第一部分总结

我们主要讲了线程的原子性和可见性,结合代码,不知不觉就讲了一堆,而且感觉还可以在讲~~多线程的话题真的是太恐怖了!未来的可预见性的规划如下:

  • 对象的安全发布
  • 对象的不变性
  • 对象的合理加锁
  • 生产者消费者模型
  • 构建高效可伸缩的缓存

恩,敬请期待~

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,954
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,479
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,291
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,108
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,740
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,774