关于java:并发编程的优缺点

6次阅读

共计 3988 个字符,预计需要花费 10 分钟才能阅读完成。

1. 为什么要用到并发

始终以来,硬件的倒退极其迅速,也有一个很驰名的 ” 摩尔定律 ”,可能会奇怪明明探讨的是并发编程为什么会扯到了硬件的倒退,这其中的关系应该是多核 CPU 的倒退为并发编程提供的硬件根底。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对将来的一种预测。依照所预测的速度,咱们的计算能力会依照指数级别的速度增长,不久以后会领有超强的计算能力,正是在畅想将来的时候,2004 年,Intel 发表 4GHz 芯片的打算推延到 2005 年,而后在 2004 年秋季,Intel 发表彻底勾销 4GHz 的打算,也就是说摩尔定律的有效性超过了半个世纪戛然而止。然而,聪慧的硬件工程师并没有进行研发的脚步,他们为了进一步晋升计算速度,而不是再谋求独自的计算单元,而是将多个计算单元整合到了一起,也就是造成了多核 CPU。短短十几年的工夫,家用型 CPU, 比方 Intel i7 就能够达到 4 外围甚至 8 外围。而业余服务器则通常能够达到几个独立的 CPU,每一个 CPU 甚至领有多达 8 个以上的内核。因而,摩尔定律仿佛在 CPU 外围扩大上持续失去体验。因而,多核的 CPU 的背景下,催生了并发编程的趋势,通过 并发编程的模式能够将多核 CPU 的计算能力施展到极致,性能失去晋升

顶级计算机科学家 Donald Ervin Knuth 如此评估这种状况:在我看来,这种景象(并发)或多或少是因为硬件设计者机关用尽了导致的,他们将摩尔定律的责任推给了软件开发者。

另外,在非凡的业务场景下先天的就适宜于并发编程。比方在图像处理畛域,一张 1024X768 像素的图片,蕴含达到 78 万 6 千多个像素。即时将所有的像素遍历一边都须要很长的工夫,面对如此简单的计算量就须要充分利用多核的计算的能力。又比方当咱们在网上购物时,为了晋升响应速度,须要拆分,减库存,生成订单等等这些操作,就能够进行拆分利用多线程的技术实现。面对简单业务模型,并行程序会比串行程序更适应业务需要,而并发编程更能吻合这种业务拆分。正是因为这些长处,使得多线程技术可能失去器重,也是一名 CS 学习者应该把握的:

  • 充分利用多核 CPU 的计算能力;
  • 不便进行业务拆分,晋升利用性能

2. 并发编程有哪些毛病

多线程技术有这么多的益处,难道就没有一点毛病么,就在任何场景下就肯定实用么?很显然不是。

2.1 频繁的上下文切换

工夫片是 CPU 调配给各个线程的工夫,因为工夫十分短,所以 CPU 一直通过切换线程,让咱们感觉多个线程是同时执行的,工夫片个别是几十毫秒。而每次切换时,须要保留以后的状态起来,以便可能进行复原先前状态,而这个切换时十分损耗性能,过于频繁反而无奈施展出多线程编程的劣势。通常缩小上下文切换能够采纳无锁并发编程,CAS 算法,应用起码的线程和应用协程。

  • 无锁并发编程:能够参照 concurrentHashMap 锁分段的思维,不同的线程解决不同段的数据,这样在多线程竞争的条件下,能够缩小上下文切换的工夫。
  • CAS 算法,利用 Atomic 下应用 CAS 算法来更新数据,应用了乐观锁,能够无效的缩小一部分不必要的锁竞争带来的上下文切换
  • 应用起码线程:防止创立不须要的线程,比方工作很少,然而创立了很多的线程,这样会造成大量的线程都处于期待状态
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个工作间的切换

因为上下文切换也是个绝对比拟耗时的操作,所以在 ”java 并发编程的艺术 ” 一书中有过一个试验,并发累加未必会比串行累加速度要快。能够应用Lmbench3 测量上下文切换的时长 vmstat 测量上下文切换次数

2.2 线程平安

多线程编程中最难以把握的就是临界区线程平安问题,略微不留神就会呈现死锁的状况,一旦产生死锁就会造成零碎性能不可用。

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {deadLock();
    }

    public static void deadLock() {Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {synchronized (resource_a) {System.out.println("get resource a");
                    try {Thread.sleep(3000);
                        synchronized (resource_b) {System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {synchronized (resource_b) {System.out.println("get resource b");
                    synchronized (resource_a) {System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();}
}
复制代码

在下面的这个 demo 中,开启了两个线程 threadA, threadB, 其中 threadA 占用了 resource_a, 并期待被 threadB 开释的 resource _b。threadB 占用了 resource _b 正在期待被 threadA 开释的 resource _a。因而 threadA,threadB 呈现线程平安的问题,造成死锁。同样能够通过 jps,jstack 证实这种推论:

"Thread-1":
  waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)
        - waiting to lock <0x00000007d5ff53a8(a java.lang.String)
        - locked <0x00000007d5ff53d8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
"Thread-0":
        at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)
        - waiting to lock <0x00000007d5ff53d8(a java.lang.String)
        - locked <0x00000007d5ff53a8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.
复制代码

如上所述,齐全能够看出以后死锁的状况。

那么,通常能够用如下形式防止死锁的状况:

  1. 防止一个线程同时取得多个锁;
  2. 防止一个线程在锁外部占有多个资源,尽量保障每个锁只占用一个资源;
  3. 尝试应用定时锁,应用 lock.tryLock(timeOut),当超时期待时以后线程不会阻塞;
  4. 对于数据库锁,加锁和解锁必须在一个数据库连贯里,否则会呈现解锁失败的状况

所以,如何正确的应用多线程编程技术有很大的学识,比方如何保障线程平安,如何正确理解因为 JMM 内存模型在原子性,有序性,可见性带来的问题,比方数据脏读,DCL 等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你播种颇丰。

3. 应该理解的概念

3.1 同步 VS 异步

同步和异步通常用来形容一次办法调用。同步办法调用一开始,调用者必须期待被调用的办法完结后,调用者前面的代码能力执行。而异步调用,指的是,调用者不必管被调用办法是否实现,都会继续执行前面的代码,当被调用的办法实现后会告诉调用者。比方,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你能力持续去收银台付款,这就相似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不必管了,该干嘛就干嘛去了,当货物达到后你收到告诉去取就好。

3.2 并发与并行

并发和并行是非常容易混同的概念。并发指的是多个工作交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果零碎内只有一个 CPU,而应用多线程时,那么实在零碎环境下不能并行,只能通过切换工夫片的形式交替进行,而成为并发执行工作。真正的并行也只能呈现在领有多个 CPU 的零碎中。

3.3 阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响,比方一个线程占有了临界区资源,那么其余线程须要这个资源就必须进行期待该资源的开释,会导致期待的线程挂起,这种状况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程能够阻塞其余线程,所有的线程都会尝试地往前运行。

3.4 临界区

临界区用来示意一种公共资源或者说是共享数据,能够被多个线程应用。然而每个线程应用时,一旦临界区资源被一个线程占有,那么其余线程必须期待。

正文完
 0