一篇文章帶你入門Java多線程
進程
1、進程是指運行中的程序,比如我們使用qq,就啟動了一個進程,操作系統(tǒng)就會為該進程分配內存空間。當我們使用迅雷,又啟動了一個進程,操作系統(tǒng)將為迅雷分配新的內存空間
2、進程是程序的一次執(zhí)行過程,或是正在運行的一個程序。是動態(tài)過程:有它自身的產生、存在和消亡的過程
其他相關概念
1、單線程:同一個時刻,只允許執(zhí)行一個線程
2、多線程:同一時刻,可以執(zhí)行多個線程,比如:一個qq進程,可以同時打開多個聊天窗口,一個迅雷進程,可以同時下載多個文件
3、并發(fā):同一時刻,多個任務交替執(zhí)行,造成一種“貌似同時”的錯覺,簡單的說,單核cpu實現的多任務就是并發(fā)
4、并行:同一時刻,多個任務同時進行。多核cpu可以實現并行。并發(fā)和并行:如果開的程序太多,有可能也會觸發(fā)并發(fā)
創(chuàng)建線程的兩種方式
1、繼承Thread類,重寫run方法
實例:
//該線程每隔1秒鐘。在控制臺輸出"喵喵",打印8次后結束線程
public class Thread01 {
public static void main(String[] args) {
//創(chuàng)建一個cat對象,可以當作線程使用
Cat cat=new Cat();
cat.start();//啟動線程
}
}
//1、當一個類繼承了 Thread 類 ,該類就可以當作線程使用
//2、我們會重寫run方法,寫上自己的業(yè)務代碼
//3、run Thread 類 實現了 Runnable 接口的run方法
class Cat extends Thread{
int times=0;
@Override
public void run() { //重寫run方法,寫上自己的業(yè)務邏輯
while(true) {
System.out.println("喵喵"+ ++times);
//讓該線程休眠1秒鐘
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times==8){//設置打印次數
break;
}
}
}
}
為什么使用start()方法而不直接使用run()方法
因為run()方法就是一個普通的方法,沒有真正的啟動一個線程,就會把run方法執(zhí)行完畢,才向下執(zhí)行
start()方法底層
(1)
public synchronized void start() {
start0();
}
//start0();是start中最主要的方法
(2)
//start0(); 是本地方法,是JVM調用,底層是C/C++實現
//真正實現多線程的效果,是start0(),而不是run,也可以說在start0()本地方法中去調用了Run()方法

