并发编程的艺术之并发编程的挑战

Author Avatar
cuteximi 10月 13, 2018
  • 在其它设备中阅读本文章

并发编程的目的是为了让我们的程序变得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。线程不在于多,在并发编程中,我们将面临如下挑战:上下文切换、死锁问题、受限于软硬件资源限制问题。

上下文切换

单核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/并发编程的艺术之并发编程的挑战/