淺析Java并發(fā)編程(一)基礎(chǔ)概念&理論

前言

學(xué)習(xí)Java并發(fā)編程,首先要搞清楚一些基礎(chǔ)概念與理論,這有助于進(jìn)一步的理解并發(fā)編程和寫出正確有效的并發(fā)代碼。本文是作者自己對Java并發(fā)編程的一些基礎(chǔ)概念與理論的理解與總結(jié),不對之處,望指出,共勉。

進(jìn)程與線程

簡而言之,進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的基本單位,而線程是操作系統(tǒng)進(jìn)行調(diào)度的基本單位,所有線程共享進(jìn)程所擁有的資源。

推薦閱讀:進(jìn)程和線程之由來

為什么需要并發(fā)

CPU的處理速度越來越快,核心越來越多,但I(xiàn)O的速度相對CPU來說非常的緩慢(就像自行車與火箭)。CPU通過IO獲取數(shù)據(jù)進(jìn)行計(jì)算時經(jīng)常需要等待,導(dǎo)致利用率很低,不能發(fā)揮自身速度的優(yōu)勢(就像一名程序員,一天可以完成一百個需求,公司卻只給其配備一名產(chǎn)品經(jīng)理,每天提一個需求)。而并發(fā)則可以更好的利用CPU,CPU同時為多個線程提供計(jì)算,當(dāng)其中一個線程因IO等待時迅速切換至其他進(jìn)程或線程(就像公司為這名程序員配備多名產(chǎn)品經(jīng)理,不停的提需求,程序員不停的切換項(xiàng)目編寫代碼,充分壓榨其勞動力)。

從另一種角度來說,并發(fā)其實(shí)是一種解耦合的策略,它幫助我們把做什么(目標(biāo))和什么時候做(時機(jī))分開。這樣做可以明顯改進(jìn)應(yīng)用程序的吞吐量(獲得更多的CPU調(diào)度時間)和結(jié)構(gòu)(程序有多個部分在協(xié)同工作)。

并發(fā)編程的優(yōu)勢

  • 充分利用CPU(核心)的計(jì)算能力
  • 提高程序的吞吐量,改善性能

并發(fā)編程的劣勢

  • 并發(fā)在CPU有很多空閑時間時能明顯改進(jìn)程序的性能,但當(dāng)線程數(shù)量較多的時候,線程間頻繁的調(diào)度切換反而會讓系統(tǒng)的性能下降
  • 編寫正確的并發(fā)程序是非常復(fù)雜的,即使對于很簡單的問題
  • 測試并發(fā)程序是困難的,并發(fā)程序中的缺陷通常不易重現(xiàn)也不容易被發(fā)現(xiàn)

Java內(nèi)存模型

JMM

Java內(nèi)存模型(Java Memory Model,JMM) 是對Java并發(fā)編程中線程與內(nèi)存的關(guān)系的定義,即線程間的共享變量存儲在主內(nèi)存(Main Memory) 中,每個線程都有一個私有的本地工作內(nèi)存(Local Memory),線程的本地內(nèi)存中存儲了該線程使用到的共享變量的副本(從主內(nèi)存復(fù)制而來),線程對該變量的所有讀/寫操作都必須在自己的本地內(nèi)存中進(jìn)行,不同的線程之間也無法直接訪問對方本地內(nèi)存中的變量,線程間變量值的傳遞需要通過與主內(nèi)存同步來完成。理解Java內(nèi)存模型,對于編寫正確的Java并發(fā)程序來說至關(guān)重要。

all threads share the main memory. each thread uses a local working memory. refreshing local memory to/from main memory must comply to JMM rules.

推薦閱讀:The Java Language Specification 17.4. Memory Model 、 淺析JVM(二)運(yùn)行時數(shù)據(jù)區(qū) 、深入理解Java內(nèi)存模型

Java并發(fā)編程需要考慮的問題

  • 共享性

由Java內(nèi)存模型得知,共享變量是所有線程共享的,如果多個線程對共享變量同時進(jìn)行讀/寫操作程序可能會達(dá)不到預(yù)期的結(jié)果。當(dāng)然,如果每個線程操作的始終是各自本地工作內(nèi)存中的變量則不存在共享性問題,比如通過方法參數(shù)傳入、使用局部變量、創(chuàng)建新的實(shí)例。有過Java Web開發(fā)經(jīng)驗(yàn)的人都知道,Servlet就是以單實(shí)例多線程的方式工作,和每個請求相關(guān)的數(shù)據(jù)都是通過Servlet的service方法(或者是doGetdoPost方法)的參數(shù)傳入的。只要Servlet中的代碼只使用局部變量,Servlet就不會導(dǎo)致同步問題。Spring MVC的控制器也是這么做的,從請求中獲得的對象都是以方法的參數(shù)傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個請求對應(yīng)一個實(shí)例。

  • 互斥性

