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

前言

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

進(jìn)程與線程

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

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

為什么需要并發(fā)

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

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

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

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

并發(fā)編程的劣勢(shì)

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

Java內(nèi)存模型

JMM

Java內(nèi)存模型(Java Memory Model,JMM) 是對(duì)Java并發(fā)編程中線程與內(nèi)存的關(guān)系的定義,即線程間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory) 中,每個(gè)線程都有一個(gè)私有的本地工作內(nèi)存(Local Memory),線程的本地內(nèi)存中存儲(chǔ)了該線程使用到的共享變量的副本(從主內(nèi)存復(fù)制而來(lái)),線程對(duì)該變量的所有讀/寫操作都必須在自己的本地內(nèi)存中進(jìn)行,不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方本地內(nèi)存中的變量,線程間變量值的傳遞需要通過(guò)與主內(nèi)存同步來(lái)完成。理解Java內(nèi)存模型,對(duì)于編寫正確的Java并發(fā)程序來(lái)說(shuō)至關(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í)數(shù)據(jù)區(qū)深入理解Java內(nèi)存模型

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

  • 共享性

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

  • 互斥性

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

  • 原子性

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

  • 可見性

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

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

  • 有序性

為了提高性能,編譯器和處理器可能會(huì)對(duì)指令做重排序,重排序通常可以分為下面三種

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

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

<small>注:水平有限,可能理解的不夠透徹,有興趣的可以看看 Doug LeaSynchronization and the Java Memory Model,我想沒(méi)有誰(shuí)比他理解的更透徹了。</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)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容