并发编程的艺术之并发编程的挑战
并发编程的目的是为了让我们的程序变得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。线程不在于多,在并发编程中,我们将面临如下挑战:上下文切换、死锁问题、受限于软硬件资源限制问题。
上下文切换
单核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在互相等待。
如何避免死锁呢?
- 避免一个线程同时获得多把锁。
- 避免一个线程在🔐内同时获得多个资源。尽量保证每把锁仅占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来代替内部锁。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里面,否则会出现解锁失败的情况。
资源限制
程序执行速度受限于极端及硬件或软件资源,叫做资源限制。
- 硬件限制:比如 带宽、硬盘读写速度、CPU处理速度等。
- 软件限制:比如 数据库连接数,sock连接数等。
对于硬件的限制,可以考虑使用集群。比如使用 ODPS、Hadoop或者自己搭建集群。
对于软件资源的限制,可以考虑使用资源池将资源复用。
小结
本文主要说明了在并发编程中遇到几个的挑战,建议使用 JDK 并发包中的提供的并发容器和工具类来解决这一类问题。
This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://blog.cuteximi.com/并发编程的艺术之并发编程的挑战/