互斥性指的是同一時間只允許一個線程對共享變量進(jìn)行操作,以保證線程安全,具有唯一性和排它性。在Java 中通常用鎖來保證共享變量的互斥性,為了提高效率通常允許多個線程同時對共享變量進(jìn)行讀操作,但同一時間內(nèi)只允許一個線程對其進(jìn)行寫操作,所以鎖又分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。對于使用不變模式(被 final 修飾)的“變量”,則無需關(guān)心互斥性,因?yàn)槠渲辉试S線程對其進(jìn)行讀操作。(不變模式也是Java并發(fā)編程時可以考慮的一種設(shè)計(jì)。讓對象的狀態(tài)是不變的,如果希望修改對象的狀態(tài),就會創(chuàng)建對象的副本并將改變寫入副本而不改變原來的對象,這樣就不會出現(xiàn)狀態(tài)不一致的情況,因此不變對象是線程安全的。Java中我們使用頻率極高的String類就采用了這樣的設(shè)計(jì))

  • 原子性

原子性指的是對共享變量的操作是一個獨(dú)立的、不可分割的整體。換句話說,就是一次操作,是一個連續(xù)不可中斷的過程,共享變量的值不會執(zhí)行到一半的時候被其他線程所修改。比如,我們經(jīng)常使用的整數(shù)i++ 的操作,其實(shí)需要分成三個步驟:(1)讀取整數(shù)i 的值;(2)對i進(jìn)行加一操作;(3)將結(jié)果寫回主內(nèi)存。在多線程下該操作便會出現(xiàn)原子性問題,不能獲得預(yù)期的值。

  • 可見性

可見性指的是當(dāng)一個線程對共享變量進(jìn)行更改后,其他線程對更改后的值是可見的(立即對主內(nèi)存進(jìn)行同步)。Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)一個共享變量被volatile修飾時,它會保證線程工作內(nèi)存中修改的值會立即被更新到主內(nèi)存中,其他線程也會將主內(nèi)存中的新值同步至工作內(nèi)存,需要注意的是volatile并不能保證原子性。

如上圖所示,如果可見性得到保證,那么當(dāng)線程1將X的值更改為2時,線程2內(nèi)的X值也將同步為2,否則線程2內(nèi)的X值仍為1。

  • 有序性

為了提高性能,編譯器和處理器可能會對指令做重排序,重排序通??梢苑譃橄旅嫒N

  1. 編譯級別的重排序,比如編譯器的優(yōu)化
  2. 指令級重排序,比如CPU指令執(zhí)行的重排序
  3. 內(nèi)存系統(tǒng)的重排序,比如緩存和讀寫緩沖區(qū)導(dǎo)致的重排序

有序性指的就是在多線程并發(fā)的情況下,代碼實(shí)際執(zhí)行的順序、結(jié)果和單線程是一樣的,不會因?yàn)橹嘏判虻膯栴}導(dǎo)致結(jié)果不可預(yù)知。

<small>注:水平有限,可能理解的不夠透徹,有興趣的可以看看 Doug LeaSynchronization and the Java Memory Model,我想沒有誰比他理解的更透徹了。</small>

Java中創(chuàng)建線程的方式

  • 方式一,繼承java.lang.Thread
 public static void method1() {
        class Task extends Thread {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " Started");
            }
        }

        new Task().start();
    }
  • 方式二,實(shí)現(xiàn)java.lang.Runnable接口(推薦)
public static void method2() {
        class Task implements Runnable {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " Started");
            }
        }
        new Thread(new Task()).start();
    }
  • 方式三,實(shí)現(xiàn)java.util.concurrent.Callable接口(推薦)
public static void method3() {
        class Task implements Callable {

            @Override
            public Object call() throws Exception {
                System.out.println(Thread.currentThread().getName() + " Started");
                //求和
                return 1 + 1;
            }
        }

        ExecutorService es = Executors.newFixedThreadPool(1);
        Future future = es.submit(new Task());
        try {
            System.out.println("Calculate Completed Sum:" + future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        es.shutdown();
    }

查看該部分源碼

參考


查看《淺析Java并發(fā)編程》系列文章目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容