一,并发编程的一些问题。
并发编程的初衷是为了让程序运行的更快,但是,并不是启动更多的线程就能让线程运行的更快,在进行并发编程时,如果希望程序运行的更快,则需要注意线程的上下文切换问题、死锁问题、以及硬件和软件现在的问题,只有在考虑到这些问题时,我们才能用好并发编程,编写出快的并发代码。
一 ,上下切换问题
什么是上下文切换 Why?
处理器在执行多线程代码时,CPU通过给每个线程分配时间片来实现这个机制。时间片就是cpu分给每个线程执行程序的时间,通常非常短,所以cpu通过不停的切换线程执行,让我们感觉是同时执行的,但是这样就会遇到一些问题,当一个线程切换到另一个线程时,在切换前需要保存上一个线程的进度,以便下一次切换到它时,可以在加载到这个进度,继续执行。这个线程从保存到加载的过程就叫线程的上下文切换,这种事比较影响执行速度的。
例子:这就像我们同时在读两本书,当我们在读一本英文技术书时,发现了一个单词不认识,需要去查词典,但是查之前需要记录我们看到什么地方了,然后再去查单词,查完单词,大脑回忆英文技术读到哪里了,才能继续读英文技术书。这样来回的切换是不是影响了读书的效率,CPU来回的切换也是同样的道理。
并行和串行的问题(哪个执行快了)
public class ConcyrrencyTest {
private static final long count =10000001;
public static void main(String[] args) throws InterruptedException {
concyrrency();
serial();
}
private static void concyrrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for(long i = 0; i < count; i ++) {
a += 5;
}
//System.out.println("concyrrency a data is ---------------> "+a);
}
});
thread.start();
int b = 0;
for(long i = 0; i < count; i ++) {
b --;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concyrrency time is "+ time+"ms,b="+b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for(long i = 0; i < count; i ++) {
a +=5;
}
int b = 0;
for(long i = 0; i < count; i ++) {
b --;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial time is "+time+"ms,b="+b+",a="+a);
}
}
通过代码我们发现当这个count小于百万时两个方法的执行时间没啥区别。为什么多线程还慢了,就是因为线程的上下文切换导致的,所以得出结论不是线程越多代码执行就越快。
如何减少上下文切换?
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以在多线程处理数据时,可以用一些办法来避免使用锁,例如数据根据ID的hash算法取模分段,不同的线程处理不同的数据。
- CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少的线程。避免创建不需要的线程。例如任务少,创建了很多线程来处理,这样会造成大量的线程处于等待状态。
- 协程。在单线程里实现多任务的调度,并在单线程里实现多个任务的切换。
二,死锁
什么是死锁 why?
死锁就是一个线程在等待另一个线程释放锁,而另一个线程也在等待第一线程释放锁,也就是两个线程在等待对方释放锁。
如何定位死锁了
如果出现了死锁,业务是可以感知的,因为不能继续服务了,那么我们可以通过dump线程查到到底是哪个线程输了问题。
如何避免死锁
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占有多个资源,尽量保证一个线程一个资源。
- 尝试使用定时锁,使用lock trylockt(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的问题。
本文参考《Java 并发编程的艺术》