前言
多线程可以提高程序响应速度,提高系统的吞吐量,发挥多处理器的强大能力,正式由于多线程的这些优势,所以我们常常会看到并发编程,异步调用这样的需求。
上篇博客讲到了创建多线程的7种方式,这篇博客,总结一下多线程带来的风险,我们只有规避了存在了风险,才能真正的提升系统效率。
线程安全性问题
什么是线程安全性问题?
线程安全问题指的是多个线程同时访问一个共享资源,并且这个资源是非原子性操作,那么结果就无法保证程序的正确性,也就是结果偏离我们的预期。
示例:
public class Sequence {
private int value;
public int getNext() {
return value ++;
}
public static void main(String[] args) {
Sequence s = new Sequence();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println(Thread.currentThread().getName() + " " + s.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println(Thread.currentThread().getName() + " " + s.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
打印结果
我们的期望是按照getNext()方法,实现自增;但是在运行结果中我们可以看到两个60,两个65,这就违反了我们的预期,没有正确的执行程序。这就是线程安全性问题。
出现线程安全性问题的条件
1、多线程环境
2、存在共享资源
3、对共享资源进行非原子性操作
如何解决多线程安全性问题
1、对于共享资源,使用synchronized。synchronized属于重量级锁,而重量级锁就和性能有关联了,下边总结性能的时候再说。
2、共享资源如果是原子性操作,可以使用Volatile关键字修饰。Volatile属于轻量级锁,可以保证可见性,但是不能保证操作的原子性。可见性就是一个线程修改变量的值,其他线程 能够读到这个修改的值。
3、使用Lock接口,使用读锁和写锁分别锁定读操作和写操作。
活跃性问题
活跃性指的是一个并发应用程序能及时执行的能力。活跃性问题有三种情况,分别是死锁,活锁,饥饿。
死锁:
两个或多个线程永久阻塞,互相等待对方释放资源。
可以使用重入锁使线程中断或限时等待来解决死锁问题。
活锁:
活锁指的是任务或执行者没有被阻塞,但由于某些条件没有满足,导致一直重复尝试-失败-尝试-失败。
就像一个小河上有两座桥,河两岸分别有一个人想要过河,首先两人上了同一座桥,碰到后都比较谦让,都退了回去。然后又都很默契的上了另外一个桥,又都回退,又都上了另外一座桥,就这样,两人一直碰面,一直谦让,处于一直活动的状态。
活锁和死锁的区别就是,活锁处于活动的状态,可以自行解开,而死锁处于等待的状态,不能自行解开。
饥饿
饥饿指的是当前线程因为优先级太低,或者其他线程一直不释放资源,而获取不到资源,无法执行。
与死锁相比,饥饿有可以获取到资源的可能,只是可能性比较低,而死锁则是没有可能。
可以设置合适的优先级或者使用锁来替代synchronized,锁是自己设置的,也就是自己控制的,而synchronized是由程序分配的。
性能问题
1、线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。如果创建的线程的大于实际需要的线程,那么多余的闲置的线程将会占用大量的内存。
2、上边说到synchronized是重量级锁,重量级锁是相当耗性能的。当系统检查到时重量级锁后,会把等待想要获得锁的线程进行阻塞,在阻塞或唤醒一个线程时,伴随着会发生cpu时间片的转换,在时间片转换前后,需要保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态。而这个转换以及状态的保存,也就是上下文切换,是相当耗性能的。