线程带来的安全性问题
多线程往往执行操作的结果不可预测,如下代码,创建了两个线程执行i+1各1000次,而此时数据的结果是不可测的。
public class Test01 implements Runnable {
static int i=0;
@Override
public void run() {
for(int j = 0;j < 1000;j++){
i++;
}
}
public static void main(String[] args) {
Test01 r= new Test01();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
while (true){
if(!t1.isAlive()&&!t2.isAlive()){
System.out.println(i);
}
}
}
}
可见性问题
在单核CPU时代,所有的线程共用一个CPU,缓存和内存间的一致性容易解决,多个线程共享一个缓存区,每次先把i读取到CPU缓存区,再执行i+1操作,再写回内存,线程1和线程2是可见的如下图
但是多核情况下,每个CPU都有自己的核心,每个核心都有自己的缓存,当多个线程在不同的核心上执行的时候,这些线程操作的缓存就不是同一个了,如图
原子性问题
什么是原子性?:原子性指代一段操作要么都执行,要么都不执行。
而上面代码中i++的操作实际上在内存中的操作是,先从内存把i取到缓存区,再进行i+1,然后i写回内存,这里分成了3个步骤,而实际上,有可能线程1将i取到缓存中的时候,还没将i+1写回内存,这时候由于CPU时间片的不确定性导致线程2先将i从内存取出了,这时候线程1又重新获得时间片,将i+1写入内存,线程2也将i+1写入内存,这是线程2的结果将线程1的结果覆盖,导致线程1作了无用功。
有序性问题
并发编程还带了额有序性的问题,在计算机的指令的先后执行顺序,常见的类加载过程,如图
类加载过程分加载,连接,初始化,使用,卸载,5个阶段,其中连接阶段又可以分3个过程(验证,准备,解析)而连接阶段三个过程的顺序是不太一致的,通常交叉进行。一般有序性问题是编译器造成的,其目的为了优化系统性能更换指令的顺序。
活跃性问题
线程总能及时的执行称之为活跃性,而活跃性问题就是使线程不能及时的执行乃至永远无法执行下去。
活跃性问题的三种情况:
死锁
线程间互相保持有对方所需要的线程,又没有线程释放自身的资源,这种情况产生了死锁。死锁的情况可以比喻成一个十字路口,如下图
每个汽车都是一个线程,这些汽车就是不让路,都要等前面的车让,这样所有的车都走不了,这就是死锁。
死锁的四个必要条件:
- 互斥条件:线程对分配到的资源独占,直到线程结束释放资源。
- 请求和保持条件:线程持有某个资源的情况下,又请求新的资源
- 不剥夺条件:线程以获得的资源,不可被剥夺,只能自己释放
- 循环等待条件:线程互相等待对方的资源释放。
活锁
两个线程都需要获取对方的资源的时候,在某些情况下,它们会释放已获得的资源,然后又开始再次尝试请求,这样线程间保持同步,这样的过程会不断重复。这时是没有线程阻塞的,但是线程仍然不会继续往下执行。这种情况称为活锁。例如,平时偶然正面碰到的陌生人,莫名默契,都想给对方让路但是总是保持步调一致,刚好又挡了对方的路。
饥饿
饥饿指的是线程因为某种原因获取不到CPU的时间片导致一致无法得到执行,这称为饥饿现象。一般来说,通过线程的setPriority设置线程的优先级,但是这个方法仅使对cpu起到建议的作用,不能保证线程会优先执行,但同时也有可能造成cpu过度执行,导致其他线程得不到时间片执行下去。比如,皇帝翻拍选择妃子侍寝,设置线程优先级相当于太监把个别妃子牌子放比较前面这样容易被选中,但是翻牌全凭皇帝喜好,但是有可能会让皇帝独宠某个妃子,导致其他的妃子被冷落。
性能问题
多线程中存在了比较重要的影响性能的因素就是线程切换 ,也称为上下文切换这种操作开销比较大。
一个线程包含:程序计数器,寄存器,堆栈,状态
线程的切换如下:
线程切换到另一线程线程,需要保存其状态,然后恢复另一线程的状态,记载新的程序计数器,在这过程中cpu不执行相关代码,而是进行线程的切换,所以线程切换的开销大。