概述:
1,什麼是進程?什麼是線程?
進程是一個應用程序(1個進程是一個軟件)
線程是一個進程中的執行場景/執行單元。
一個進程可以啓動多個線程
2,對於Java程序來說,當在DOS命令窗口中輸入:
java HelloWorld 回車之後
會先啓動JVM,而JVM則是一個進程
JVM再啓動一個主線程調用main方法。
同時再啓動一個垃圾回收線程負責看護,回收垃圾。
最起碼,現在的java程序中至少有兩個線程併發。
一個是垃圾回收線程,一個是執行main方法的主線程
3,進程與線程關係:
進程A和進程B的內存獨立不共享
線程A和線程B:
在Java中線程A和線程B,堆內存和方法區內存共享。
但是棧內存獨立,一個線程一個棧
假設啓動10個線程,會有10個棧空間,每個棧之間互不干擾,各自執行各自的,這就是多線程併發。
Java中之所以有多線程機制,目的就是爲了提高程序的處理效率。
4,單核處理器無法完全做到多線程併發,表面上看似爲多線程,實則爲線程交替運行。
5、關於線程對象的生命週期?
新建狀態
就緒狀態
運行狀態
阻塞狀態
死亡狀態
Java語言實現線程的兩種方式:
直接繼承Thread類
第一種方式:編寫一個類,直接繼承java.lang.Thread,重寫run方法。
怎麼創建線程對象? new就行了。
怎麼啓動線程呢? 調用線程對象的start()方法。
注意:
亙古不變的道理:
方法體當中的代碼永遠都是自上而下的順序依次逐行執行的。
// 定義線程類
public class MyThread extends Thread{
public void run(){
}
}
// 創建線程對象
MyThread t = new MyThread();
// 啓動線程。
t.start();
代碼演示:
public class ThreadTest02 {
public static void main(String[] args) {
// 這裏是main方法,這裏的代碼屬於主線程,在主棧中運行。
// 新建一個分支線程對象
MyThread t = new MyThread();
// 啓動線程
//t.run(); // 不會啓動線程,不會分配新的分支棧。(這種方式就是單線程。)
// start()方法的作用是:啓動一個分支線程,在JVM中開闢一個新的棧空間,這段代碼任務完成之後,瞬間就結束了。
// 這段代碼的任務只是爲了開啓一個新的棧空間,只要新的棧空間開出來,start()方法就結束了。線程就啓動成功了。
// 啓動成功的線程會自動調用run方法,並且run方法在分支棧的棧底部(壓棧)。
// run方法在分支棧的棧底部,main方法在主棧的棧底部。run和main是平級的。
t.start();
// 這裏的代碼還是運行在主線程中。
for(int i = 0; i < 1000; i++){
System.out.println("主線程--->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
// 編寫程序,這段程序運行在分支線程中(分支棧)。
for(int i = 0; i < 1000; i++){
System.out.println("分支線程--->" + i);
}
}
}
Thread常用方法:
1、怎麼獲取當前線程對象?
Thread t = Thread.currentThread();
返回值t就是當前線程。
2、獲取線程對象的名字
String name = 線程對象.getName();
3、修改線程對象的名字
線程對象.setName("線程名字");
4、當線程沒有設置名字的時候,默認的名字有什麼規律?(瞭解一下)
Thread-0
Thread-1
Thread-2
Thread-3
.....
代碼演示:
public class ThreadTest05 {
public void doSome(){
// 這樣就不行了
//this.getName();
//super.getName();
// 但是這樣可以
String name = Thread.currentThread().getName();
System.out.println("------->" + name);
}
public static void main(String[] args) {
ThreadTest05 tt = new ThreadTest05();
tt.doSome();
//currentThread就是當前線程對象
// 這個代碼出現在main方法當中,所以當前線程就是主線程。
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()); //main
// 創建線程對象
MyThread2 t = new MyThread2();
// 設置線程的名字
t.setName("t1");
// 獲取線程的名字
String tName = t.getName();
System.out.println(tName); //Thread-0
MyThread2 t2 = new MyThread2();
t2.setName("t2");
System.out.println(t2.getName()); //Thread-1\
t2.start();
// 啓動線程
t.start();
}
}
class MyThread2 extends Thread {
public void run(){
for(int i = 0; i < 100; i++){
// currentThread就是當前線程對象。當前線程是誰呢?
// 當t1線程執行run方法,那麼這個當前線程就是t1
// 當t2線程執行run方法,那麼這個當前線程就是t2
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() + "-->" + i);
//System.out.println(super.getName() + "-->" + i);
//System.out.println(this.getName() + "-->" + i);
}
}
}
實現Runnable接口
第二種方式:編寫一個類,實現java.lang.Runnable接口,實現run方法。
// 定義一個可運行的類
public class MyRunnable implements Runnable {
public void run(){
}
}
// 創建線程對象
Thread t = new Thread(new MyRunnable());
// 啓動線程
t.start();
注意:第二種方式實現接口比較常用,因爲一個類實現了接口,它還可以去繼承
其它的類,更靈活。
代碼演示:
public class ThreadTest03 {
public static void main(String[] args) {
// 創建一個可運行的對象
//MyRunnable r = new MyRunnable();
// 將可運行的對象封裝成一個線程對象
//Thread t = new Thread(r);
Thread t = new Thread(new MyRunnable()); // 合併代碼
// 啓動線程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("主線程--->" + i);
}
}
}
// 這並不是一個線程類,是一個可運行的類。它還不是一個線程。
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("分支線程--->" + i);
}
}
}
代碼演示(採用匿名內部類):
public class ThreadTest04 {
public static void main(String[] args) {
// 創建線程對象,採用匿名內部類方式。
// 這是通過一個沒有名字的類,new出來的對象。
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("t線程---> " + i);
}
}
});
// 啓動線程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("main線程---> " + i);
}
}
}
Sleep方法
關於線程的sleep方法:
static void sleep(long millis)
1、靜態方法:Thread.sleep(1000);
2、參數是毫秒
3、作用:讓當前線程進入休眠,進入“阻塞狀態”,放棄佔有CPU時間片,讓給其它線程使用。
這行代碼出現在A線程中,A線程就會進入休眠。
這行代碼出現在B線程中,B線程就會進入休眠。
4、Thread.sleep()方法,可以做到這種效果:
間隔特定的時間,去執行一段特定的代碼,每隔多久執行一次。
代碼演示:
public class ThreadTest06 {
public static void main(String[] args) {
// 讓當前線程進入休眠,睡眠5秒
// 當前線程是主線程!!!
/*try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
// 5秒之後執行這裏的代碼
//System.out.println("hello world!");
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
// 睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代碼演示(Sleep面試題):
/*
關於Thread.sleep()方法的一個面試題:
*/
public class ThreadTest07 {
public static void main(String[] args) {
// 創建線程對象
Thread t = new MyThread3();
t.setName("t");
t.start();
// 調用sleep方法
try {
// 問題:這行代碼會讓線程t進入休眠狀態嗎?
t.sleep(1000 * 5); // 在執行的時候還是會轉換成:Thread.sleep(1000 * 5);
// 這行代碼的作用是:讓當前線程進入休眠,也就是說main線程進入休眠。
// 這樣代碼出現在main方法中,main線程睡眠。
} catch (InterruptedException e) {
e.printStackTrace();
}
// 5秒之後這裏纔會執行。
System.out.println("hello World!");
}
}
class MyThread3 extends Thread {
public void run(){
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
終止線程Sleep睡眠
終斷t線程的睡眠(這種終斷睡眠的方式依靠了java的異常處理機制。)
t.interrupt(); // 干擾,一盆冷水過去!
重點:run()當中的異常不能throws,只能try catch
代碼演示:
/*
sleep睡眠太久了,如果希望半道上醒來,你應該怎麼辦?也就是說怎麼叫醒一個正在睡眠的線程??
注意:這個不是終斷線程的執行,是終止線程的睡眠。
*/
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
// 希望5秒之後,t線程醒來(5秒之後主線程手裏的活兒幹完了。)
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 終斷t線程的睡眠(這種終斷睡眠的方式依靠了java的異常處理機制。)
t.interrupt(); // 干擾,一盆冷水過去!
}
}
class MyRunnable2 implements Runnable {
// 重點:run()當中的異常不能throws,只能try catch
// 因爲run()方法在父類中沒有拋出任何異常,子類不能比父類拋出更多的異常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
// 打印異常信息
//e.printStackTrace();
}
//1年之後纔會執行這裏
System.out.println(Thread.currentThread().getName() + "---> end");
// 調用doOther
//doOther();
}
// 其它方法可以throws
/*public void doOther() throws Exception{
}*/
}
合理的終止一個線程的執行
代碼演示:
/*
怎麼合理的終止一個線程的執行。這種方式是很常用的。
*/
public class ThreadTest10 {
public static void main(String[] args) {
MyRunable4 r = new MyRunable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 模擬5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 終止線程
// 你想要什麼時候終止t的執行,那麼你把標記修改爲false,就結束了。
r.run = false;
}
}
class MyRunable4 implements Runnable {
// 打一個布爾標記
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// return就結束了,你在結束之前還有什麼沒保存的。
// 在這裏可以保存呀。
//save....
//終止當前線程
return;
}
}
}
}
線程的調度
關於線程的調度
1.1、常見的線程調度模型有哪些?
搶佔式調度模型:
那個線程的優先級比較高,搶到的CPU時間片的概率就高一些/多一些。
java採用的就是搶佔式調度模型。
均分式調度模型:
平均分配CPU時間片。每個線程佔有的CPU時間片時間長度一樣。
平均分配,一切平等。
有一些編程語言,線程調度模型採用的是這種方式。
1.2、java中提供了哪些方法是和線程調度有關係的呢?
實例方法:
void setPriority(int newPriority) 設置線程的優先級
int getPriority() 獲取線程優先級
最低優先級1
默認優先級是5
最高優先級10
優先級比較高的獲取CPU時間片可能會多一些。(但也不完全是,大概率是多的。)
靜態方法:
static void yield() 讓位方法
暫停當前正在執行的線程對象,並執行其他線程
yield()方法不是阻塞方法。讓當前線程讓位,讓給其它線程使用。
yield()方法的執行會讓當前線程從“運行狀態”回到“就緒狀態”。
注意:在回到就緒之後,有可能還會再次搶到。
實例方法:
void join()
合併線程
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 當前線程進入阻塞,t線程執行,直到t線程結束。當前線程纔可以繼續。
}
}
class MyThread2 extends Thread{
}
線程優先級
實例方法:
void setPriority(int newPriority) 設置線程的優先級
int getPriority() 獲取線程優先級
最低優先級1
默認優先級是5
最高優先級10
優先級比較高的獲取CPU時間片可能會多一些。(但也不完全是,大概率是多的。)
最高優先級(10):Thread.MAX_PRIORITY
最低優先級(1):Thread.MIN_PRIORITY
默認優先級(5):Thread.NORM_PRIORITY
代碼演示:
/*
瞭解:關於線程的優先級
*/
public class ThreadTest11 {
public static void main(String[] args) {
// 設置主線程的優先級爲1
Thread.currentThread().setPriority(1);
// 獲取當前線程對象,獲取當前線程的優先級
Thread currentThread = Thread.currentThread();
// main線程的默認優先級是:5
//System.out.println(currentThread.getName() + "線程的默認優先級是:" + currentThread.getPriority());
Thread t = new Thread(new MyRunnable5());
t.setPriority(10);
t.setName("t");
t.start();
// 優先級較高的,只是搶到的CPU時間片相對多一些。
// 大概率方向更偏向於優先級比較高的。
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
class MyRunnable5 implements Runnable {
@Override
public void run() {
// 獲取線程優先級
//System.out.println(Thread.currentThread().getName() + "線程的默認優先級:" + Thread.currentThread().getPriority());
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
讓位:Thread.yield()
靜態方法:
static void yield() 讓位方法
暫停當前正在執行的線程對象,並執行其他線程
yield()方法不是阻塞方法。讓當前線程讓位,讓給其它線程使用。
yield()方法的執行會讓當前線程從“運行狀態”回到“就緒狀態”。
注意:在回到就緒之後,有可能還會再次搶到。
代碼演示:
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
for(int i = 1; i <= 10000; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyRunnable6 implements Runnable {
@Override
public void run() {
for(int i = 1; i <= 10000; i++) {
//每100個讓位一次。
if(i % 100 == 0){
Thread.yield(); // 當前線程暫停一下,讓給主線程。
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
線程合併:Join
實例方法:
合併線程:void join()
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 當前線程進入阻塞,t線程執行,直到t線程結束。當前線程纔可以繼續。
}
}
class MyThread2 extends Thread{
}
代碼演示:
public class ThreadTest13 {
public static void main(String[] args) {
System.out.println("main begin");
Thread t = new Thread(new MyRunnable7());
t.setName("t");
t.start();
//合併線程
try {
t.join(); // t合併到當前線程中,當前線程受阻塞,t線程執行直到結束。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable7 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
死鎖
死鎖代碼要會寫。
一般面試官要求你會寫。
只有會寫的,纔會在以後的開發中注意這個事兒。
因爲死鎖很難調試。
代碼演示:
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2兩個線程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
線程安全:
2、關於多線程併發環境下,數據的安全問題。
2.1、爲什麼這個是重點?
以後在開發中,我們的項目都是運行在服務器當中,
而服務器已經將線程的定義,線程對象的創建,線程
的啓動等,都已經實現完了。這些代碼我們都不需要
編寫。
最重要的是:你要知道,你編寫的程序需要放到一個
多線程的環境下運行,你更需要關注的是這些數據
在多線程併發的環境下是否是安全的。(重點:*****)
2.2、什麼時候數據在多線程併發的環境下會存在安全問題呢?
三個條件:
條件1:多線程併發。
條件2:有共享數據。
條件3:共享數據有修改的行爲。
滿足以上3個條件之後,就會存在線程安全問題。
2.3、怎麼解決線程安全問題呢?
當多線程併發的環境下,有共享數據,並且這個數據還會被修改,此時就存在
線程安全問題,怎麼解決這個問題?
線程排隊執行。(不能併發)。
用排隊執行解決線程安全問題。
這種機制被稱爲:線程同步機制。
專業術語叫做:線程同步,實際上就是線程不能併發了,線程必須排隊執行。
怎麼解決線程安全問題呀?
使用“線程同步機制”。
線程同步就是線程排隊了,線程排隊了就會犧牲一部分效率,沒辦法,數據安全
第一位,只有數據安全了,我們纔可以談效率。數據不安全,沒有效率的事兒。
2.4、說到線程同步這塊,涉及到這兩個專業術語:
異步編程模型:
線程t1和線程t2,各自執行各自的,t1不管t2,t2不管t1,
誰也不需要等誰,這種編程模型叫做:異步編程模型。
其實就是:多線程併發(效率較高。)
異步就是併發。
同步編程模型:
線程t1和線程t2,在線程t1執行的時候,必須等待t2線程執行
結束,或者說在t2線程執行的時候,必須等待t1線程執行結束,
兩個線程之間發生了等待關係,這就是同步編程模型。
效率較低。線程排隊執行。
同步就是排隊。
3、Java中有三大變量?【重要的內容。】
實例變量:在堆中。
靜態變量:在方法區。
局部變量:在棧中。
以上三大變量中:
局部變量永遠都不會存在線程安全問題。
因爲局部變量不共享。(一個線程一個棧。)
局部變量在棧中。所以局部變量永遠都不會共享。
實例變量在堆中,堆只有1個。
靜態變量在方法區中,方法區只有1個。
堆和方法區都是多線程共享的,所以可能存在線程安全問題。
局部變量+常量:不會有線程安全問題。
成員變量:可能會有線程安全問題。
4、如果使用局部變量的話:
建議使用:StringBuilder。
因爲局部變量不存在線程安全問題。選擇StringBuilder。
StringBuffer效率比較低。
ArrayList是非線程安全的。
Vector是線程安全的。
HashMap HashSet是非線程安全的。
Hashtable是線程安全的。
線程同步代碼塊
線程同步機制的語法是:
synchronized(){
// 線程同步代碼塊。
}
synchronized後面小括號中傳的這個“數據”是相當關鍵的。
這個數據必須是多線程共享的數據。才能達到多線程排隊。
()中寫什麼?
那要看你想讓哪些線程同步。
假設t1、t2、t3、t4、t5,有5個線程,
你只希望t1 t2 t3排隊,t4 t5不需要排隊。怎麼辦?
你一定要在()中寫一個t1 t2 t3共享的對象。而這個
對象對於t4 t5來說不是共享的。
在java語言中,任何一個對象都有“一把鎖”,其實這把鎖就是標記。(只是把它叫做鎖。)
100個對象,100把鎖。1個對象1把鎖。
synchronized的三種寫法
synchronized有三種寫法:
第一種:同步代碼塊
靈活
synchronized(線程共享對象){
同步代碼塊;
}
第二種:在實例方法上使用synchronized
public synchronized void withdraw(double money){
}
表示共享對象一定是this
並且同步代碼塊是整個方法體。
缺點:
synchronized出現在實例方法上,一定鎖的是this。
沒得挑。只能是this。不能是其他的對象了。
所以這種方式不靈活。
synchronized出現在實例方法上,表示整個方法體都需要同步,
可能會無故擴大同步的範圍,導致程序的執行效率降低。
所以這種方式不常用。
優點:
代碼寫的少了。節儉了。
建議:
如果共享的對象就是this,並且需要同步的代碼塊是整個方法體, 建議使用這種方式。
第三種:在靜態方法上使用synchronized
表示找類鎖。
類鎖永遠只有1把。
就算創建了100個對象,那類鎖也只有一把。
對象鎖:1個對象1把鎖,100個對象100把鎖。
類鎖:100個對象,也可能只是1把類鎖。
代碼演示:
銀行賬戶
/*
銀行賬戶
使用線程同步機制,解決線程安全問題。
*/
public class Account {
// 賬號
private String actno;
// 餘額
private double balance; //實例變量。
//對象
Object obj = new Object(); // 實例變量。(Account對象是多線程共享的,Account對象中的實例變量obj也是共享的。)
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法
public void withdraw(double money){
//int i = 100;
//i = 101;
// 以下這幾行代碼必須是線程排隊的,不能併發。
// 一個線程把這裏的代碼全部執行結束之後,另一個線程才能進來。
/*
這裏的共享對象是:賬戶對象。
賬戶對象是共享的,那麼this就是賬戶對象吧!!!
不一定是this,這裏只要是多線程共享的那個對象就行。
以下代碼的執行原理?
1、假設t1和t2線程併發,開始執行以下代碼的時候,肯定有一個先一個後。
2、假設t1先執行了,遇到了synchronized,這個時候自動找“後面共享對象”的對象鎖,
找到之後,並佔有這把鎖,然後執行同步代碼塊中的程序,在程序執行過程中一直都是
佔有這把鎖的。直到同步代碼塊代碼結束,這把鎖纔會釋放。
3、假設t1已經佔有這把鎖,此時t2也遇到synchronized關鍵字,也會去佔有後面
共享對象的這把鎖,結果這把鎖被t1佔有,t2只能在同步代碼塊外面等待t1的結束,
直到t1把同步代碼塊執行結束了,t1會歸還這把鎖,此時t2終於等到這把鎖,然後
t2佔有這把鎖之後,進入同步代碼塊執行程序。
這樣就達到了線程排隊執行。
這裏需要注意的是:這個共享對象一定要選好了。這個共享對象一定是你需要排隊
執行的這些線程對象所共享的。
*/
//Object obj2 = new Object();
//synchronized (this){
//synchronized (obj) {
//synchronized ("abc") { // "abc"在字符串常量池當中。
//synchronized (null) { // 報錯:空指針。
//synchronized (obj2) { // 這樣編寫就不安全了。因爲obj2不是共享對象。
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
//}
}
}
線程類
public class AccountThread extends Thread {
// 兩個線程必須共享同一個賬戶對象。
private Account act;
// 通過構造方法傳遞過來賬戶對象
public AccountThread(Account act) {
this.act = act;
}
public void run(){
// run方法的執行表示取款操作。
// 假設取款5000
double money = 5000;
// 取款
// 多線程併發執行這個方法。
//synchronized (this) { //這裏的this是AccountThread對象,這個對象不共享!
synchronized (act) { // 這種方式也可以,只不過擴大了同步的範圍,效率更低了。
act.withdraw(money);
}
System.out.println(Thread.currentThread().getName() + "對"+act.getActno()+"取款"+money+"成功,餘額" + act.getBalance());
}
}
主程序main測試
public class Test {
public static void main(String[] args) {
// 創建賬戶對象(只創建1個)
Account act = new Account("act-001", 10000);
// 創建兩個線程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
// 設置name
t1.setName("t1");
t2.setName("t2");
// 啓動線程取款
t1.start();
t2.start();
}
}
開發中應該怎麼解決線程安全問題?
是一上來就選擇線程同步嗎?synchronized
不是,synchronized會讓程序的執行效率降低,用戶體驗不好。
系統的用戶吞吐量降低。用戶體驗差。在不得已的情況下再選擇
線程同步機制。
第一種方案:儘量使用局部變量代替“實例變量和靜態變量”。
第二種方案:如果必須是實例變量,那麼可以考慮創建多個對象,這樣
實例變量的內存就不共享了。(一個線程對應1個對象,100個線程對應100個對象,
對象不共享,就沒有數據安全問題了。)
第三種方案:如果不能使用局部變量,對象也不能創建多個,這個時候
就只能選擇synchronized了。線程同步機制。
守護線程
java語言中線程分爲兩大類:
一類是:用戶線程
一類是:守護線程(後臺線程)
其中具有代表性的就是:垃圾回收線程(守護線程)。
守護線程的特點:
一般守護線程是一個死循環,所有的用戶線程只要結束,
守護線程自動結束。
注意:主線程main方法是一個用戶線程。
守護線程用在什麼地方呢?
每天00:00的時候系統數據自動備份。
這個需要使用到定時器,並且我們可以將定時器設置爲守護線程。
一直在那裏看着,沒到00:00的時候就備份一次。所有的用戶線程
如果結束了,守護線程自動退出,沒有必要進行數據備份了。
代碼演示
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("備份數據的線程");
// 啓動線程之前,將線程設置爲守護線程
t.setDaemon(true);
t.start();
// 主線程:主線程是用戶線程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循環,但由於該線程是守護者,當用戶線程結束,守護線程自動終止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定時器
定時器的作用:
間隔特定的時間,執行特定的程序。
每週要進行銀行賬戶的總賬操作。
每天要進行數據的備份操作。
在實際的開發中,每隔多久執行一段特定的程序,這種需求是很常見的,
那麼在java中其實可以採用多種方式實現:
可以使用sleep方法,睡眠,設置睡眠時間,沒到這個時間點醒來,執行
任務。這種方式是最原始的定時器。(比較low)
在java的類庫中已經寫好了一個定時器:java.util.Timer,可以直接拿來用。
不過,這種方式在目前的開發中也很少用,因爲現在有很多高級框架都是支持
定時任務的。
在實際的開發中,目前使用較多的是Spring框架中提供的SpringTask框架,
這個框架只要進行簡單的配置,就可以完成定時器的任務。
代碼演示:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
使用定時器指定定時任務。
*/
public class TimerTest {
public static void main(String[] args) throws Exception {
// 創建定時器對象
Timer timer = new Timer();
//Timer timer = new Timer(true); //守護線程的方式
// 指定定時任務
//timer.schedule(定時任務, 第一次執行時間, 間隔多久執行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-03-14 09:34:30");
//timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
// 每年執行一次。
//timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);
//匿名內部類方式
timer.schedule(new TimerTask(){
@Override
public void run() {
// code....
}
} , firstTime, 1000 * 10);
}
}
// 編寫一個定時任務類
// 假設這是一個記錄日誌的定時任務
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 編寫你需要執行的任務就行了。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次數據備份!");
}
}
實現線程的第三種方式:實現Callable接口。(JDK8新特性。)
這種方式實現的線程可以獲取線程的返回值。
之前講解的那兩種方式是無法獲取線程返回值的,因爲run方法返回void。
思考:系統委派一個線程去執行一個任務,該線程執行完任務之後,可能會有一個執行結果,我們怎麼能拿到這個執行結果呢?
使用第三種方式:實現Callable接口方式。
這種方式的優點:可以獲取到線程的執行結果。
這種方式的缺點:效率比較低,在獲取t線程執行結果的時候,當前線程受阻塞,效率較低。
代碼演示:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask; // JUC包下的,屬於java的併發包,老JDK中沒有這個包。新特性。
/*
*/
public class ThreadTest15 {
public static void main(String[] args) throws Exception {
// 第一步:創建一個“未來任務類”對象。
// 參數非常重要,需要給一個Callable接口實現類對象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call()方法就相當於run方法。只不過這個有返回值
// 線程執行一個任務,執行之後可能會有一個執行結果
// 模擬執行
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b; //自動裝箱(300結果變成Integer)
}
});
// 創建線程對象
Thread t = new Thread(task);
// 啓動線程
t.start();
// 這裏是main方法,這是在主線程中。
// 在主線程中,怎麼獲取t線程的返回結果?
// get()方法的執行會導致“當前線程阻塞”
Object obj = task.get();
System.out.println("線程執行結果:" + obj);
// main方法這裏的程序要想執行必須等待get()方法的結束
// 而get()方法可能需要很久。因爲get()方法是爲了拿另一個線程的執行結果
// 另一個線程執行是需要時間的。
System.out.println("hello world!");
}
}
關於Object類中的wait和notify方法。(生產者和消費者模式!)
第一:wait和notify方法不是線程對象的方法,是java中任何一個java對象
都有的方法,因爲這兩個方式是Object類中自帶的。
wait方法和notify方法不是通過線程對象調用,
不是這樣的:t.wait(),也不是這樣的:t.notify()..不對。
第二:wait()方法作用?
Object o = new Object();
o.wait();
表示:讓正在o對象上活動的線程進入等待狀態,無期限等待,直到被喚醒爲止。
o.wait();方法的調用,會讓“當前線程(正在o對象上活動的線程)”進入等待狀態。
第三:notify()方法作用?
Object o = new Object();
o.notify();
表示:喚醒正在o對象上等待的線程。
還有一個notifyAll()方法:
這個方法是喚醒o對象上處於等待的所有線程。
1、使用wait方法和notify方法實現“生產者和消費者模式”
什麼是“生產者和消費者模式”?
生產線程負責生產,消費線程負責消費。
生產線程和消費線程要達到均衡。
這是一種特殊的業務需求,在這種特殊的情況下需要使用wait方法和notify方法。
1、wait和notify方法不是線程對象的方法,是普通java對象都有的方法。
2、wait方法和notify方法建立在線程同步的基礎之上。因爲多線程要同時操作一個倉庫。有線程安全問題。
3、wait方法作用:o.wait()讓正在o對象上活動的線程t進入等待狀態,並且釋放掉t線程之前佔有的o對象的鎖。
4、notify方法作用:o.notify()讓正在o對象上等待的線程喚醒,只是通知,不會釋放o對象上之前佔有的鎖。
代碼演示:
import java.util.ArrayList;
import java.util.List;
/*
模擬這樣一個需求:
倉庫我們採用List集合。
List集合中假設只能存儲1個元素。
1個元素就表示倉庫滿了。
如果List集合中元素個數是0,就表示倉庫空了。
保證List集合中永遠都是最多存儲1個元素。
必須做到這種效果:生產1個消費1個。
*/
public class ThreadTest16 {
public static void main(String[] args) {
// 創建1個倉庫對象,共享的。
List list = new ArrayList();
// 創建兩個線程對象
// 生產者線程
Thread t1 = new Thread(new Producer(list));
// 消費者線程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生產者線程");
t2.setName("消費者線程");
t1.start();
t2.start();
}
}
// 生產線程
class Producer implements Runnable {
// 倉庫
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生產(使用死循環來模擬一直生產)
while(true){
// 給倉庫對象list加鎖。
synchronized (list){
if(list.size() > 0){ // 大於0,說明倉庫中已經有1個元素了。
try {
// 當前線程進入等待狀態,並且釋放Producer之前佔有的list集合的鎖。
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能夠執行到這裏說明倉庫是空的,可以生產
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 喚醒消費者進行消費
list.notifyAll();
}
}
}
}
// 消費線程
class Consumer implements Runnable {
// 倉庫
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消費
while(true){
synchronized (list) {
if(list.size() == 0){
try {
// 倉庫已經空了。
// 消費者線程等待,釋放掉list集合的鎖
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能夠執行到此處說明倉庫中有數據,進行消費。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 喚醒生產者生產。
list.notifyAll();
}
}
}
}