前言
咱们之前也提到过线程不光有它的劣势,同样的线程也会带来肯定的危险
个别有以下三点问题:
- 线程安全性问题
- 线程活跃性问题
- 线程性能问题
从第一点到第二点线程带来的危险严重性顺次升高,第三个其实就是相当于优化。
线程安全性问题是一块大的专题,所以咱们在解决线程安全性问题之前,咱们先把第二点和第三点这两个问题先解决了。
一、线程活跃性问题
什么是活跃性问题呢?活跃性问题有多种形式,比方死锁、饥饿、活锁等等就是一种活跃性问题的体现。
死锁问题剖析
死锁有一个十分经典的就是所谓的哲学家就餐的问题,有五个哲学家,每个哲学家在吃饭的时候,分给他们每人一只筷子,每个人只有一只筷子。
咱们只有个别吃饭都须要用一双筷子,那么这些哲学家在议论问题的时候,可能有些哲学家就饿了,于是就借用他旁边的人的筷子就组成了一双筷子,这样就能够吃饭了
然而如果每个人都不聊哲学问题了,都在那里吃饭,于是每个人都拿起了本人手中的那双筷子,再期待着另外一个人放下筷子,后果每个人都不放下本人手中的筷子,后果这五个哲学家最终就饿死了
就是说另外一个人手中有这个人所须要的资源,而另外一个人又有这个人手中所须要的资源,然而这两个资源他们两个人都不开释,所以他们两个人都拿不到本人须要的资源,这就是死锁问题
饥饿问题剖析
就是餐厅排队吃饭,只有一个打饭窗口,有很多人来排队打饭,然而这些人十分没有素质十分没有礼貌来了就插队硬挤,而且买到了饭之后也不来到打饭窗口
可能就有那么一个强大的同学,他死活就是挤不进去不能打饭,于是就被饿死了
在咱们的线程中,线程有优先级这么一个概念,有的线程的优先级高,有的线程的优先级低,那么优先级低的线程,它就有可能始终得不到 CPU 的资源,也就是所谓的饥饿问题
活锁问题剖析
相当于,两个人,这两个人十分的有礼貌,这两个人在独木桥相遇了,(假如一条河下面有两座桥)这两个人十分的有礼貌,握了一个手,说,“不好意思”,而后就退回去了
另一个人也不好意的退回去了,于是抉择了另外的一条路,后果两个人又都在另一座桥上相遇,同理始终这样上来于是就始终反反复复。这就是活锁问题
二、线程性能的问题
比如说咱们想看 sping 的材料,于是百度进入 Spring 的官网链接进去
然而咱们来到这个网页上之后,咱们发现,很多货色我都不意识
比如说我不意识 Supports 这个英文单词,我翻阅了一本英文词典查了一下这个单词
而后我回到那个网页,找到 Supports 这个中央,才能够持续往下读。
也就是说当我在查阅英文单词的时候:
- 我须要记住我在刚刚读的网页中读到了哪一页的哪一个中央了
- 我再来打开这本词典查问这个英文单词
- 再对到网页中翻到刚刚读到的地位,这就是所谓的上下文切换。
这样的话咱们晓得,显然不如间接在网页中往下读快。这就是性能问题
三、饥饿与偏心问题
咱之前也曾经说过一个餐厅外面排队的例子,如果始终打不到饭,那么就饿死了
咱们晓得当咱们调用线程 Start 办法的时候,线程就会处于就绪状态
而就绪状态到运行状态之间是毫无法则的,谁能抢到 CPU 的工夫片,那么谁就能够运行
而刚刚的的例子就是所谓的高优先级吞噬所有低优先级的 CPU 工夫片,也就是在抢占 CPU 工夫片的时候,高优先级它有可能会比低优先级的要快一些,但并不是相对的
而低优先级的线程若始终没有抢占到 CPU 的工夫片,那么也就导致了饥饿
除此之外还有就是所谓的线程被永恒梗塞在一个期待进入同步块的状态
比如说,这里有一个办法两个线程同时的去执行这个办法
而这个办法加了锁了,当一个线程进来之后,在这外面干不完了,始终出不来了,那么,另外一个线程就始终等在这个办法里面
四、单线程与多线程的不同
个别在没有短缺的同步的状况下,多个线程中的操作的执行程序是不可预测的,那么可能就会咱们说在单线程中失常执行的问题,那么在多线程中可能就会呈现十分奇怪的问题
那么咱们就来举一个例子来看一下线程安全性问题,咱们写一个数值序列生成器
public class Sequence {
int value;
public int getNext(){return value++;}
}
在这个类外面实现一个变量来保留以后的值,而后提供一个办法获取下一个值
那么当咱们程序在调用的过程中,咱们就能够发现不管怎么执行,它必定是自增的,而且合乎咱们的预期
public static void main(String[] args) {Sequence sequence =new Sequence();
while(true){System.out.println(sequence.getNext());
}
}
// 运行后果如下:426358
426359
426360
426361
426362
426363
那么在咱们多线程环境下,它会不会就有可能会呈现不可预期的问题,执行的程序是不是不可预期的呢?
public static void main(String[] args) {Sequence sequence =new Sequence();
for (int i = 0 ;i<3; i++){new Thread(new Runnable() {
@Override
public void run() {while (true){System.out.println(Thread.currentThread().getName() +" " +sequence.getNext());
try {Thread.sleep(100);
} catch (InterruptedException e) {e.printStackTrace();
}
}
}
}).start();}
}
// 运行后果如下:Thread-1 0
Thread-0 1
Thread-2 1
Thread-0 2
Thread-1 2
Thread-2 3
Thread-1 3
Thread-0 3
发现出问题了,咱们工作数值序列生成器不是产生一个惟一的产生反复的数值了,这样显然是不行的,那么咱们发现这就是所谓的线程安全性问题
咱们一起来剖析看看是哪里出了问题,首先如图所对应三个线程
那么每一个线程是独立的,它都会执行这么一段代码 getNext() 办法
value++ 是个什么意思呢?value = value + 1
也就是说 value 的值先加 1 而后在赋给 value,所以 value++ 其实并不是一步,它相当于是两步操作
咱们查看字节码来剖析看看,是怎么回事,是怎么演示的?
首先咱们定义这个变量是处于多个线程共享的一块区域,它属于一个公共区域外面,最初始的时候它的值为 0
当第一个线程执行完 iadd 之后,value 的值在咱们的操作数栈中变成 1 了然而它还没有去设置 value 的值,因而此时 value 的值还是 0
此时,如果第二个线程抢到了 CPU 的执行工夫片,于是它也在执行 iadd 因为前一个线程并没有写入批改,所以此时它获取到的 value 的值也是 0
而当咱们开始执行 putfield,执行往栈帧中的局部变量表中的 value 进行赋值的操作给 value 赋值为 1
因为线程执行 iadd 时 value 值为 0,而 ++ 后为 1,所以两个线程应该是 2 的反而后果还是 1,这就是造成了线程平安问题
那么如何解决线程安全性问题呢?其实非常简单,咱们只须要在这里加一行
这样就不再会呈现线程安全性问题了,咱们再来执行代码看看吧
这里总结几点,具备以下这三个条件才会产生线程安全性问题。
- 第一个是多线程环境下
- 第二个是多个线程共享一个资源
- 第三个是对共享资源进行非原子性操作
参考资料
龙果学院:并发编程原理与实战(叶子猿老师)