java多线程总结(二)
線程一般有6個(gè)狀態(tài):
新建狀態(tài):NEW
可運(yùn)行狀態(tài):RUNNABLE
休眠狀態(tài):TIMED_WAITING
等待狀態(tài):WAITING
阻塞狀態(tài):BLOCKED
終止?fàn)顟B(tài)“TERMINATED
當(dāng)我們使用new創(chuàng)建線程之后,線程處于新建狀態(tài),當(dāng)調(diào)用start方法之后,線程出于可運(yùn)行狀態(tài),當(dāng)線程需要獲得對(duì)象的內(nèi)置鎖,而這個(gè)鎖被其他線程所占用的時(shí)候,線程就出于阻塞狀態(tài),當(dāng)線程等待其他線程通知調(diào)度表可以運(yùn)行時(shí),線程處于等待狀態(tài),當(dāng)一個(gè)含有時(shí)間參數(shù)的方法,必須sleep()方法,可以讓線程處于計(jì)時(shí)等待狀態(tài),當(dāng)run()方法運(yùn)行完畢或者出現(xiàn)異常,線程處于終止?fàn)顟B(tài)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package?Thread; public?class?ThreadStateDemo{ ????public?static?void?main(String[] args)?throws?Exception{ ????????ThreadState state =?new?ThreadState(); ????????Thread demo =?new?Thread(state); ????????System.out.println("新建狀態(tài):"?+ demo.getState()); ????????demo.start(); ????????System.out.println("可運(yùn)行狀態(tài):"?+ demo.getState()); ????????Thread.sleep(100); ????????System.out.println("休眠狀態(tài):"?+ demo.getState()); ????????Thread.sleep(1000); ????????System.out.println("等待狀態(tài):"?+ demo.getState()); ????????state.notifyWait(); ????????System.out.println("阻塞狀態(tài):"?+ demo.getState()); ????????Thread.sleep(1000); ????????System.out.println("終止?fàn)顟B(tài)“"?+ demo.getState()); ????} } class?ThreadState?implements?Runnable{ ????@Override ????public?void?run(){ ????????try{ ????????????waitForASecond(); ????????????waitForAYear(); ????????}catch(Exception e){ ????????????e.printStackTrace(); ????????} ????} ????// 當(dāng)前線程等待1秒 ????public?synchronized?void?waitForASecond()?throws?Exception{ ????????wait(1000); ????} ????// 當(dāng)前線程一直等待 ????public?synchronized?void?waitForAYear()?throws?Exception{ ????????wait(); ????} ????// 喚醒線程 ????public?synchronized?void?notifyWait()?throws?Exception{ ????????notify(); ????} } |
?
【運(yùn)行結(jié)果】:
?
新建狀態(tài):NEW
?
可運(yùn)行狀態(tài):RUNNABLE
?
休眠狀態(tài):TIMED_WAITING
?
等待狀態(tài):WAITING
?
阻塞狀態(tài):BLOCKED
?
終止?fàn)顟B(tài)“TERMINATED
?
?
?
線程組表示一個(gè)線程線程的集合,線程組中也可以包含其他的線程組。線程組構(gòu)成一棵樹(shù)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | package?Thread; import?java.util.ArrayList; import?java.util.List; public?class?ThreadGroupDemo{ ????public?static?void?main(String[] args){ ????????for(String str : getThreadGroups(GetRootThreadGroups())){ ????????????System.out.println(str); ????????} ????} ????// 獲得根線程組 ????private?static?ThreadGroup GetRootThreadGroups(){ ????????// 獲得當(dāng)前的線程組 ????????ThreadGroup rootGroup = Thread.currentThread().getThreadGroup(); ????????while(true){ ????????????if(rootGroup.getParent() !=?null){ ????????????????rootGroup = rootGroup.getParent(); ????????????}else{ ????????????????break; ????????????} ????????} ????????return?rootGroup; ????} ????// 獲得給定線程組中所有線程名 ????public?static?List<String> getThreads(ThreadGroup group){ ????????List<String> threadList =?new?ArrayList<String>(); ????????Thread[] threads =?new?Thread[group.activeCount()]; ????????int?count = group.enumerate(threads,?false); ????????for(int?i =?0; i < count; i++){ ????????????threadList.add(group.getName() +?" 線程組 "?+ threads[i].getName()); ????????} ????????return?threadList; ????} ????// 獲得線程組中所有子線程組 ????public?static?List<String> getThreadGroups(ThreadGroup group){ ????????List<String> threadList = getThreads(group); ????????ThreadGroup[] groups =?new?ThreadGroup[group.activeGroupCount()]; ????????int?count = group.enumerate(groups,?false); ????????for(int?i =?0; i < count; i++){ ????????????threadList.addAll(getThreads(groups[i])); ????????} ????????return?threadList; ????} } |
?
【運(yùn)行結(jié)果】:
?
system?線程組?Reference Handler
?
system?線程組?Finalizer
?
system?線程組?Signal Dispatcher
?
system?線程組?Attach Listener
?
main?線程組?main
?
?
?
使用守護(hù)線程
?
java中的線程分為2類(lèi),用戶(hù)線程和守護(hù)線程,守護(hù)線程主要為其他線程提供服務(wù),守護(hù)線程會(huì)隨時(shí)被中斷,所以一般不要再守護(hù)線程中使用需要釋放資源的資源,比如輸入輸出流等,守護(hù)線程一般都是后臺(tái)線程,如果虛擬機(jī)只剩下守護(hù)線程,虛擬機(jī)就會(huì)退出。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package?Thread; public?class?DaemonThreadTest{ ????public?static?void?main(String[] args){ ????????Thread worker =?new?Thread(new?Worker()); ????????Thread timer =?new?Thread(new?Timer()); ????????//設(shè)置守護(hù)線程 ????????timer.setDaemon(true); ????????worker.start(); ????????timer.start(); ????} } class?Worker?implements?Runnable{ ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?5; ++i){ ????????????System.out.println("rollen真帥! 第"?+ i +?"次"); ????????} ????} } class?Timer?implements?Runnable{ ????@Override ????public?void?run(){ ????????long?currentTime = System.currentTimeMillis(); ????????long?processTime =?0; ????????while(true){ ????????????if((System.currentTimeMillis() - currentTime) > processTime){ ????????????????processTime = System.currentTimeMillis() - currentTime; ????????????????System.out.println("程序運(yùn)行時(shí)間:"?+ processTime); ????????????} ????????} ????} } |
?
rollen真帥!?第0次
?
程序運(yùn)行時(shí)間:1
?
程序運(yùn)行時(shí)間:2
?
程序運(yùn)行時(shí)間:3
?
程序運(yùn)行時(shí)間:4
?
程序運(yùn)行時(shí)間:5
?
rollen真帥!?第1次
?
rollen真帥!?第2次
?
rollen真帥!?第3次
?
rollen真帥!?第4次
?
程序運(yùn)行時(shí)間:6
?
終止指定的線程
?
雖然在Thread類(lèi)中提供了stop()方法可以終止線程,但是由于其固有的不安全性,所以一般不要采用,本例子只是起到拋磚引玉的作用。
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package?Thread; import?java.awt.FlowLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; public?class?ThreadStopDemo?extends?JFrame{ ????public?ThreadStopDemo(){ ????????panel.setLayout(new?FlowLayout(FlowLayout.CENTER)); ????????panel.add(label); ????????panel.add(startButton); ????????panel.add(endButton); ????????setContentPane(panel); ????????setSize(200,?300); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????????startButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????counter =?new?CounterThread(); ????????????????new?Thread(counter).start(); ????????????} ????????}); ????????endButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????if(counter ==?null){ ????????????????????return; ????????????????} ????????????????counter.setStop(false); ????????????} ????????}); ????} ????public?static?void?main(String[] args){ ????????new?ThreadStopDemo(); ????} ????class?CounterThread?implements?Runnable{ ????????@Override ????????public?void?run(){ ????????????while(this.flag){ ????????????????try{ ????????????????????Thread.sleep(500); ????????????????}catch(Exception e){ ????????????????????e.printStackTrace(); ????????????????} ????????????????label.setText("更新"?+ (count++) +?"更新"); ????????????} ????????} ????????public?void?setStop(boolean?flag){ ????????????this.flag = flag; ????????} ????????private?int?count =?0; ????????private?boolean?flag =?true; ????} ????private?CounterThread counter =?null; ????private?final?JPanel panel =?new?JPanel(); ????private?final?JLabel label =?new?JLabel("更新0次"); ????private?final?JButton startButton =?new?JButton("開(kāi)始"); ????private?final?JButton endButton =?new?JButton("結(jié)束"); } |
?
?
【運(yùn)行結(jié)果】:
?
?
?
?
?
線程的插隊(duì)
?
在編寫(xiě)多線程的程序的時(shí)候,經(jīng)常會(huì)遇到讓一個(gè)線程優(yōu)先于另外i個(gè)線程運(yùn)行的情況,此時(shí),除了設(shè)置這個(gè)線程的優(yōu)先級(jí)高(不推薦這種方法)之外,更加直接的辦法是采用Thread類(lèi)中的join()方法。當(dāng)插隊(duì)的線程運(yùn)行結(jié)束之后,其他的線程才能運(yùn)行。
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package?Thread; public?class?ThreadJoinDemo{ ????public?static?void?main(String[] args){ ????????Thread demo1 =?new?Thread(new?EmergencyThread()); ????????demo1.start(); ????????for(int?i =?0; i <?5; ++i){ ????????????try{ ????????????????Thread.sleep(100); ????????????}catch(InterruptedException e){ ????????????????e.printStackTrace(); ????????????} ????????????System.out.println("正常情況 :"?+ i +?"號(hào)車(chē)開(kāi)始出發(fā)"); ????????????try{ ????????????????//開(kāi)始插隊(duì) ????????????????demo1.join(); ????????????}catch(InterruptedException e){ ????????????????e.printStackTrace(); ????????????} ????????} ????} } class?EmergencyThread?implements?Runnable{ ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?5; ++i){ ????????????try{ ????????????????Thread.sleep(100); ????????????}catch(InterruptedException e){ ????????????????e.printStackTrace(); ????????????} ????????????System.out.println("緊急情況 :"?+ i +?"號(hào)車(chē)開(kāi)始出發(fā)"); ????????} ????} } |
?
?
【運(yùn)行結(jié)果】:
?
正常情況?:0號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:0號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:1號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:2號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:3號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:4號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:1號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:2號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:3號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:4號(hào)車(chē)開(kāi)始出發(fā)
?
如果我們?nèi)サ鬸oin哪一行的話,運(yùn)行結(jié)果:(結(jié)果不唯一)
?
緊急情況:0號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:0號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:1號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:1號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:2號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:2號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:3號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:3號(hào)車(chē)開(kāi)始出發(fā)
?
正常情況?:4號(hào)車(chē)開(kāi)始出發(fā)
?
緊急情況:4號(hào)車(chē)開(kāi)始出發(fā)
?
線程的同步
?
多線程編程的一個(gè)重要原因是實(shí)現(xiàn)數(shù)據(jù)的共享,但是如果兩個(gè)線程同時(shí)修改一個(gè)數(shù)據(jù)的話,則會(huì)產(chǎn)生同步問(wèn)題。
?
下面采用一個(gè)2個(gè)人同時(shí)往銀行存錢(qián)的例子,銀行卡初始金額為100元。每次存10元,大家仔細(xì)查看余額。(對(duì)于簡(jiǎn)單的多線程,出錯(cuò)的概率很小,今天很不巧,我一向地下的RP今天居然爆發(fā)了,實(shí)驗(yàn)了很多次,都沒(méi)錯(cuò),最后終于出現(xiàn)了)
?
先看一下結(jié)果吧:
?
?
?
案例說(shuō)兩側(cè)不能出現(xiàn)一樣余額的。
?
代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | package?Thread; import?java.awt.GridLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; import?javax.swing.JScrollPane; import?javax.swing.JTextArea; public?class?UnSynBank?extends?JFrame{ ????public?UnSynBank(){ ????????panel.setLayout(new?GridLayout(2,?2,?3,?3)); ????????panel.add(label1); ????????panel.add(label2); ????????JScrollPane js1 =?new?JScrollPane(oneArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js1); ????????JScrollPane js2 =?new?JScrollPane(twoArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js2); ????????panel2.add(panel); ????????panel2.add(statrButton); ????????setContentPane(panel2); ????????statrButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????Thread demo1 =?new?Thread(new?Transfer(bank, oneArea)); ????????????????demo1.start(); ????????????????Thread demo2 =?new?Thread(new?Transfer(bank, twoArea)); ????????????????demo2.start(); ????????????} ????????}); ????????setSize(300,?400); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????} ????public?static?void?main(String[] args){ ????????new?UnSynBank(); ????} ????private?final?Bank bank =?new?Bank(); ????JPanel panel =?new?JPanel(); ????JPanel panel2 =?new?JPanel(); ????private?final?JButton statrButton =?new?JButton("開(kāi)始存錢(qián)"); ????private?final?JLabel label1 =?new?JLabel("一號(hào)線程"); ????private?final?JLabel label2 =?new?JLabel("二號(hào)線程"); ????private?final?JTextArea oneArea =?new?JTextArea(5,?10); ????private?final?JTextArea twoArea =?new?JTextArea(5,?10); } /** ?* 表示銀行的類(lèi) ?* */ class?Bank{ ????public?Bank(){ ????} ????public?void?deposit(int?money){ ????????account += money; ????} ????public?int?getAccount(){ ????????return?account; ????} ????private?int?account =?100; } /** ?* 表示往賬戶(hù)存錢(qián) ?* */ class?Transfer?implements?Runnable{ ????public?Transfer(){ ????} ????public?Transfer(Bank bank, JTextArea textArea){ ????????this.bank = bank; ????????this.textArea = textArea; ????} ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?20; ++i){ ????????????bank.deposit(10); ????????????String str = textArea.getText(); ????????????textArea.setText(str +?"賬戶(hù)余額為:"?+ bank.getAccount() +?"\n"); ????????} ????} ????private?Bank bank =?null; ????private?JTextArea textArea =?null; } |
?
?
?
因?yàn)橐粋€(gè)進(jìn)程中的所有線程會(huì)共享進(jìn)程中的資源,所以當(dāng)一個(gè)線程還沒(méi)有將修改之后的結(jié)果保存的時(shí)候,另外一個(gè)線程卻進(jìn)行讀取,這樣自然會(huì)產(chǎn)生錯(cuò)誤,所以這個(gè)時(shí)候就需要我們采用同步來(lái)解決問(wèn)題的。
?
使用同步方法實(shí)現(xiàn)同步
?
所謂同步方法,就是用synchronized修飾的方法,之所以這十幾個(gè)字母能解決困難的同步問(wèn)題,這是和java中的內(nèi)置鎖密切相關(guān)的,每一個(gè)java對(duì)象中有一個(gè)內(nèi)置鎖,如果方法使用了synchronized進(jìn)行修飾的話,內(nèi)置鎖會(huì)保護(hù)整個(gè)方法,也就是在調(diào)用方法之前,需要、獲得內(nèi)置鎖,否則就會(huì)處于阻塞狀態(tài),當(dāng)然這個(gè)關(guān)鍵字也可以修飾靜態(tài)方法,如果調(diào)用靜態(tài)方法,就會(huì)鎖住整個(gè)類(lèi)
但是要提醒一下大家,同步是一種高開(kāi)銷(xiāo)的操作,所以應(yīng)該盡量減少需要同步的內(nèi)容
?
使用特殊域變量實(shí)現(xiàn)同步
volatile提供了一種免鎖的機(jī)制,使用這個(gè)關(guān)鍵字修飾的域相當(dāng)于告訴虛擬機(jī),這個(gè)域可能會(huì)被其他的線程跟新,因此每次讀取這個(gè)域的時(shí)候都需要重新計(jì)算,而不是使用寄存器中的值,這個(gè)關(guān)鍵字不會(huì)提供任何的原子操作,也不能用來(lái)修飾final類(lèi)型的變量、
?修改后代碼為:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | package?com.java; import?java.awt.GridLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; ?? import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; import?javax.swing.JScrollPane; import?javax.swing.JTextArea; ?? public?class?UnSynBank?extends?JFrame{ ????public?UnSynBank(){ ????????panel.setLayout(new?GridLayout(2,?2,?3,?3)); ????????panel.add(label1); ????????panel.add(label2); ????????JScrollPane js1 =?new?JScrollPane(oneArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js1); ????????JScrollPane js2 =?new?JScrollPane(twoArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js2); ?? ????????panel2.add(panel); ????????panel2.add(statrButton); ????????setContentPane(panel2); ?? ????????statrButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????Thread demo1 =?new?Thread(new?Transfer(bank, oneArea)); ????????????????demo1.start(); ????????????????Thread demo2 =?new?Thread(new?Transfer(bank, twoArea)); ????????????????demo2.start(); ????????????} ????????}); ?? ????????setSize(300,?400); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????} ?? ????public?static?void?main(String[] args){ ????????new?UnSynBank(); ????} ?? ????private?final?Bank bank =?new?Bank(); ????JPanel panel =?new?JPanel(); ????JPanel panel2 =?new?JPanel(); ????private?final?JButton statrButton =?new?JButton("開(kāi)始存錢(qián)"); ????private?final?JLabel label1 =?new?JLabel("一號(hào)線程"); ????private?final?JLabel label2 =?new?JLabel("二號(hào)線程"); ????private?final?JTextArea oneArea =?new?JTextArea(5,?10); ????private?final?JTextArea twoArea =?new?JTextArea(5,?10); } ?? /** ?* 表示銀行的類(lèi) ?* */ class?Bank{ ????public?Bank(){ ?? ????} ?? ????public?synchronized?void?deposit(int?money){ ????????account += money; ????} ?? ????public?int?getAccount(){ ????????return?account; ????} ?? ????private?volatile?int?account =?100; } ?? /** ?* 表示往賬戶(hù)存錢(qián) ?* */ class?Transfer?implements?Runnable{ ????public?Transfer(){ ?? ????} ?? ????public?Transfer(Bank bank, JTextArea textArea){ ????????this.bank = bank; ????????this.textArea = textArea; ????} ?? ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?20; ++i){ ????????????bank.deposit(10); ????????????String str = textArea.getText(); ????????????textArea.setText(str +?"賬戶(hù)余額為:"?+ bank.getAccount() +?"\n"); ????????} ????} ?? ????private?Bank bank =?null; ????private?JTextArea textArea =?null; } |
提醒一下:關(guān)于安全域的并發(fā)訪問(wèn):
?
多線程中的非同步問(wèn)題出現(xiàn)在對(duì)于域的讀寫(xiě)上的時(shí)候,如果讓域自身避免這個(gè)問(wèn)題的話,則不需要修改操作的方法,在java中有3中域自身就可以避免非同步問(wèn)題:final域,使用volatile域,以及有鎖保護(hù)的域。
?
?
使用重入鎖實(shí)現(xiàn)線程同步
| 1 2 | import?java.util.concurrent.locks.Lock; import?java.util.concurrent.locks.ReentrantLock; |
?
可以在程序中添加上面兩行代碼,。然后將Bank修改為:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class?Bank{ ????public?Bank(){ ????} ????public?void?deposit(int?money){ ????????lock.lock(); ????????try{ ????????????account += money; ????????}finally{ ????????????lock.unlock(); ????????} ????} ????public?int?getAccount(){ ????????return?account; ????} ????private?final?Lock lock =?new?ReentrantLock(); ????private?int?account =?100; } |
?
這樣也可以解決非同步問(wèn)題。至于這個(gè)類(lèi),大家可以自行去查看API,我只是在這里提醒一下,如果synchronized能夠滿(mǎn)足需求的話,就使用synchronized關(guān)鍵字,因?yàn)檫@個(gè)可以簡(jiǎn)化代碼,如果需要更加高級(jí)的功能的時(shí)候,就使用Lock對(duì)象,在使用ReentrantLock的時(shí)候,一定要注意及時(shí)釋放鎖,否則程序會(huì)出現(xiàn)死鎖。
?
使用線程局部變量實(shí)現(xiàn)線程同步
?
這個(gè)例子演示的是兩個(gè)線程同時(shí)修改一個(gè)變量,運(yùn)行結(jié)果:
?
?
可以發(fā)現(xiàn),每個(gè)線程完成修改之后的副本是完全獨(dú)立的,如果使用TreadLocal來(lái)管理變量,則每個(gè)使用這個(gè)變量的線程都會(huì)獲得這個(gè)變量的一個(gè)副本。,并且可以隨意修改這個(gè)副本,每個(gè)線程之間不會(huì)影響。
?
TreadLocal和同步機(jī)制都是為了解決多線程中的相同變量訪問(wèn)沖突的問(wèn)題的,前者采用的是空間還時(shí)間,后者采用的是時(shí)間換空間
?
代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class?Bank{ ????public?Bank(){ ????} ????public?void?deposit(int?money){ ????????account.set(account.get() + money); ????} ????public?int?getAccount(){ ????????return?account.get(); ????} ????private?static?ThreadLocal<Integer> account =?new?ThreadLocal<Integer>(){ ????????@Override ????????protected?Integer initialValue(){ ????????????return?100; ????????} ????}; } |
?
線程之間的通信
?
還記得我在我的筆記java IO總結(jié)中給出了一個(gè)使用管道流進(jìn)行線程之間通信的例子:
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | package?Thread; import?java.io.IOException; import?java.io.PipedInputStream; import?java.io.PipedOutputStream; /** ?* 使用管道流進(jìn)行線程之間的通信 ?* */ public?class?PipedTreadDemo{ ????public?static?void?main(String[] args){ ????????Sender send =?new?Sender(); ????????Reciver rec =?new?Reciver(); ????????try{ ????????????send.getOut().connect(rec.getReciver()); ????????}catch(Exception e){ ????????????e.printStackTrace(); ????????} ????????new?Thread(send).start(); ????????new?Thread(rec).start(); ????} } /** ?* 消息發(fā)送類(lèi) ?* */ class?Sender?implements?Runnable{ ????private?PipedOutputStream out =?null; ????public?Sender(){ ????????out =?new?PipedOutputStream(); ????} ????public?PipedOutputStream getOut(){ ????????return?this.out; ????} ????@Override ????public?void?run(){ ????????String str =?"rollen holt"; ????????try{ ????????????out.write(str.getBytes()); ????????}catch(IOException e){ ????????????e.printStackTrace(); ????????} ????????try{ ????????????out.close(); ????????}catch(IOException e){ ????????????e.printStackTrace(); ????????} ????} } /** ?* 消息接受類(lèi) ?* */ class?Reciver?implements?Runnable{ ????private?PipedInputStream input =?null; ????public?Reciver(){ ????????input =?new?PipedInputStream(); ????} ????public?PipedInputStream getReciver(){ ????????return?this.input; ????} ????@Override ????public?void?run(){ ????????byte[] bytes =?new?byte[1024]; ????????int?len =?0; ????????try{ ????????????len = input.read(bytes); ????????}catch(IOException e){ ????????????e.printStackTrace(); ????????} ????????try{ ????????????input.close(); ????????}catch(IOException e){ ????????????e.printStackTrace(); ????????} ????????System.out.println("讀取的內(nèi)容為:"?+?new?String(bytes,?0, len)); ????} } |
?
?
【運(yùn)行結(jié)果】:
?
讀取的內(nèi)容為:rollen holt
?
下面,我們?cè)谕降那疤嵯?#xff0c;在舉出一個(gè)線程通信的例子:
?
首先擺出運(yùn)行結(jié)果再說(shuō):
?
?
程序代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | package?Thread; /** ?* 線程之間的通信 ?* */ import?java.awt.GridLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; import?javax.swing.JScrollPane; import?javax.swing.JTextArea; public?class?TreadCommunicate?extends?JFrame{ ????public?TreadCommunicate(){ ????????panel.setLayout(new?GridLayout(2,?2,?3,?3)); ????????panel.add(label1); ????????panel.add(label2); ????????JScrollPane js1 =?new?JScrollPane(oneArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js1); ????????JScrollPane js2 =?new?JScrollPane(twoArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js2); ????????statrButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????Sender sender =?new?Sender(); ????????????????Thread demo1 =?new?Thread(sender); ????????????????Thread demo2 =?new?Thread(new?Receiver(sender)); ????????????????demo1.start(); ????????????????demo2.start(); ????????????} ????????}); ????????panel2.add(panel); ????????panel2.add(statrButton); ????????setContentPane(panel2); ????????setSize(300,?400); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????} ????public?static?void?main(String[] args){ ????????new?TreadCommunicate(); ????} ????/** ?????* 賣(mài)家 ?????* */ ????class?Sender?implements?Runnable{ ????????@Override ????????public?void?run(){ ????????????for(int?i =?0; i <?5; ++i){ ????????????????// 如果已經(jīng)發(fā)送,那么就等待 ????????????????while(isValid){ ????????????????????Thread.yield(); ????????????????} ????????????????product = products[i]; ????????????????String text = oneArea.getText(); ????????????????oneArea.setText(text +?"發(fā)送"?+ product +?"\n"); ????????????????try{ ????????????????????Thread.sleep(1000); ????????????????}catch(Exception e){ ????????????????????e.printStackTrace(); ????????????????} ????????????????isValid =?true; ????????????} ????????} ????????public?boolean?isisValid(){ ????????????return?this.isValid; ????????} ????????public?void?setValid(boolean?flag){ ????????????this.isValid = flag; ????????} ????????public?String getProduct(){ ????????????return?product; ????????} ????????private?volatile?String[] products = {?"《金 瓶梅》",?"《紅樓夢(mèng)》",?"《平凡的世界》", ????????????????"《流氓老師》",?"《西游記》"?}; ????????private?volatile?boolean?isValid =?false; ????????private?volatile?String product; ????}// end sender ????/** ?????* 買(mǎi)家 ?????* */ ????class?Receiver?implements?Runnable{ ????????public?Receiver(){ ????????} ????????public?Receiver(Sender sender){ ????????????this.sender = sender; ????????} ????????@Override ????????public?void?run(){ ????????????for(int?i =?0; i <?5; ++i){ ????????????????// 如果沒(méi)有發(fā)送,就等待 ????????????????while(!sender.isisValid()){ ????????????????????Thread.yield(); ????????????????} ????????????????String test = twoArea.getText(); ????????????????twoArea.setText(test +?"接受到"?+ sender.getProduct() +?"\n"); ????????????????try{ ????????????????????Thread.sleep(1000); ????????????????}catch(Exception e){ ????????????????????e.printStackTrace(); ????????????????} ????????????????sender.setValid(false); ????????????} ????????} ????????private?Sender sender; ????} ????JPanel panel =?new?JPanel(); ????JPanel panel2 =?new?JPanel(); ????private?final?JButton statrButton =?new?JButton("開(kāi)始交易"); ????private?final?JLabel label1 =?new?JLabel("賣(mài)家"); ????private?final?JLabel label2 =?new?JLabel("買(mǎi)家"); ????private?final?JTextArea oneArea =?new?JTextArea(5,?10); ????private?final?JTextArea twoArea =?new?JTextArea(5,?10); } |
?
死鎖的范例
?
下面絕對(duì)不是本人蛋疼的寫(xiě)出這個(gè)一個(gè)更加叫人蛋疼的程序。只是給出了一個(gè)例子:
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /** ?* 簡(jiǎn)單的死鎖 ?* */ public?class?DeadLockDemo?implements?Runnable{ ????@Override ????public?void?run(){ ????????// 獲得當(dāng)前線程的名字 ????????String str = Thread.currentThread().getName(); ????????System.out.println(str +?": flag= "?+ flag); ????????if(flag){ ????????????synchronized?(obj1){ ????????????????try{ ????????????????????Thread.sleep(1000); ????????????????}catch(Exception e){ ????????????????????e.printStackTrace(); ????????????????} ????????????????System.out.println(str +?"已經(jīng)進(jìn)入同步快obj1,準(zhǔn)備進(jìn)入同步快obj2"); ????????????????synchronized?(obj2){ ????????????????????System.out.println(str +?"已經(jīng)進(jìn)入同步快obj2"); ????????????????} ????????????} ????????} ????????if(!flag){ ????????????synchronized?(obj2){ ????????????????try{ ????????????????????Thread.sleep(1000); ????????????????}catch(Exception e){ ????????????????????e.printStackTrace(); ????????????????} ????????????????System.out.println(str +?"已經(jīng)進(jìn)入同步快obj2,準(zhǔn)備進(jìn)入同步快obj1"); ????????????????synchronized?(obj1){ ????????????????????System.out.println(str +?"已經(jīng)進(jìn)入同步快obj1"); ????????????????} ????????????} ????????} ????} ????public?static?void?main(String[] args){ ????????DeadLockDemo demo1 =?new?DeadLockDemo(); ????????DeadLockDemo demo2 =?new?DeadLockDemo(); ????????demo1.flag =?true; ????????demo2.flag =?false; ????????new?Thread(demo1).start(); ????????new?Thread(demo2).start(); ????} ????private?boolean?flag; ????private?final?Object obj1 =?new?Object(); ????private?final?Object obj2 =?new?Object(); } |
?
?
【運(yùn)行結(jié)果】
?
(我承認(rèn)我今天RP爆發(fā),我運(yùn)行了10次,還是沒(méi)出現(xiàn)死鎖那種情況,但是這個(gè)程序確實(shí)可以產(chǎn)生死鎖的,哪位運(yùn)行這個(gè)程序,要是產(chǎn)生了死鎖,麻煩說(shuō)一下,謝謝)
?
?
?
使用線程池優(yōu)化多線程編程
?
這個(gè)例子使用的是Executors類(lèi),讀者自行查看API,因?yàn)橐庹f(shuō)的話,就太多了。
?
下面這個(gè)例子給出了使用線程池和不使用線程池的情況下的效率的問(wèn)題。
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | package?Thread; import?java.util.concurrent.ExecutorService; import?java.util.concurrent.Executors; /** ?* 使用線程池優(yōu)化多線程編程 ?* */ public?class?ThreadPoolDemo{ ????public?static?void?main(String[] args){ ????????Runtime run = Runtime.getRuntime(); ????????// 為了減少誤差 ????????run.gc(); ????????long?currentTime = System.currentTimeMillis(); ????????long?freemonery = run.freeMemory(); ????????for(int?i =?0; i <?10000; ++i){ ????????????new?Thread(new?Temo()).start(); ????????} ????????System.out.println("獨(dú)立運(yùn)行10000個(gè)線程占用內(nèi)存為:" ????????????????+ (freemonery - run.freeMemory())); ????????System.out.println("獨(dú)立運(yùn)行10000個(gè)線程占用時(shí)間為:" ????????????????+ (System.currentTimeMillis() - currentTime)); ????????// 下面使用線程池來(lái)試試 ????????run.gc(); ????????freemonery = run.freeMemory(); ????????currentTime = System.currentTimeMillis(); ????????ExecutorService executorService = Executors.newFixedThreadPool(3); ????????for(int?i =?0; i <?10000; ++i){ ????????????executorService.submit(new?Temo()); ????????} ????????System.out.println("使用線程池運(yùn)行10000個(gè)線程占用內(nèi)存為:" ????????????????+ (freemonery - run.freeMemory())); ????????System.out.println("使用線程池運(yùn)行10000個(gè)線程占用時(shí)間為:" ????????????????+ (System.currentTimeMillis() - currentTime)); ????} } class?Temo?implements?Runnable{ ????@Override ????public?void?run(){ ????????count++; ????} ????private?int?count =?0; } |
?
?
【運(yùn)行結(jié)果】:
?
獨(dú)立運(yùn)行10000個(gè)線程占用內(nèi)存為:3490440
?
獨(dú)立運(yùn)行10000個(gè)線程占用時(shí)間為:1808
?
使用線程池運(yùn)行10000個(gè)線程占用內(nèi)存為:1237424
?
使用線程池運(yùn)行10000個(gè)線程占用時(shí)間為:62
?
關(guān)于哲學(xué)家就餐的問(wèn)題
?
由于代碼比較長(zhǎng),所以單獨(dú)列出為一篇文章
?
地址:http://www.cnblogs.com/rollenholt/archive/2011/09/15/2178004.html
?
使用信號(hào)量實(shí)現(xiàn)線程同步
?
現(xiàn)在我們繼續(xù)回答之前銀行存款的問(wèn)題,相信大家還沒(méi)有忘記,哈哈,真是不好意思,本來(lái)線程同步這一塊應(yīng)該整理在一起的。
?
一個(gè)信號(hào)量有3中操作,而且他們?nèi)慷际窃拥?#xff0c;初始化,增加,減少。增加可以為一個(gè)進(jìn)程解除阻塞,減少可以為一個(gè)進(jìn)程進(jìn)入阻塞。
?
Semaphore類(lèi)是一個(gè)技術(shù)信號(hào)量,從概念上信號(hào)量維持了一個(gè)許可集。
?
現(xiàn)在我們繼續(xù)看上面的銀行存款問(wèn)題:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | package?Thread; import?java.awt.GridLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; import?java.util.concurrent.Semaphore; import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; import?javax.swing.JScrollPane; import?javax.swing.JTextArea; public?class?synDemo?extends?JFrame{ ????public?synDemo(){ ????????panel.setLayout(new?GridLayout(2,?2,?3,?3)); ????????panel.add(label1); ????????panel.add(label2); ????????JScrollPane js1 =?new?JScrollPane(oneArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js1); ????????JScrollPane js2 =?new?JScrollPane(twoArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js2); ????????panel2.add(panel); ????????panel2.add(statrButton); ????????setContentPane(panel2); ????????statrButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????Thread demo1 =?new?Thread(new?Transfer1(bank, oneArea, ????????????????????????semaphore)); ????????????????demo1.start(); ????????????????Thread demo2 =?new?Thread(new?Transfer1(bank, twoArea, ????????????????????????semaphore)); ????????????????demo2.start(); ????????????} ????????}); ????????setSize(300,?400); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????} ????public?static?void?main(String[] args){ ????????new?synDemo(); ????} ????Semaphore semaphore =?new?Semaphore(1,?true); ????private?final?Bank1 bank =?new?Bank1(); ????JPanel panel =?new?JPanel(); ????JPanel panel2 =?new?JPanel(); ????private?final?JButton statrButton =?new?JButton("開(kāi)始存錢(qián)"); ????private?final?JLabel label1 =?new?JLabel("一號(hào)線程"); ????private?final?JLabel label2 =?new?JLabel("二號(hào)線程"); ????private?final?JTextArea oneArea =?new?JTextArea(5,?10); ????private?final?JTextArea twoArea =?new?JTextArea(5,?10); } /** ?* 表示銀行的類(lèi) ?* */ class?Bank1{ ????public?Bank1(){ ????} ????public?void?deposit(int?money){ ????????account += money; ????} ????public?int?getAccount(){ ????????return?account; ????} ????private?int?account; } /** ?* 表示往賬戶(hù)存錢(qián) ?* */ class?Transfer1?implements?Runnable{ ????public?Transfer1(){ ????} ????public?Transfer1(Bank1 bank, JTextArea textArea, Semaphore semaphore){ ????????this.bank = bank; ????????this.textArea = textArea; ????????this.semaphore = semaphore; ????} ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?20; ++i){ ????????????// 獲得許可 ????????????try{ ????????????????semaphore.acquire(); ????????????}catch(InterruptedException e){ ????????????????e.printStackTrace(); ????????????} ????????????bank.deposit(10); ????????????String str = textArea.getText(); ????????????textArea.setText(str +?"賬戶(hù)余額為:"?+ bank.getAccount() +?"\n"); ????????????// 釋放許可 ????????????semaphore.release(); ????????} ????} ????// 注意 ????private?Semaphore semaphore; ????private?Bank1 bank =?null; ????private?JTextArea textArea =?null; } |
運(yùn)行結(jié)果:
?
?
使用原子變量實(shí)現(xiàn)線程同步
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | package?Thread; import?java.awt.GridLayout; import?java.awt.event.ActionEvent; import?java.awt.event.ActionListener; import?java.util.concurrent.atomic.AtomicInteger; import?javax.swing.JButton; import?javax.swing.JFrame; import?javax.swing.JLabel; import?javax.swing.JPanel; import?javax.swing.JScrollPane; import?javax.swing.JTextArea; public?class?synDemo?extends?JFrame{ ????public?synDemo(){ ????????panel.setLayout(new?GridLayout(2,?2,?3,?3)); ????????panel.add(label1); ????????panel.add(label2); ????????JScrollPane js1 =?new?JScrollPane(oneArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js1); ????????JScrollPane js2 =?new?JScrollPane(twoArea, ????????????????JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, ????????????????JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); ????????panel.add(js2); ????????panel2.add(panel); ????????panel2.add(statrButton); ????????setContentPane(panel2); ????????statrButton.addActionListener(new?ActionListener(){ ????????????@Override ????????????public?void?actionPerformed(ActionEvent e){ ????????????????Thread demo1 =?new?Thread(new?Transfer1(bank, oneArea)); ????????????????demo1.start(); ????????????????Thread demo2 =?new?Thread(new?Transfer1(bank, twoArea)); ????????????????demo2.start(); ????????????} ????????}); ????????setSize(300,?400); ????????setVisible(true); ????????setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ????} ????public?static?void?main(String[] args){ ????????new?synDemo(); ????} ????private?final?Bank1 bank =?new?Bank1(); ????JPanel panel =?new?JPanel(); ????JPanel panel2 =?new?JPanel(); ????private?final?JButton statrButton =?new?JButton("開(kāi)始存錢(qián)"); ????private?final?JLabel label1 =?new?JLabel("一號(hào)線程"); ????private?final?JLabel label2 =?new?JLabel("二號(hào)線程"); ????private?final?JTextArea oneArea =?new?JTextArea(5,?10); ????private?final?JTextArea twoArea =?new?JTextArea(5,?10); } /** ?* 表示銀行的類(lèi) ?* */ class?Bank1{ ????public?Bank1(){ ????} ????public?void?deposit(int?money){ ????????account.addAndGet(money); ????} ????public?int?getAccount(){ ????????return?account.get(); ????} ????//注意 ????private?final?AtomicInteger account =?new?AtomicInteger(100); } /** ?* 表示往賬戶(hù)存錢(qián) ?* */ class?Transfer1?implements?Runnable{ ????public?Transfer1(){ ????} ????public?Transfer1(Bank1 bank, JTextArea textArea){ ????????this.bank = bank; ????????this.textArea = textArea; ????} ????@Override ????public?void?run(){ ????????for(int?i =?0; i <?20; ++i){ ????????????bank.deposit(10); ????????????String str = textArea.getText(); ????????????textArea.setText(str +?"賬戶(hù)余額為:"?+ bank.getAccount() +?"\n"); ????????} ????} ????private?Bank1 bank =?null; ????private?JTextArea textArea =?null; } |
【運(yùn)行結(jié)果】:
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的java多线程总结(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: WorkerMan 入门学习之(二)基础
- 下一篇: sass使用相关报错