2、實現Runnable接口,重寫run方法
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1=new Thread(t1);
Thread thread2=new Thread(t2);
thread1.start();
thread2.start();
}
}
class T1 implements Runnable{
int count=0;
@Override
public void run() {
while (true) {
//每隔1秒輸出"hello,world",輸出10次
System.out.println("hello,world " + ++count + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count==50){
break;
}
}
}
}
class T2 implements Runnable{
int count=0;
@Override
public void run() {
while (true) {
//每隔1秒輸出"hello,world",輸出10次
System.out.println("hi " + ++count + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count==60){
break;
}
}
}
繼承Thread 和 實現Rnnable的區(qū)別
1、從Java的設計來看,通過繼承Thread或者實現Runnable接口來創(chuàng)建線程本質上沒有區(qū)別,從jdk幫助文檔我們可以看到Thread類本身就實現了Runnable接口
2、實現Runnable接口方式更加適合多個線程共享一個資源的情況,并且避免了單繼承的限制,建議使用Runnable接口
售票系統(tǒng)
SellTicket01類繼承Thread實現
class SellTicket01 extends Thread{
private static int ticketNum=100; //讓多個線程共享num
@Override
public void run() {
while(true) {
if (ticketNum <= 0) {
System.out.println("售票結束");
break;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一張票" + "剩余票數=" + --ticketNum);
}
}
}
//====================main方法===========================
public static void main(String[] args) {
//測試
SellTicket01 sellTicket01 = new SellTicket01();
SellTicket01 sellTicket02 = new SellTicket01();
SellTicket01 sellTicket03 = new SellTicket01();
//這里會出現票數超賣現象
sellTicket01.start();
sellTicket02.start();
sellTicket03.start();
}
SellTicket02類實現Runnable接口
class SellTicket02 implements Runnable{
private int ticketNum=99;
@Override
public void run() {
while(true) {
if (ticketNum <= 0) {
System.out.println("售票結束");
break;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一張票" + "剩余票數=" + --ticketNum);
}
}
}
//=================main================
public static void main(String[] args) {
SellTicket02 sellTicket02 = new SellTicket02();
new Thread(sellTicket02).start();//第一個線程-窗口
new Thread(sellTicket02).start();//第二個線程-窗口
new Thread(sellTicket02).start();//第三個線程-窗口
}
兩個方法都會有超票的現象,線程安全的問題
線程終止
基本說明
1、當線程完成任務后,會自動退出
2、還可以通過使用變量來控制run方法退出的方式停止線程,即通知方式
通知方式
public class ThreadExit_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
//如果希望主線程去控制t中線程的終止,需要能夠控制loop
//修改loop,讓t退出run方法,從而終止t線程-->通知方式
//讓主線程休眠10秒,在通知t線程退出
Thread.sleep(10000);
t.setLoop(false);//將T線程中的循環(huán)判斷為false
}
}
class T extends Thread{
private int count=0;
private boolean loop=true;
@Override
public void run() {
while (loop){
try {
Thread.sleep(1000); //休眠50毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T線程執(zhí)行"+ ++count);
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
線程常用方法
常用第一組
1、setName:設置線程名稱,使之與參數name相同
2、getName:返回該線程的名稱
3、start:該線程開始執(zhí)行;java虛擬機底層調用該線程的start()方法
4、run:調用線程對象run方法
5、setPriority:更改線程的優(yōu)先級
6、getPriority:獲取線程的優(yōu)先級
7、sleep:在指定的毫秒數內讓當前正在執(zhí)行的線程休眠(暫停執(zhí)行)
8、interrupt:中斷線程
注意事項和細節(jié)
1、start底層會創(chuàng)建新的線程,調用run,run就是一個簡單的方法調用,不會啟動新線程
2、線程優(yōu)先級的范圍
3、interrupt,中斷線程,但沒有真正的結束線程,所以一般用于中斷正在休眠線程
4、sleep:線程的靜態(tài)方法,使當前線程休眠
常用方法第二組
1、yield:線程的禮讓。讓出cpu,讓其他線程執(zhí)行,但禮讓的時間不確定,所以也不一定禮讓成功
2、join:線程的插隊。插隊的線程一旦插隊成功,則肯定先執(zhí)行完插入的線程所有的任務
案例
創(chuàng)建一個子線程,每隔1s輸出hello,輸出20次,主線程每隔1s,輸出hi,輸出20次。要求:兩個線程同時執(zhí)行,當主線程輸出5次后,就讓子線程運行完畢,主線程再繼續(xù)
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
T2 t2 = new T2();
t2.start();
for (int i = 1;i<=20;i++){
Thread.sleep(1000);
System.out.println("主線程(小弟)吃了"+i+"個包子");
if (i==5){
System.out.println("主線程(小弟)讓子線程(老大)先吃");
//yield 禮讓
t2.yield();
//線程插隊,join
// t2.join();
System.out.println("子線程(老大)吃完,主線程(小弟)再吃");
}
}
}
}
class T2 extends Thread{
@Override
public void run() {
for (int i=1;i<=20;i++){
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子線程(老大)吃了"+i+"個包子");
}
}
}
插隊的話是百分百成功的,但是禮讓如果資源過剩的話,禮讓會不成功,例如上面資源不是特別缺乏,所以禮讓會不成功
常用方法第三組
用戶線程和守護線程
1、用戶線程:也叫工作線程,當線程的任務執(zhí)行完或通知方式結束
2、守護線程:一般是為工作線程服務的,當所有的用戶線程結束,守護線程自動結束
3、常見的守護線程:垃圾回收機制
自定義守護線程
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
//如果我們希望當main線程結束后,子線程自動結束
//只需將子線程設為守護線程即可
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for (int i =1;i<=10;i++){
System.out.println("媽媽做飯");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread{
@Override
public void run() {
for (;;){ //等價于無限循環(huán)
try {
Thread.sleep(50); //休眠50毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我吃飯。。。");
}
}
}
MyDaemonThread類的進程會在主線程進程結束后相繼結束
線程的生命周期
線程狀態(tài):線程可以處于一下狀態(tài)之一:
NEW
尚未啟動的線程處于此狀態(tài)
RUNNABLE
在Java虛擬機中執(zhí)行的線程處于此狀態(tài)
BLOCKED
被阻塞等待監(jiān)視器鎖定的線程處于此狀態(tài)
WAITING
正在等待另一個線程執(zhí)行特定動作的線程處于此狀態(tài)
TIMED_WAITING
正在等待另一個線程執(zhí)行動作達到指定等待時間的線程處于此狀態(tài)
TERMINATED
已退出的線程處于此狀態(tài)
RUNNABLE又可分為兩個狀態(tài):Ready狀態(tài):就緒狀態(tài) 和 Running運行狀態(tài)
線程同步機制
1、在多線程編程,一些敏感數據不允許被多個線程同時訪問,此時就是用同步訪問技術,保證數據在任何時刻,最多有一個線程訪問,以保證數據的完整性。
2、也可以這樣理解:線程同步,即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到線程完成操作,其他線程才能對該內存地址進行操作。
利用同步解決買票超賣問題
public class SellTicket {
public static void main(String[] args) {
//測試同步解決超賣現象
SellTicket03 sellTicket03 = new SellTicket03();
new Thread(sellTicket03).start();//第一個線程-窗口
new Thread(sellTicket03).start();//第二個線程-窗口
new Thread(sellTicket03).start();//第三個線程-窗口
}
}
//實現接口的方式,使用synchronized實現線程同步
class SellTicket03 implements Runnable{
private boolean loop=true;
private int ticketNum=99;
public synchronized void sell(){
if (ticketNum <= 0) {
System.out.println("售票結束");
loop=false;
return;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一張票" + "剩余票數=" + --ticketNum);
}
@Override
public void run() { //在同一時刻只能有一個線程來執(zhí)行sell方法
while(loop) {
sell(); //sell方法是一個同步方法
}
}
}
synchronized關鍵字為鎖的意思,如果有線程去調用了synchronized關鍵字修飾的方法,則不會去再有線程調用
synchronized的使用方法
- 修飾一個代碼塊,被修飾的代碼塊稱為同步代碼塊,作用范圍是大括號{}括起來的代碼;
- 修飾一個方法,被修飾的方法稱為同步方法,其作用范圍是整個方法;
- 修改一個靜態(tài)方法,作用范圍是整個靜態(tài)方法;
- 修改一個類,作用范圍是synchronized后面括號括起來的部分。
互斥鎖
基本介紹
1、Java語言中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。
2、每個對象都對應一個可稱為互斥鎖的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象
3、關鍵字synchronized來與對象的互斥鎖聯系。當某個對象用synchronized修飾時,表明該對象在任一時刻只能由一個線程訪問
4、同步的局限性:導致程序的執(zhí)行效率要降低
5、同步方法(非靜態(tài)的)的鎖可以是this,也可以是其他對象(要求是同一對象)
6、同步方法(靜態(tài)的)的鎖為當前類本身
同步方法靜態(tài)與非靜態(tài)實例
//實現接口的方式,使用synchronized實現線程同步
class SellTicket03 implements Runnable{
private boolean loop=true;
private int ticketNum=99;
Object object=new Object();
//同步方法(靜態(tài)的)的鎖為當前類本身
//1.public synchronized static void m1(){} 鎖是加在SellTicket03.class 類本身
//2.如果要在靜態(tài)方法中,實現一個同步代碼塊
//3.synchronized中的參數不能為this,要為類的class 類如:
/*public static void m2() {
synchronized (SellTicket03.class) {
System.out.println("m2");
}
}*/
public synchronized static void m1(){
}
public static void m2() {
synchronized (SellTicket03.class) {
System.out.println("m2");
}
}
//1、 public synchronized void sell(){}這是一個同步方法
//2、這時鎖在this對象
//3、也可以在代碼塊上寫synchronize , 同步代碼塊
public /*synchronized*/ void sell() {
synchronized (object) {
if (ticketNum <= 0) {
System.out.println("售票結束");
loop = false;
return;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一張票" + "剩余票數=" + --ticketNum);
}
}
@Override
public void run() { //在同一時刻只能有一個線程來執(zhí)行sell方法
while(loop) {
sell(); //sell方法是一個同步方法
}
}
}
注意事項和細節(jié)
1、同步方法如果沒有使用static修飾:默認鎖對象為this
2、如果方法使用static修飾,默認鎖對象:當前類.class
3、實現的落地步驟
- 需要先分析上鎖的代碼
- 選擇同步代碼塊或同步方法
- 要求多個線程的鎖對象為同一個即可!
線程死鎖
public class DeadLock_ {
public static void main(String[] args) {
//模擬一個死鎖現象
DeadLockDemo A=new DeadLockDemo(true);
DeadLockDemo B=new DeadLockDemo(false);
deadLockDemo1.start();
deadLockDemo2.start();
}
}
class DeadLockDemo extends Thread{
static Object o1 = new Object(); //保證多線程,共享一個對象,這里使用static
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag){ //構造器
this.flag = flag;
}
@Override
public void run() {
//下面的業(yè)務邏輯的分析
//1.如果flag為T , 線程就會先得到/持有 o1 對象鎖 , 然后嘗試去獲得 o2對象鎖
//2.如果線程A 得不到o2對象鎖,就會Blocked
//3.如果flag為F,線程B就會先得到/持有 o2 對象鎖,然后嘗試去獲取 o1 對象鎖
//4.如果線程B 得不到 o1 對象鎖,就會Blocked
if (flag){
synchronized (o1){ //對象互斥鎖,下面就是我們同步代碼
System.out.println(Thread.currentThread().getName() + "進入1");
synchronized (o2){ //這里獲得li對象的監(jiān)視權
System.out.println(Thread.currentThread().getName()+"進入2");
}
}
}else {
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"進入3");
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"進入4");
}
}
}
}
}
因為線程A會去搶線程B占著的對象,線程B也會去搶線程A占著的對象,所以會出現線程鎖死的現象,寫代碼的時候要避免這個錯誤
釋放鎖
下面操作會釋放鎖
1、當前線程的同步方法、同步代碼塊執(zhí)行結束
案例:上廁所,完事出來
2、當前線程在同步代碼塊、同步方法中遇到break、return
案例:沒有正常的完事,經理叫你去修改bug,不得已出來
3、當前線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束
案例:沒有正常的完事,發(fā)現忘記帶紙,不得已出來
4、當前線程在同步代碼塊、同步方法中執(zhí)行了線程對象的wait()方法,當前線程暫停,并釋放鎖。
案例:沒有正常完事,覺得需要醞釀下,所以出來等會在進去
下面操作不會釋放鎖
1、線程執(zhí)行同步代碼塊或同步方法時,程序調用了Thread.sleep()、Thread.yield()方法暫停當前線程的執(zhí)行,不會釋放鎖
案例:上廁所,太困了,在坑位上瞇了一會
2、線程執(zhí)行同步代碼塊時,其他線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放鎖
提示:應盡量避免使用suspend()和resume()來控制線程,方法不再推薦使用
練習題
一、
(1)在main方法中啟動兩個線程
(2)第一個線程循環(huán)隨機打印100以內的整數
(3)直到第二個線程從鍵盤上讀取了"Q"命令
通過線程守護解決
public class homeWork01 {
public static void main(String[] args) {
//創(chuàng)建線程B,并運行
B b=new B();
b.start();
}
}
class A extends Thread{
@Override
public void run() {
while (true){
try {
//休眠1秒運行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印隨機數
int num = (int)(Math.random()*100);
System.out.println(num);
}
}
}
class B extends Thread{
@Override
public void run() {
//創(chuàng)建A線程對象,并創(chuàng)建守護線程
A a=new A();
a.setDaemon(true);
a.start();
while (true) {
//當輸入Q的時候B線程結束,因為是守護線程,所以線程A也會跟著結束
System.out.println("請輸入你的指令");
Scanner sc = new Scanner(System.in);
String Z = sc.next();
System.out.println(Z);
if (Z.equals("Q")) {
System.out.println("B線程結束");
break;
}
}
}
}
通過通知方式解決
public class homeWork01 {
public static void main(String[] args) {
//創(chuàng)建線程A、B,并且執(zhí)行線程A、B
A a= new A();
B b=new B(a);
a.start();
b.start();
}
}
class A extends Thread{
private boolean loop=true;
//創(chuàng)建setLoop用來通知
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while (loop){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int num = (int)(Math.random()*100);
System.out.println(num);
}
}
}
class B extends Thread{
A a;
//通過B的構造器,傳入main中的線程A
public B(A a){
this.a=a;
}
@Override
public void run() {
while (true) {
System.out.println("請輸入你的指令");
Scanner sc = new Scanner(System.in);
String Z = sc.next();
System.out.println(Z);
if (Z.equals("Q")) {
//通過setLoop提醒線程A結束
a.setLoop(false);
break;
}
}
}
}
二、
(1)有兩個用戶分別從同一個卡上取錢(總額:10000)
(2)每次都取1000,當余額不足時,就不能取款了
(3)不能出現超取現象 —>線程同步問題
同步方法
public class homeWork02 {
public static void main(String[] args) {
C c=new C();
//將兩個線程運行
new Thread(c).start();
new Thread(c).start();
}
}
class C implements Runnable{
private static boolean loop=true;
private static int money=10000;
@Override
public void run() {
while (loop){
//讓兩個線程去搶同步方法
quMoney();
}
}
public synchronized void quMoney(){
if (money<=0){
System.out.println("余額不足,線程退出"+Thread.currentThread().getName());
loop=false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
money=money-1000;
System.out.println(Thread.currentThread().getName()+"從余額中取到了1000元還剩"+money+"元");
}
}
通過創(chuàng)建同步方法,避免超取現象
同步代碼塊
public class homeWork03 {
public static void main(String[] args) {
T t=new T();
Thread thread=new Thread(t);
Thread thread1=new Thread(t);
thread.setName("t1");
thread1.setName("t2");
thread.start();
thread1.start();
}
}
//編寫取款的線程
//因為這里涉及到多個程序共享資源,所以我們使用實現Runnable方式
class T implements Runnable{
private int money=10000;
@Override
public void run() {
while (true){
//解讀
//1.這里使用 synchronized 實現了線程同步
//2.當多個線程執(zhí)行到這里時,就會去爭奪 this 對象鎖
//3.那個線程獲取到了this鎖,就執(zhí)行 synchronized 代碼塊,執(zhí)行完后,會釋放this對象鎖
//4.獲取不到this對象鎖,就會blocked(阻塞),準備繼續(xù)爭奪
synchronized (this) {
if (money < 1000) {
System.out.println("余額不足");
break;
}
money -= 1000;
System.out.println(Thread.currentThread().getName() + "取出了1000 當前余額" + money);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ss T implements Runnable{
private int money=10000;
@Override
public void run() {
while (true){
//解讀
//1.這里使用 synchronized 實現了線程同步
//2.當多個線程執(zhí)行到這里時,就會去爭奪 this 對象鎖
//3.那個線程獲取到了this鎖,就執(zhí)行 synchronized 代碼塊,執(zhí)行完后,會釋放this對象鎖
//4.獲取不到this對象鎖,就會blocked(阻塞),準備繼續(xù)爭奪
synchronized (this) {
if (money < 1000) {
System.out.println("余額不足");
break;
}
money -= 1000;
System.out.println(Thread.currentThread().getName() + "取出了1000 當前余額" + money);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
總結
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關注腳本之家的更多內容!
相關文章
java.lang.Runtime.exec的左膀右臂:流輸入和流讀取詳解
這篇文章主要介紹了java.lang.Runtime.exec的左膀右臂:流輸入和流讀取詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
java?ResourceBundle讀取properties文件方式
這篇文章主要介紹了java?ResourceBundle讀取properties文件方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

