Java多线程专题一:并发所面临的问题
并發的概念:
在Java中是支持多線程的,多線程在有的時候可以大提高程序的速度,比如你的程序中有兩個完全不同的功能操作,你可以讓兩個不同的線程去各自執行這兩個操作,互不影響,不需要執行完一個操作才能執行另一個操作。這樣大大提高了效率。但是并不是什么多線程就可以隨便用,有的時候多線程反而會造成系統的負擔,而且多線程還會造成其他的數據問題,下面就來介紹一下多線程面臨的問題。
一、上下問切換問題
在單核處理器上多線程也是可以運行的,它實現的原理其實是每個線程都執行一段時間,快速切換,看上去就好像是所有的線程一起執行。每當CPU切換線程的時候它都會保存上一個線程的狀態,確保下次執行這個線程的時候可以接著上次執行的地方繼續執行,這個保存的狀態的過程就是一次上下文切換。但是保存狀態肯定是需要花時間的,這也就影響了多線程的效率,下面我們用代碼來試驗一下。
1.創建一個Count類,里面有兩個方法,count是讓多線程交替+1打印值并且是線程安全的,sigleCount()只是一個單純的+1方法。
public class Count {private int num = 0;private int max;private boolean flag = true;public Count(int max) {this.max = max;}public synchronized void count() {Long start = System.currentTimeMillis();while (flag) {Thread self = Thread.currentThread();notify();if (num < max) {num++;System.out.println("當前線程-" + self.getName() + "的值為" + num);try {wait();} catch (InterruptedException e) {e.printStackTrace();}} else {flag = false;Long time = System.currentTimeMillis() - start;System.out.println("運行時間" + time);}}}public void singleCount(){Thread self = Thread.currentThread();Long start = System.currentTimeMillis();while (num<max){num++;System.out.println("當前線程-" + self.getName() + "的值為" + num);}Long time = System.currentTimeMillis() - start;System.out.println("運行時間" + time);} }2.再創建ThreadDemo類,里面有兩個方法moreThread和singleThread。moreThread會創建多個線程調用Count類中的count方法交替打印數值,singleThread類則是單獨一個線程執行singleCount方法打印數值。
public class ThreadDemo {public static void main(String[] args) {//從控制臺輸入設置循環打印的次數Scanner scanner = new Scanner(System.in);System.out.println("循環次數");int num = scanner.nextInt();//從控制臺選擇哪種運行方式System.out.println("1多個線程,2單個線程");int flag = scanner.nextInt();if (flag == 1) {//設置創建線程的數量System.out.println("創建線程數量");int threadNum = scanner.nextInt();moreThread(num, threadNum);} else if (flag == 2) {singleThread(num);}}public static void moreThread(int num, int threadNum) {int i;Count count = new Count(num);Runnable runnable = new Runnable() {@Overridepublic void run() {count.count();}};for (i = 0; i < threadNum; i++) {Thread thread = new Thread(runnable);thread.start();}}public static void singleThread(int num) {Count count = new Count(num);Runnable runnable = new Runnable() {@Overridepublic void run() {count.singleCount();}};Thread threadA = new Thread(runnable);threadA.start();} }3.這里我把代碼放到阿里云服務器上運行,配置是單核內存1G處理器,系統是CentOS7分別運行了1000次、5000次、10000次和20000次循環,在單線程執行下執行的時間分別是37ms、75ms、110ms和165ms,在50個線程交替運行下的結果分別是39ms、119ms、210ms和363ms。很明顯多線程并沒有體現出任何優勢,反而更加慢了。
4.我們可以在服務器上監控一下,我們可以輸入vmstat 1來獲取每秒服務器的情況,其中cs那一項代表了每秒上下文切換的次數。下面這張圖是多線程運行時候的情況,我們發現上下文切換的次數暴增。
5.下面這張圖是單線程運行的情況,我們可以看到上下文切換的次數沒有增加多少,就是因為多線程多次切換所以導致代碼的效率沒有提高,反而降低了,時間都浪費在切換線程了。如果想實際測試上下文切換的時間可以使用Lmbench3工具,我這里就不演示了。
6.現在知道是上下文切換過多的問題了,我們可以選擇下面這些方法來減少上下文的切換。
- 無鎖并發編程,為了保證線程安全我們會使用鎖,每次競爭鎖都會造成上下文切換,我們可以減少鎖的使用競爭。比如分段鎖,將數據分為多段,不同的線程操作不同的鎖,避免大量的鎖競爭行為。
- CAS算法,使用特定算法來保證線程的同步安全,不需要使用鎖。
- 合理計算線程數量,任務少的時候就不要創建太多線程,避免無意義的上下文切換。
- 協程,在單線程里實現多任務調度,維持多個任務的切換。
7.根據判斷我們的代碼應該是適合上述第三個方法,因為我們只是一段簡單的自增循環,不需要那么多線程來執行。我們可以在服務器上看一下這些線程的狀態。我們在服務器上輸入jps獲取正在運行的進程pid,看到我們代碼的pid是3902,然后我們輸入jstack 3902 > /usr/local/personal/javaTest /dump.log來把這個進程中所有的信息都保存在這個目錄下。
8.我們打開剛剛保存的那個日志文件,這里日志比較長只截取一部分,里面的內容大致上是各個線程的運行狀況,有沒有發現只有Thread-49這個線程的狀態是RUNNABLE,其他的都是BLOCKED狀態,當然這是因為我們為了測試結果強行讓線程切換,不然的話有可能一個線程搶到執行權之后直接循環完了,沒法和單線程運行形成對比。但是50個線程肯定是只有一個能拿到鎖,也就是說其他49個線程是沒事干的,不僅沒事干還老是相互切換影響我們的效率,所以我們應該選擇合適的線程數量。
"Thread-49" #57 prio=5 os_prio=0 tid=0x00007fc63014b800 nid=0xf79 runnable [0x00007fc60d1cc000]java.lang.Thread.State: RUNNABLEat java.io.FileOutputStream.writeBytes(Native Method)at java.io.FileOutputStream.write(FileOutputStream.java:326)at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)- locked <0x00000000f5978690> (a java.io.BufferedOutputStream)at java.io.PrintStream.write(PrintStream.java:482)- locked <0x00000000f596ad50> (a java.io.PrintStream)at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)- locked <0x00000000f59786d0> (a java.io.OutputStreamWriter)at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)at java.io.PrintStream.newLine(PrintStream.java:546)- locked <0x00000000f596ad50> (a java.io.PrintStream)at java.io.PrintStream.println(PrintStream.java:807)- locked <0x00000000f596ad50> (a java.io.PrintStream)at Count.count(Count.java:20)- locked <0x00000000f5966158> (a Count)at ThreadDemo$1.run(ThreadDemo.java:28)at java.lang.Thread.run(Thread.java:748)"Thread-48" #56 prio=5 os_prio=0 tid=0x00007fc630149800 nid=0xf78 waiting for monitor entry [0x00007fc60d2cd000]java.lang.Thread.State: BLOCKED (on object monitor)at Count.count(Count.java:14)- waiting to lock <0x00000000f5966158> (a Count)at ThreadDemo$1.run(ThreadDemo.java:28)at java.lang.Thread.run(Thread.java:748)"Thread-47" #55 prio=5 os_prio=0 tid=0x00007fc630147000 nid=0xf77 waiting for monitor entry [0x00007fc60d3ce000]java.lang.Thread.State: BLOCKED (on object monitor)at Count.count(Count.java:14)- waiting to lock <0x00000000f5966158> (a Count)at ThreadDemo$1.run(ThreadDemo.java:28)at java.lang.Thread.run(Thread.java:748)"Thread-46" #54 prio=5 os_prio=0 tid=0x00007fc630145000 nid=0xf76 waiting for monitor entry [0x00007fc60d4cf000]java.lang.Thread.State: BLOCKED (on object monitor)at Count.count(Count.java:14)- waiting to lock <0x00000000f5966158> (a Count)at ThreadDemo$1.run(ThreadDemo.java:28)at java.lang.Thread.run(Thread.java:748)二、死鎖
1.因為我們使用多線程可能會發生數據同步的問題,所以我們使用了鎖保證數據同步,但是也有了新的問題那就是死鎖,我們看下面這段代碼的運行情況來了解死鎖。
public class DeadLock {public static void main(String[] args){new DeadLock().deadLock();}public void deadLock(){Object objectA = new Object();Object objectB = new Object();new Thread(new Runnable() {@Overridepublic void run() {synchronized (objectB){System.out.println("線程1獲取了B鎖還想要獲取A鎖");synchronized (objectA){System.out.println("線程1獲取了A鎖");}}}}).start();new Thread(new Runnable() {@Overridepublic void run() {synchronized (objectA){System.out.println("線程2獲取了A鎖還想要獲取B鎖");synchronized (objectB){System.out.println("線程2獲取了B鎖");}}}}).start();} }結果:
線程1獲取了B鎖還想要獲取A鎖
線程2獲取了A鎖還想要獲取B鎖
2.上面就是死鎖的發生的情況,兩個線程,分別獲得了一個鎖,它們還都想獲取對方的鎖,就會一直卡在這里,代碼不會結束也不會報錯。我們可以用jps命令看看線程的狀況,下面這張圖就是我們截取的一部分日志,很清晰的看到發生了一個死鎖。
3.死鎖有幾種避免的方法
- 不要讓同一個線程去獲取多個鎖
- 使用定時鎖,比如Lock,它可以設置獲取鎖的時間,不會一直等待下去
- 每個線程獲取鎖的順序都一致,就不會造成拿著不同的鎖獲取對方的鎖的情況
三、資源限制
舉個例子,當一個服務器的帶寬只有5M,一個線程的下載速度是1M,你開10個線程也只是5M的速度不會有10M的下載速度,這就是資源限制。所以當我們使用多線程的時候要考慮有沒有超過硬件的限制,硬件跟不上,開再多的線程也沒效果。還有一種情況就是類似我們講的上下文切換的問題,硬件配置本來就低,還開那么多線程,資源都消耗在線程的切換上了。對于資源限制的問題我們可以提高硬件配置或者是服務器集群來突破瓶頸。
總結
以上是生活随笔為你收集整理的Java多线程专题一:并发所面临的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于shiro的改造集成真正支持rest
- 下一篇: python报错'str' object