java并发编程 1
1.1 上下文切换
并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程就能让程序最大限度地并发执行。
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制,线程是CPU最小调度单位,时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以一个任务从保存到另一个任务再加载的过程就是一次上下文切换。
这就像平时我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,我们会及时用纸质书签标记当前的页数,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
1.1.1 多线程与单线程执行效率
下面的代码演示串行和并发执行的操作时间:
1 public class ConcurrentTest { 2 3 public static final long count = 100000001; 4 5 public static void main(String[] args) throws InterruptedException { 6 concurrency(); 7 serial(); 8 } 9 10 private static void serial() { 11 long start = System.currentTimeMillis(); 12 int a = 0; 13 for (int i = 0; i <count ; i++) { 14 a += 5; 15 } 16 int b = 0; 17 for (int i = 0; i <count ; i++) { 18 b--; 19 } 20 long time = System.currentTimeMillis()-start; 21 System.out.println("serial:" + time +"ms,b="+b+",a="+a); 22 } 23 24 private static void concurrency() throws InterruptedException { 25 long start = System.currentTimeMillis(); 26 Thread thread = new Thread(new Runnable() { 27 @Override 28 public void run() { 29 int a = 0; 30 for (int i = 0; i <count ; i++) { 31 a+=5; 32 } 33 } 34 }); 35 thread.start(); 36 int b = 0; 37 for (int i = 0; i < count; i++) { 38 b--; 39 } 40 thread.join(); 41 long time = System.currentTimeMillis()-start; 42 System.out.println("concurreny:"+time+"ms,b="+b); 43 } 44 }
通过更改count的大小,从而更改线程操作的量,我的机器上测试结果是count小于等于100W时,count越接近100w,并发的执行效率越接近串行,此时并发的效率小于等于串行;count大于100w时,count值越大,并发的执行效率越大于串行。
那么,为什么并发的执行速度会比串行慢尼,这就是之前提到的,线程有创建和上下文切换的开销。使用Lmbench3,vmstat测量上下文切换次数的示例,大概每秒切换1000多次。
1.1.2 如何减少上下文的切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
- 无锁并发编程,多线程竞争锁时,会引起上下文切换,所以多线程处理数据时可以使用一些方法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法,java的Atomic包使用使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程,避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务之间的切换。
1.2 死锁
一旦产生死锁,就会造成系统功能不可用。以下是一段死锁的代码,线程t1和线程t2互相等待对方释放锁。
1 public class DeadLockDemo { 2 private static String A = "A"; 3 private static String B = "B"; 4 5 public static void main(String[] args) { 6 new DeadLockDemo().deadLock(); 7 } 8 9 private void deadLock() { 10 Thread t1 = new Thread(new Runnable() { 11 @Override 12 public void run() { 13 synchronized (A) { 14 try { 15 Thread.currentThread().sleep(10000); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 synchronized (B) { 20 System.out.println("1"); 21 } 22 } 23 } 24 }); 25 Thread t2 = new Thread(new Runnable() { 26 @Override 27 public void run() { 28 synchronized (B) { 29 synchronized (A) { 30 System.out.println("2"); 31 } 32 } 33 } 34 }); 35 t1.start(); 36 t2.start(); 37 } 38 }
这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为复杂的业务场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环),又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放掉。
避免死锁的几个常见方法:
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内占据多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.trylock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
1.3 资源限制的挑战
并发编程时要考虑的资源限制,程序受限于计算机软硬件资源
硬件:带宽的上传下载速度,硬盘读写速度和CPU的处理速度
软件:数据库的连接数和socket连接数等
资源限制引发的问题:
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这个时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发地下载和处理数据时,导致CPU利用率达到100% ,几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。
如何解决资源限制的问题:
对于硬件资源的限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据;对于软件资源的限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
在资源限制的情况下进行并发编程:
如何在资源限制的情况下,让程序执行的更快尼,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

浙公网安备 33010602011771号