本文轉載自:https://blog.csdn.net/qq_41965731/article/details/88679710#commentBox
目錄
程序、進程、線程:
在操作系統中運行的程序就是進程,一個進程可以有多個線程
線程和進程的區別:
值得注意:我們所接觸的多線程是模擬出來的,真正的多線程是在多個CPU中進行,即多核,如服務器,模擬出來的多線程是在一個CPU下進行,在CPU中同一時間只能執行之中代碼,因爲執行的快,所以就會用同時執行的感覺
線程的核心概念:
- 線程是獨立的執行片段
- 在程序運行時,即便是你沒有創建線程,後臺也會有多個線程存在,如gc線程,主線程(main)
- main()稱爲主線程,用來執行整個程序
- 如果在一個進程中創建多個線程,線程的運行是由調度器安排的,調度器與操作系統緊密相關,所以線程的順序不是認爲能干預的。
- 對用一份資源使用時,會發生資源搶奪的問題,需要加入併發控制
- 線程會帶來額外的開銷,如:cup調度時間,併發控制的開銷
- 每個線程在自己工作內存交互,加載和存儲主內存控制不當會造成數據不一致
創建線程
- 繼承Thread類(現實的Runable接口)
- 實現Runnable接口 優先使用Runnable,因爲在JAVA中,是單繼承,而避免單繼承的侷限性,還方便我們共享資源
- 實現Callable
package Thread;
public class Test1 {
public static void main(String[] args) {
Test1Test test = new Test1Test();
test.start(); //不是馬上執行,而是交給CPU,即我準備好了,啥時候用看你
Test1Test1 test1 = new Test1Test1(); 、//實現類對象
//實現Runable接口必須藉助Thread對象, Thread稱爲代理對象
new Thread(test1).start();
for(int i=0;i<5;i++){
System.out.println("我在看電影");
}
}
}
/**
* 繼承Thread類來實現多線程
* 1.創建 2 啓動
*/
class Test1Test extends Thread{
@Override
public void run() {
for(int i =0;i<5;i++){
System.out.println("我在聽歌");
}
}
}
/**
* 實現Runable接口來實現多線程
*/
class Test1Test1 implements Runnable{
@Override
public void run() {
for(int i = 0; i<5;i++){
System.out.println("我在玩遊戲");
}
}
}
注:因爲每次CPU處理的不用,所以每次的執行結界不一定相等
例子:用多線程下載圖片
-
package Thread; /** * 用繼承Thread類的開啓多線程下載圖片 */ import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; public class Download { public static void main(String[] args) { new DowmThread("http://spider.nosdn.127.net/fdcfb76b8e77a120810190438002a34d.jpeg","src/Thread/艾薇兒1.gif").start(); new DowmThread("http://p4.yokacdn.com/pic/people/spotlight/2013/U454P41T8D265160F430DT20131029114943_maxw808.jpg","src/Thread/艾薇兒2.gif").start(); new DowmThread("http://p0.ifengimg.com/pmop/2018/0920/0AAB0969EAEB2FF4FB2AF960F6BA82695BC0C00B_size214_w1024_h768.jpeg","src/Thread/艾薇兒3.gif").start(); } } class DowmThread extends Thread{ private String url; String name; public DowmThread(String url, String name) { this.url = url; this.name = name; } @Override public void run() { Down.dowwPicture(url,name); System.out.println(name); } } class Down{ public static void dowwPicture(String url, String name){ try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); } } }
-
src/Thread/艾薇兒1.gif
-
src/Thread/艾薇兒3.gif
-
src/Thread/艾薇兒2.gif
package Thread;
/**
* 利用Runnable接口實現多窗口買票
* Runnble的優點:方便共享資源
*/
public class ticket {
public static void main(String[] args) {
//一份資源
TicketTest tt = new TicketTest();
//多個代理
new Thread(tt,"碼畜").start();
new Thread(tt,"碼奴").start();
new Thread(tt,"碼農").start();
}
}
class TicketTest implements Runnable{
int ticketcount = 99;
public void run() {
while (true){
if(ticketcount>0){
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"===>"+ticketcount--);
}
else break;
}
}
}
代理對象
例如婚慶公司,雖然主體還是以結婚雙方,但婚慶公司參與了協助工作,即結婚雙方爲真實對象,而婚慶公司爲代理對象
package Thread;
/**
* 靜態代理
* 1.真實對象
* 2.代理對象
* 倆個對象實現同一個接口
*/
public class StaticPorxy {
public static void main(String[] args) {
new MarryCompony(new Couple()).happyMarry();
/**
* 同實現Runnable接口的開啓方法是相一致的
* new Thread(線程對象).start();
*/
}
}
interface Marry{
void happyMarry();
}
//真實對象
class Couple implements Marry{
@Override
public void happyMarry() {
System.out.println("我們結婚啦");
}
}
//代理對象
class MarryCompony implements Marry{
//真實對象
Couple couple;
public MarryCompony(Couple couple) {
this.couple = couple;
}
@Override
public void happyMarry() {
ready();
couple.happyMarry();
after();
}
private void ready(){
System.out.println("準備工作......");
}
private void after(){
System.out.println("我們收錢啦......");
}
}
-
準備工作......
-
我們結婚啦
-
我們收錢啦......
lambda簡化線程
package Thread;
public class LambdaThreadTest {
//靜態內部類
static class things implements Runnable{
@Override
public void run() {
System.out.println("我在聽歌..........");
}
}
public static void main(String[] args) {
//靜態內部類
new Thread(new things()).start();
//局部內部類
class things implements Runnable{
@Override
public void run() {
System.out.println("我在聽歌.......");
}
}
new Thread(new things()).start();
//匿名類(需要藉助接口)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我在聽歌....");
}
}).start();
//Lamdba表達式(JDK8)
new Thread(()-> {
{
System.out.println("我在聽歌.");
}
}).start();
//在簡化
new Thread( ()-> System.out.println("我還在聽歌") ).start();
}
}
-
我在聽歌..........
-
我在聽歌.......
-
我在聽歌....
-
我在聽歌.
-
我還在聽歌
線程狀態
線程方法
- sleep()
- 使線程停止運行一段時間,將處於阻塞狀態
- 如果調用了sleep方法之後,沒有其他等待執行的線程,這個時候當前線程不會馬上恢復執行
- jion()
- 阻塞指定線程等到另一個線程完成以後再繼續執行
- yield()
- 讓當前正在執行線程暫停,不是阻塞線程,而是將線程轉入就緒狀態;
- 調用了yield方法之後,如果沒有其他等待執行的線程,此時當前線程就會馬上恢復執行
- setDeamon()
- 可以將指定的線程設置成後臺線程,守護線程;
- 創建用戶線程的線程結束時,後臺線程也隨之消亡;
- 只能在線程啓動之前把它設爲後臺線程
- setPriority(int newPriority) getPriority()
- 線程的優先級代表的是概率
- 範圍從1到10,默認爲5
- stop()停止線程
- 不推薦使用
線程終止:
- 不使用JDK提供的stop()/destroy()方法(它們本身也被JDK廢棄了)。
- 提供一個boolean型的終止變量,當這個變量置爲false,則終止線程的運行。
- 線程正常結束完畢——>次數
package Thread;
/**
* 終止線程:
* 1.程序正常執行完畢
* 2.外部干涉 ==》 加入標識
*/
public class StopThread {
public static void main(String[] args) {
StopThreadTest st = new StopThreadTest("納爾");
new Thread(st).start();
for (int i = 0; i < 50; i++) {
if (i == 49)
st.stop();
System.out.println("我是=====>提莫");
}
}
}
class StopThreadTest implements Runnable {
private String name;
//標識
private boolean flag = true;
public StopThreadTest(String name) {
this.name = name;
}
@Override
public void run() {
while (flag) {
System.out.println("我是=====>" + name);
}
}
public void stop() {
flag = false;
}
}
線程暫停——Sleep
- sleep阻塞當前線程的毫秒數
- sleep存在InterruptedException異常
- sleep時間到達後,線程進入就緒狀態
- sleep可以模擬網絡延時,倒計時等。(在上面賣票就用的模擬網絡延時
- 每個對象都有一個鎖,sleep不會釋放鎖
Yield——禮讓
- 禮讓線程是讓當前的正在執行線程暫停
- 不是阻塞線程,而是將線程從運行狀態轉到就緒狀態
- 讓CPU調度器,重新調度
package Thread;
public class Yield {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 50; i++){
System.out.println("我是喬峯——"+i);
}
}).start();
for(int i = 0; i <50; i++){
if(i%10 == 0)
Thread.yield();
System.out.println("我是虛竹——"+i);
}
}
}
注:因爲禮讓線程是讓線程從運行狀態轉到就緒狀態,即重新繼續和其他線程搶奪CPU的使用權,具體翻誰牌子還是得看CPU
Join——插隊
join合併線程,待此線程執 行完成後,再執行其他線 程,其他線程阻塞
package Thread;
public class Jion {
public static void main(String[] args) {
System.out.println("爸爸讓兒子買菸的故事...");
new Thread(new JionFather()).start();
}
}
class JionFather implements Runnable {
@Override
public void run() {
System.out.println("想抽菸,沒煙了,給兒子錢,讓兒子去買");
Thread thread = new Thread(new JionSon());
thread.start();
System.out.println("等待兒子買菸回來....");
try {
thread.join(); //爸爸這個線程就被阻塞了
System.out.println("煙拿回來了,抽上煙的爸爸把兒子胖揍了一頓");
} catch (InterruptedException e) {
System.out.println("兒子走丟了");
}
}
}
class JionSon implements Runnable{
@Override
public void run() {
System.out.println("兒子拿錢去買菸了");
System.out.println("然而先去了網吧");
for(int i = 0; i < 5; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i+1+"小時過去了.....");
}
System.out.println("趕緊區買菸,回家");
}
}
-
爸爸讓兒子買菸的故事...
-
想抽菸,沒煙了,給兒子錢,讓兒子去買
-
等待兒子買菸回來....
-
兒子拿錢去買菸了
-
然而先去了網吧
-
1小時過去了.....
-
2小時過去了.....
-
3小時過去了.....
-
4小時過去了.....
-
5小時過去了.....
-
趕緊區買菸,回家
-
煙拿回來了,抽上煙的爸爸把兒子胖揍了一頓
線程的狀態
- NEW
- 尚未啓動的線程的線程狀態。
- RUNNABLE (就緒狀態和運行狀態)
- 一個可運行的線程的線程狀態。在運行狀態的線程在java虛擬機執行,但它可能在等待其他資源,如處理器的操作系統。
- BLOCKED
- 線程阻塞等待監視器鎖的線程狀態。在阻塞狀態的線程等待監控鎖進入一個同步塊/方法或進入一個同步塊/方法調用後
Object.wait
。
- 線程阻塞等待監視器鎖的線程狀態。在阻塞狀態的線程等待監控鎖進入一個同步塊/方法或進入一個同步塊/方法調用後
- WAITING
- 等待線程的線程狀態。一個線程處於等待狀態由於調用以下方法之一:
Object.wait
沒有超時Thread.join
沒有超時LockSupport.park
- 等待線程的線程狀態。一個線程處於等待狀態由於調用以下方法之一:
- TIMED_WAITING
- 具有指定等待時間的等待線程的線程狀態。一個線程在等待狀態的時間由於調用下面的方法用指定的正等待時間:
- TERMINATED
- 終止線程的線程狀態。線程已完成執行。
package Thread;
public class ThreadState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(300);
System.out.println("...........");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(thread.getState()); //NEW
thread.start();
System.out.println(thread.getState()); //RUNNABLE
while (!thread.getState().equals(Thread.State.TERMINATED)){
try {
Thread.sleep(200);
System.out.println(thread.getState()); //TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(thread.getState()); //TERMINATED
}
}
優先級(Priority)
Java提供一個線程調度器來監控程序中啓動後進入就緒狀態的所有線程。線程調 度器按照線程的優先級決定應調度哪個線程來執行。 線程的優先級用數字表示,範圍從1到10
• Thread.MIN_PRIORITY = 1
• Thread.MAX_PRIORITY = 10
• Thread.NORM_PRIORITY = 5
使用下述方法獲得或設置線程對象的優先級。
- int getPriority()
- void setPriority(int newPriority);
package Thread;
public class ThreadPriority {
public static void main(String[] args) {
Thread thread1 = new Thread(new ThreadPriorityTest(),"碼畜");
Thread thread2 = new Thread(new ThreadPriorityTest(),"碼奴");
Thread thread3 = new Thread(new ThreadPriorityTest(),"碼農");
Thread thread4 = new Thread(new ThreadPriorityTest(),"IT民工");
Thread thread5 = new Thread(new ThreadPriorityTest(),"IT工程師");
Thread thread6 = new Thread(new ThreadPriorityTest(),"IT人才");
//設置優先級
thread1.setPriority(Thread.MIN_PRIORITY); //1
thread2.setPriority(Thread.MIN_PRIORITY); //1
thread3.setPriority(Thread.MIN_PRIORITY); //1
thread4.setPriority(Thread.MAX_PRIORITY); //10
thread5.setPriority(Thread.MAX_PRIORITY); //10
thread6.setPriority(Thread.MAX_PRIORITY); //10
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
}
}
class ThreadPriorityTest implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getPriority()+"======>"+Thread.currentThread().getName());
}
}
優先級的設定建議在start()調用前
注意:優先級低只是意味着獲得調度的概率低。並不是絕對先調用優先級高後調 用優先級低的線程。
守護線程(Deamon)
- 線程分爲用戶線程和守護線程
- 虛擬機必須要確保用戶線程執行完步
- 虛擬機不需要等待守護線程執行完畢
- 如後臺操作日誌、監控內存使用
package Thread;
public class ThreadDeamon {
public static void main(String[] args) {
ThreadDeamonMan tdm = new ThreadDeamonMan();
Thread thread1 = new Thread(tdm);
thread1.start();
}
}
class ThreadDeamonMan implements Runnable {
private int i=0;
@Override
public void run() {
while (true){
i++;
System.out.println(i);
}
}
}
多線程_併發_不同步三大經典案例
1.賣票
package Thread;
/**
*
*/
public class ticket {
public static void main(String[] args) {
//一份資源
TicketTest tt = new TicketTest();
//多個代理
new Thread(tt,"碼畜").start();
new Thread(tt,"碼奴").start();
new Thread(tt,"碼農").start();
}
}
class TicketTest implements Runnable{
int ticketcount = 10;
boolean flag =true;
public void run() {
while (flag){
vest();
}
}
public void vest(){
if(ticketcount<=0) {
flag = false;
return;
}else {
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"===>"+ticketcount--);
}
}
}
2。取錢
package Thread;
/**
* 線程不安全:取錢
*/
public class ThreadGetMoney {
public static void main(String[] args) {
Bank bank = new Bank("喜酒錢",100);
Thread you = new Thread(new ThreadGetMoneyTest(bank,80));
Thread he = new Thread(new ThreadGetMoneyTest(bank,70));
you.start();
he.start();
}
}
class Bank{
String name;
double money;
public Bank(String name, double money) {
this.name = name;
this.money = money;
}
}
class ThreadGetMoneyTest implements Runnable{
Bank bank;
double needMoney; //要取的錢
double pocketMoney; //口袋裏的錢
public ThreadGetMoneyTest(Bank bank, double needMoney) {
this.bank = bank;
this.needMoney = needMoney;
}
@Override
public void run() {
if(bank.money - needMoney < 0.0){
return;
}
try {
Thread.sleep(1000); //取錢所用的時間
bank.money -= needMoney;
pocketMoney += needMoney;
System.out.println(Thread.currentThread().getName()+"取的錢數"+needMoney);
System.out.println(Thread.currentThread().getName()+"口袋裏的錢"+pocketMoney);
System.out.println("賬戶裏的錢"+bank.money);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.操作容器
package Thread;
/**
* 線程不安全,操作容器(會存在數組的覆蓋)
*/
import java.util.ArrayList;
import java.util.List;
public class ThreadArray {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for(int i = 0; i < 10000; i++){
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
System.out.println(list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
線程同步
併發:同一個對象被多個線程用時操作
現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題。 比 如:派發禮品,多個人都想獲得。天然的解決辦法就是,在禮品前,大 家排隊。前一人領取完後,後一人再領取。處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改 這個對象。 這時候,我們就需要用到“線程同步”。 線程同步其實就 是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程再使用。
由於同一進程的多個線程共享同一塊存儲空間。爲了保證數據在方法中被訪問時的正確性,在訪問 時加入鎖機制(synchronized),當一個線程獲得對象的排它鎖,獨佔資源, 其他線程必須等待,使用後釋放鎖即可。存在以下問題:
- 一個線程持有鎖會導致其它所有需要此鎖的線程掛起
- 在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時, 引起性能問題;
- 如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級 倒置,引起性能問題
中間有一部分內容,電腦死機了,沒了,哎。先留着吧
synchronized關鍵字
- synchronized 方法
- synchronized 塊
¥¥同步方法
public synchronized void method(int args) {}
synchronized 方法控制對“成員變量|類變量”對象的訪問:每個 對象對應一把鎖,每個 synchronized 方法都必須獲得調用該方法 的對象的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨佔 該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能獲 得該鎖,重新進入可執行狀態。
缺陷:若將一個大的方法聲明爲synchronized 將會大大影響效率。
eg.以前面三大經典中的賣票爲例
package Thread;
/**
* 同步方法
*/
public class ticket {
public static void main(String[] args) {
//一份資源
TicketTest tt = new TicketTest();
//多個代理
new Thread(tt,"碼畜").start();
new Thread(tt,"碼奴").start();
new Thread(tt,"碼農").start();
}
}
class TicketTest implements Runnable{
int ticketcount = 10;
boolean flag =true;
public void run() {
while (flag){
vest();
}
}
//同步方法
public synchronized void vest(){
if(ticketcount<=0) {
flag = false;
return;
}else {
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"===>"+ticketcount--);
}
}
}
¥¥ 同步塊:
- synchronized (obj){ }, obj稱之爲同步監視器
- obj可以是任何對象,但是推薦使用共享資源作爲同步監視器
- 同步方法中無需指定同步監視器,因爲同步方法的同步監 視器是this即該對象本身,或class即類的模子
- 同步監視器的執行過程
- 第一個線程訪問,鎖定同步監視器,執行其中代碼
- 第二個線程訪問,發現同步監視器被鎖定,無法訪問
- 第一個線程訪問完畢,解鎖同步監視器
- 第二個線程訪問,發現同步監視器未鎖,鎖定並訪問
eg.以前面三大經典中的操作容器爲例
package Thread;
/**
*
* 使用同步塊實現線程安全
*/
import java.util.ArrayList;
import java.util.List;
public class ThreadArray {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for(int i = 0; i < 10000; i++){
new Thread(() -> {
//同步塊
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(1000);
System.out.println(list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
eg.以前面三大經典中的取錢爲例
package Thread;
public class ThreadGetMoney {
public static void main(String[] args) {
Bank bank = new Bank("喜酒錢",100);
Thread you = new Thread(new ThreadGetMoneyTest(bank,80));
Thread he = new Thread(new ThreadGetMoneyTest(bank,70));
you.start();
he.start();
}
}
class Bank{
String name;
double money;
public Bank(String name, double money) {
this.name = name;
this.money = money;
}
}
class ThreadGetMoneyTest implements Runnable{
Bank bank;
double needMoney; //要取的錢
double pocketMoney; //口袋裏的錢
public ThreadGetMoneyTest(Bank bank, double needMoney) {
this.bank = bank;
this.needMoney = needMoney;
}
@Override
public synchronized void run() {
test();
}
//目標應該鎖定bank
public void test(){
//提高性能
if(bank.money <= 0){
return;
}
synchronized (bank) {
if (bank.money - needMoney < 0.0) {
return;
}
try {
Thread.sleep(1000); //取錢所用的時間
bank.money -= needMoney;
pocketMoney += needMoney;
System.out.println(Thread.currentThread().getName() + "取的錢數" + needMoney);
System.out.println(Thread.currentThread().getName() + "口袋裏的錢" + pocketMoney);
System.out.println("賬戶裏的錢" + bank.money);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
性能分析
package Thread;
/**
*
*/
public class ticket {
public static void main(String[] args) {
//一份資源
TicketTest tt = new TicketTest();
//多個代理
new Thread(tt,"碼畜").start();
new Thread(tt,"碼奴").start();
new Thread(tt,"碼農").start();
}
}
class TicketTest implements Runnable{
int ticketcount = 10;
boolean flag =true;
public void run() {
while (flag){
vest5();
}
}
//儘可能鎖定合理的範圍(不是指代碼,指的是代碼的完整性)
public void vest5(){
if (ticketcount <= 0) { //考慮到的是沒有票的情況(沒有票的情況很多)
flag = false;
return; //這被稱作雙重檢測
}
synchronized(this) {
if (ticketcount <= 0) { //考慮最後一張票
flag = false;
return;
}
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>" + ticketcount--);
}
}
//線程不安全,範圍太小鎖不住
public void vest4(){
synchronized(this) {
if (ticketcount <= 0) {
flag = false;
return;
}
}
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>" + ticketcount--);
}
//線程不安全 ticketcount對象在變
//synchronized是要鎖一個不變的對象
public void vest3(){
synchronized((Integer)ticketcount) {
if (ticketcount <= 0) {
flag = false;
return;
} else {
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>" + ticketcount--);
}
}
}
//同步塊 範圍太大——》性能底下
public void vest2(){
synchronized(this) {
if (ticketcount <= 0) {
flag = false;
return;
} else {
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>" + ticketcount--);
}
}
}
//線程安全:同步方法
public synchronized void vest1(){
if(ticketcount<=0) {
flag = false;
return;
}else {
try {
Thread.sleep(100); //模擬現實網絡延遲,那麼結果就會出現問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"===>"+ticketcount--);
}
}
}
線程協作(cooperation)
應用場景:生產者和消費者問題
- 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入 倉庫,消費者將倉庫中產品取走消費
- 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生 產並等待,直到倉庫中的產品被消費者取走爲止
- 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停 止消費並等待,直到倉庫中再次放入產品爲止。
分析:這是一個線程同步問題,生產者和消費者共享同一個資源,並且生產者和消 費者之間相互依賴,互爲條件
- 對於生產者,沒有生產產品之前,要通知消費者等待。而生產了產品之後, 又需要馬上通知消費者消費
- 對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新 產品以供消費
- 在生產者消費者問題中,僅有synchronized是不夠的
- synchronized可阻止併發更新同一個共享資源,實現了同步
- synchronized不能用來實現不同線程之間的消息傳遞(通信)
解決方法
- 管程法
- 生產者:負責生產數據的模塊(這裏模塊可能是:方法、對象、線程、進程);
- 消費者:負責處理數據的模塊(這裏模塊可能是:方法、對象、線程、進程);
- 緩衝區:消費者不能直接使用生產者的數據,它們之間有個“緩衝區”; 生產者將生產好的數據放入“緩衝區”,消費者從“緩衝區”拿要處理的數據
- 信號燈法(標記法)
package Thread.day2;
import javax.swing.plaf.metal.MetalBorders;
/**
* 協作模型:生產者和消費者——管程法
* 藉助數組(管道)
*/
//模擬生產者、消費者、和包子之間的故事
public class test0 {
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Thread(new Producer(buffer)).start();
new Thread(new Customer(buffer)).start();
}
}
//生產者
class Producer implements Runnable {
SteamedBun steamedBun;
Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 5 == 0) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產者正在生產第===" + i + "個包子");
buffer.push(new SteamedBun(i));
}
}
}
//消費者
class Customer implements Runnable {
Buffer buffer;
public Customer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("消費者買了第===" + buffer.get().getId() + "個包子");
}
}
}
//緩存區,也就是控制我生產的量
class Buffer {
int count = 0; //計數器
SteamedBun[] buffer = new SteamedBun[11];
//生產饅頭
public synchronized void push(SteamedBun steamedBun) {
if (count == 10) {
try {
//容器已滿,等候消費者處理
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer[count++] = steamedBun;
//容器裏又加入有饅頭,提醒消費者前來處理
this.notifyAll();
}
//獲取饅頭
public synchronized SteamedBun get() {
if (count == 0) {
try {
//容器裏沒有饅頭,只得等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//容器裏的饅頭沒有滿,喚醒生產者繼續生產
this.notifyAll();
return buffer[--count];
}
}
//饅頭
class SteamedBun {
private int id;
public SteamedBun(int id) {
this.id = id;
}
public int getId() {
return id;
}
public SteamedBun() {
}
}
下面這個例子不僅描述了信號燈法,而且告訴我們,搞好一件事情的重要性
-
package Thread.day2; /** * 生產者與消費者模型——信號燈法 * 設置標誌位 */ public class test { public static void main(String[] args) { Kungfu kungfu = new Kungfu(); new Thread(new Attack(kungfu)).start(); new Thread(new Defense(kungfu)).start(); } } //進攻者 class Attack implements Runnable{ Kungfu kungfu; private String[] kungfus = {"打狗棒","葵花點穴手","降龍十八掌","黯然銷魂掌","玉女心經"}; public Attack(Kungfu kungfu) { this.kungfu = kungfu; } @Override public void run() { for(int i =0; i < 10; i++){ kungfu.attack(kungfus[(int)(Math.floor(Math.random()*kungfus.length))]); } } } //防守者 class Defense implements Runnable{ Kungfu kungfu; public Defense(Kungfu kungfu) { this.kungfu = kungfu; } @Override public void run() { for(int i =0; i < 10; i++){ kungfu.defense(); } } } //武功 class Kungfu { //招式 String sonicSlash; boolean flag = true; public synchronized void attack(String sonicSlash){ if(!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("進攻者使用出了招式===》"+sonicSlash); //喚醒防守者防守 this.notifyAll(); flag = !flag; } public synchronized void defense(){ if(flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("防守者使出了龜殼神功,並大笑到,誰能傷我"); //喚醒防守者防守 this.notifyAll(); flag = !flag; } }
-
進攻者使用出了招式===》黯然銷魂掌
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》葵花點穴手
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》打狗棒
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》黯然銷魂掌
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》打狗棒
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》葵花點穴手
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》降龍十八掌
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》降龍十八掌
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》黯然銷魂掌
-
防守者使出了龜殼神功,並大笑到,誰能傷我
-
進攻者使用出了招式===》打狗棒
-
防守者使出了龜殼神功,並大笑到,誰能傷我
Java提供了三種解決了線程間信息通信的問題
高級主題
1.任務定時調度
package Thread.day2;
import java.util.Date;
import java.util.TimerTask;
import java.util.Timer;
public class TimerTest {
public static void main(String[] args) {
Timer time = new Timer();
// time.schedule(new Work(),3000); 3秒後執行一次
// time.schedule(new Work(),2000,1000); 重複執行
time.schedule(new Work(),new Date(2050,5,20));
}
}
class Work extends TimerTask {
@Override
public void run() {
for(int i = 0; i < 3; i++){
System.out.println("我愛學習,無法自拔,玩遊戲沒意思");
}
System.out.println("真香");
}
}
2.quartz的使用
3.HappenBefore(指令重排)
你寫的代碼很可能根本沒按你期望的順序執行,因爲編譯器和 CPU 會嘗 試重排指令使得代碼更快地運行。
執行代碼的順序可能與編寫代碼不一致,即虛擬機優化代碼順序,則爲指令重排 happen-before 即:編譯器或運行時環境爲了優化程序性能而採取的對指令進行重新 排序執行的一種手段
- 在虛擬機層面,爲了儘可能減少內存操作速度遠慢於CPU運行速度所帶來的CPU空置的影 響,虛擬機會按照自己的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在 後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘可能充分 地利用CPU。拿上面的例子來說:假如不是a=1的操作,而是a=new byte[1024*1024](分 配1M空間),那麼它會運行地很慢,此時CPU是等待其執行結束呢,還是先執行下面那句 flag=true呢?顯然,先執行flag=true可以提前使用CPU,加快整體效率,當然這樣的前 提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裏有兩種情況:後面的代碼先於前 面的代碼開始執行;前面的代碼先開始執行,但當效率較慢的時候,後面的代碼開始執 行並先於前面的代碼執行結束。不管誰先開始,總之後面的代碼在一些情況下存在先結 束的可能。
- 在硬件層面,CPU會將接收到的一批指令按照其規則重排序,同樣是基於CPU速度比緩存 速度快的原因,和上一點的目的類似,只是硬件處理的話,每次只能在接收到的有限指 令範圍內重排序,而虛擬機可以在更大層面、更多指令範圍內重排序。
數據依賴
如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時這兩個 操作之間就存在數據依賴。數據依賴分下列三種類型:
- 寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
- 寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
- 讀後寫 a = b;b = 1; 讀一個變量之後,再寫這個變量
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改 變。所以,編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理 器不會改變存在數據依賴關係的兩個操作的執行順序。
-
package Thread.day2; public class HappenBefore { //變量1 private static int a = 0; //變量2 private static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread t1,t2; for(int i = 0; i < 100; i++) { a = 0; flag = false; //線程1:更改數據 t1 = new Thread(() -> { a = 1; flag = true; }); //線程2:讀取數據 t2 = new Thread(() -> { if (flag) { a *= 1; } //存在指令重排 if (a == 0) System.out.println("happpen Before ===>" + a); }); t1.start(); t2.start(); //合成線程 t1.join(); t2.join(); } } }
-
happpen Before ===>0
-
happpen Before ===>0
-
happpen Before ===>0
-
happpen Before ===>0
-
happpen Before ===>1
-
happpen Before ===>0
-
happpen Before ===>0
-
happpen Before ===>1
-
happpen Before ===>1
4.volitale
volatile保證線程間變量的可見性,簡單地說就是當線程A對變量X進行了修改後,在線 程A後面執行的其他線程能看到變量X的變動,更詳細地說是要符合以下兩個規則:
- 線程對變量進行修改之後,要立刻回寫到主內存
- 線程對變量讀取的時候,要從主內存中讀,而不是緩存。
各線程的工作內存間彼此獨立、互不可見,在線程啓動的時候,虛擬機爲每個內存分配一 塊工作內存,不僅包含了線程內部定義的局部變量,也包含了線程所需要使用的共享變量 (非線程內構造的對象)的副本,即爲了提高執行效率。
volatile是不錯的機制,但是volatile不能保證原子性。
5.單例模式(DCL)
- double-checking
- volitate
- synchronized
-
package Thread.day3; /** * DCL單例模式:套路==》在多線程環境下,對外存在一個對象 * 1.構造器私有化——》避免外部new對象 * 2.提供私有的靜態屬性——》存儲對象的地址 * 3.提供共有的靜態方法——》獲取地址 */ public class Danli { public static void main(String[] args) { System.out.println("執行DanliTest的結果:"); new Thread(() ->{ System.out.println(DanliTest.getInstance()); }).start(); System.out.println(DanliTest.getInstance()); System.out.println("執行DanliTest2的結果:"); new Thread(() ->{ System.out.println(DanliTest2.getInstance()); }).start(); System.out.println(DanliTest2.getInstance()); } } class DanliTest { //2.提供私有靜態熟悉 private static volatile DanliTest dl; //沒有volatile,其他線程可能會訪問一個沒有初始化的對象 //1.構造器私有化 private DanliTest() { } public static DanliTest getInstance(){ //再次檢測(雙重檢測) if(null != dl) //避免不必要的同步,已經存在對象 return dl; synchronized (DanliTest.class) { if (null == dl) dl = new DanliTest(); //new一個對象的步驟:1.開闢空間,2.初始化對象信息,3.返回對象的地址給引用 //new對象中初始化對象信息比較耗時,慢,可能會出現指令重排,先返回對象的地址, //可能會出現A線程還在初始化對象信息,B線程就已經拿走了對象的引用(空的對象) } return dl; } } class DanliTest2 { private static volatile DanliTest2 dl; private DanliTest2() { } public static DanliTest2 getInstance(){ if (null == dl) dl = new DanliTest2(); return dl; } }
-
執行DanliTest的結果:
-
Thread.day3.DanliTest@4b9385
-
Thread.day3.DanliTest@4b9385
-
執行DanliTest2的結果:
-
Thread.day3.DanliTest2@2a0b20
-
Thread.day3.DanliTest2@14827d5
Threadlocal
- 在多線程環境下,每個線程都有自己的數據。一個線程使用自己的局部 變量比使用全局變量好,因爲局部變量只有線程自己能看見,不會影響 其他線程
- ThreadLocal能夠放一個線程級別的變量,其本身能夠被多個線程共享 使用,並且又能夠達到線程安全的目的。說白了,ThreadLocal就是想 在多線程環境下去保證成員變量的安全,常用的方法,就是 get/set/initialValue 方法。
- JDK建議ThreadLocal定義爲private static
- ThreadLocal最常用的地方就是爲每個線程綁定一個數據庫連接,HTTP 請求,用戶身份信息等,這樣一個線程的所有調用到的方法都可以非常 方便地訪問這些資源
- Hibernate的Session 工具類HibernateUtil
- 通過不同的線程對象設置Bean屬性,保證各個線程Bean對象的獨立 性。
-
package Thread.day3; /** * ThreadLocal:每個線程自身的存儲本地,局部區域 * get、set、initialValue */ public class Threadlocal { private static ThreadLocal<Integer> th = new ThreadLocal<>(); //更改初始化的值 /* private static ThreadLocal<Integer> th = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { return 200; } };*/ //java 1.8後 /*private static ThreadLocal<Integer> th = ThreadLocal.withInitial(() ->{return 200;}); private static ThreadLocal<Integer> th = ThreadLocal.withInitial(() -> 200);*/ public static void main(String[] args) { //獲取值 System.out.println(Thread.currentThread().getName()+"===>"+th.get()); //設置值 th.set(99); System.out.println(Thread.currentThread().getName()+"===>"+th.get()); new Thread(new Myrun()).start(); new Thread(new Myrun()).start(); new Thread(new Myrun()).start(); } static class Myrun implements Runnable{ @Override public void run() { //設置一個1-100的隨機數 th.set((int)(Math.random()*100+1)); System.out.println(Thread.currentThread().getName()+"===>"+th.get()); } } }
-
main===>null
-
main===>99
-
Thread-2===>31
-
Thread-0===>69
-
Thread-1===>57
-
package Thread.day3; /** * ThreadLcoal 分析上下文 * 1.構造器:哪裏調用就屬於哪裏 * 2.run:屬於本線程自身 */ public class Threadlocal1 { private static ThreadLocal<Integer> th = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Myrun1()).start(); } static class Myrun1 implements Runnable{ //屬於main方法的區域 public Myrun1() { System.out.println(Thread.currentThread().getName()+"===>"+th.get()); } @Override public void run() { System.out.println(Thread.currentThread().getName()+"===>"+th.get()); } } }
-
main===>null
-
Thread-0===>null
可重複鎖
鎖作爲併發共享數據保證一致性的工具,大多數內置鎖都是可重入的,也就是 說,如果某個線程試圖獲取一個已經由它自己持有的鎖時,那麼這個請求會立 刻成功,並且會將這個鎖的計數值加1,而當線程退出同步代碼塊時,計數器 將會遞減,當計數值等於0時,鎖釋放。如果沒有可重入鎖的支持,在第二次 企圖獲得鎖時將會進入死鎖狀態。可重入鎖隨處可見
eg:手動實現不可重複鎖
package Thread.day3;
/**
* 不可重入鎖:所不可以延續使用
*/
public class Lock1 {
public static LockTest lt = new LockTest();
public static void aa() throws InterruptedException {
lt.lock();
bb();
lt.unlock();
}
public static void bb() throws InterruptedException {
lt.lock();
System.out.println("......");
lt.unlock();
}
public static void main(String[] args) throws InterruptedException {
aa();
}
}
//鎖
class LockTest {
private boolean flag = false;
//獲取鎖
public synchronized void lock() throws InterruptedException {
while (flag == true){
wait();
}
flag = true;
}
//釋放鎖
public synchronized void unlock() throws InterruptedException {
flag = false;
notify();
}
}
eg:手動實現可重複鎖
package Thread.day3;
/**
* 可重入鎖:所可以延續使用
*/
public class ReLock {
public static LockTest1 lt = new LockTest1();
public static void aa() throws InterruptedException {
lt.lock();
System.out.println(lt.getCount());
bb();
lt.unlock();
Thread.sleep(100);
System.out.println(lt.getCount());
}
public static void bb() throws InterruptedException {
lt.lock();
System.out.println(lt.getCount());
lt.unlock();
System.out.println(lt.getCount());
}
public static void main(String[] args) throws InterruptedException {
aa();
}
}
class LockTest1 {
private boolean flag = false;
private Thread thread = null; //當前線程
private int count = 0; //計數器
public int getCount() {
return count;
}
//獲取鎖
public synchronized void lock() throws InterruptedException {
Thread t = Thread.currentThread();
while (flag == true && t!=thread){
wait();
}
count++;
thread = t;
flag = true;
}
//釋放鎖
public synchronized void unlock() throws InterruptedException {
if(Thread.currentThread() == thread){
count--;
if(count == 0){
flag = false;
notify();
thread = null;
}
}
}
}
-
1
-
2
-
1
-
0
通過封裝好的類(ReentrantLockTest)
package Thread.day3;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入鎖:所可以延續使用
*/
public class ReLock {
public static ReentrantLock lt = new ReentrantLock();
public static void aa() throws InterruptedException {
System.out.println(lt.getHoldCount());
lt.lock();
System.out.println(lt.getHoldCount());
bb();
lt.unlock();
Thread.sleep(100);
System.out.println(lt.getHoldCount());
}
public static void bb() throws InterruptedException {
lt.lock();
System.out.println(lt.getHoldCount());
lt.unlock();
System.out.println(lt.getHoldCount());
}
public static void main(String[] args) throws InterruptedException {
aa();
}
}