并发编程的目的是为了让我们的程序变得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。线程不在于多,在并发编程中,我们将面临如下挑战:上下文切换、死锁问题、受限于软硬件资源限制问题。
单核CPU是如果实现多线程的呢?
CPU通过给每个线程分配时间片
来实现,时间片
是分配给线程的时间。因为时间片
非常短,所以CPU不停地切换线程执行,我们感觉上是同时执行的。(一般的,时间片只有十几毫秒 ms
)
当一个任务执行一个时间片之后,会切换成下一个任务执行,但是切换之前会保存这个任务的状态,以便下次回到这个任务的时候,可以下载上一次的状态,继续执行。这样,一个任务从保存到再加载的过程就是一次上下文切换。
上面给出了时间片
和 上下文切换
的解释。不只是计算机遵循这些原则。
拿现实生活中的例子,我们阅读英文书籍,遇到不会的单词,记住读到的位置,然后去查词典,查会了之后,我们回到上次卡住的位置继续阅读。想到回到原来的位置,大脑必须得记住在什么位置。这样周而复始,查单词和读书之间的切换是会影响读书效率的。这样也就不难理解,上下文切换对多线程的影响了。
多线程一定快吗? 根据一个代码的实例,来探讨一下这个问题:
package com.cuteximi.concurrent; /** * @program: Java-300 * @description: 多线程一定快吗? * @author: TSL * @create: 2018-10-11 14:22 **/ public class TestConcurrent { private static final long count = 10000; public static void main(String[] args) throws InterruptedException { concurrency(); serial(); } // 并行 private static void concurrency() throws InterruptedException{ long start = System.currentTimeMillis(); Thread thread = new Thread(new Runnable() { @Override public void run() { int a = 0; for (int i = 0;i< count;i++){ a+=5; } } }); thread.start(); int b = 0; for (long i = 0;i<count;i++){ b--; } long time = System.currentTimeMillis() - start; thread.join(); System.out.println("concurrency: "+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+" ms,b="+b+",a="+a); } }
经过不断的修改,上述代码的 count 的值。对比两种方式的执行速度。
|count的值|串行方法耗时(ms)|并行方法耗时(ms)|并行比串行快多少|
|--|--|--|--|
|1万|0|1|慢|
|10万|3|4|慢|
|100万|6|6|差不多|
|1000万|18|11|快|
|1亿|160|79|快了一倍多
上表的数据是一个平均值,但是很容易看出多线程就不一定快!
那么一开始的时候为什么串行比并行还要快呢?是因为并行存在创建线程和上下文切换的花销。随着数量的扩大,多线程才显示出优势。
我们不得不重视,上下文切换的开销!
🔐是一个很有用的工具,使用锁的过程中最容易出现的就是死锁,一旦产生死锁就会造成系统不可用。看下面这段代码:
package com.cuteximi.concurrent; /** * @program: Java-300 * @description: 死锁的例子 * @author: TSL * @create: 2018-10-11 15:14 **/ public class DeadLockDemo { private static String A = "A"; private static String B = "B"; public static void main(String[] args) { deadLock(); } private static void deadLock(){ // 线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (A){ try { Thread.sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } synchronized (B){ System.out.println("111111"); } } } }); // 线程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (B){ synchronized (A){ System.out.println("222222222"); } } } }); t1.start(); t2.start(); } }
执行这段代码的时候,一直结束不了,因为线程1 和线程2在互相等待。
如何避免死锁呢?
程序执行速度受限于极端及硬件或软件资源,叫做资源限制。
对于硬件的限制,可以考虑使用集群。比如使用 ODPS、Hadoop或者自己搭建集群。
对于软件资源的限制,可以考虑使用资源池将资源复用。
本文主要说明了在并发编程中遇到几个的挑战,建议使用 JDK 并发包中的提供的并发容器和工具类来解决这一类问题。