Java多線程
進程:
是一個正在運行的程序,比如正在運行的迅雷,QQ等。
線程:
每個進程執行都有一個執行順序,該順序是一個執行路徑,或者叫做一個控制單元,線程就是進程中的一個獨立的控制單元;線程控制着進程的運行;一個進程中至少有一個線程。Java虛擬機允許應用程序併發地運行多個執行線程。
比如:JVM啓動時會運行一個進程java.exe該進程中至少有一個線程負責java程序的執行,並且這個線程運行的代碼存在於main方法中,該線程成爲主線程。除了主線程外,還有負責垃圾回收機制的進程。
class Test {
public static void main(String[] args) // 主線程
{
for (int i = 0; i < 4000; i++) {
System.out.println("Hello World!");
}
}
}
創建線程
創建新執行線程有兩種方法:
方法一:將類聲明爲Thread
的子類。該子類應重寫Thread
類的run
方法。接下來可以分配並啓動該子類的實例。
class Demo extends Thread // 定義子類繼承Thread
{
public void run() // 複寫run()方法,存儲自定義代碼
{
for (int i = 0; i < 30; i++) {
System.out.println("Demo run" + i);
}
}
}
class ThreadDemo {
public static void main(String[] arge) {
Demo d = new Demo(); // 創建子類對象,創建一個線程
d.start(); // 調用start()方法:啓動線程;調用run()方法
d.run(); // 僅僅是對象調用方法,沒有開啓新的線程
for (int i = 0; i < 30; i++) {
System.out.println("Hello World" + i);
}
}
}
方法二:聲明實現Runnable
接口的類。該類然後實現run
方法。然後可以分配該類的實例,在創建Thread
時作爲一個參數來傳遞並啓動。
-
定義類實現Runnable接口
-
覆蓋Runnable接口中run()方法,將線程要運行的代碼存放在該run方法中
-
通過Thread類建立線程對象
-
將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數;因爲自定義的run()方法所屬對象是Runnable接口的子類對象,而又要線程去執行指定對象的run()方法,所以要明確該run()方法所屬的對象
-
調用Thread類的start()方法開啓線程並調用Runnable接口子類的run()方法
//定義類實現Runnable接口
class Ticket implements Runnable {
private int ticket = 100;
public void run() { // 覆蓋Runnable接口中的run()方法
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ " sale..." + ticket--);
}
}
}
}
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket t = new Ticket();
// 創建線程
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
// 開啓線程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
兩種創建線程的方式的區別:
第一種方式:直接繼承Thread類創建對象,線程代碼存放在Thread子類的
run()方法中。Thread子類無法再從其它類繼承(java語言單繼承);編寫簡
單,run()方法的當前對象就是線程對象,可直接操作。
第二種方式:使用Runnable接口創建線程,線程代碼存放在Runnable接口
實現的子類的run()方法中。可以將CPU,代碼和數據分開,形成清晰的模
型;線程體run()方法所在的類可以從其它類中繼承一些有用的屬性和方法;
有利於保持程序的設計風格一致
在實際應用中,幾乎都採用第二種方式,第二種能避免單繼承的侷限性
*從運行的結果中可以發現:
發現運行結果每次都不一樣,因爲多個線程都在獲取cpu的執行使用權,cpu
執行到誰,誰就運行。在某一時刻,cpu(多核除外)只能有一個程序在運
行,由於cpu在做着快速的切換,一大到看上去是在同時運行的效果。可以
得到多線程的一個特性:隨機性。
爲什麼要覆蓋run()方法呢?
Run()方法:用於存儲要運行的代碼,
Start()方法:用於開啓線程
線程的四種狀態
獲取線程對象及名稱
線程都有自己默認的名稱名稱的格式:Thread-編號編號從0開始
1、currentThread():靜態方法,用於獲取當前正在執行的線程對象的引用
2、GetName():用戶與獲取當前線程對象的名稱
3、currentThread().getName()和this.getName()作用相同,都用於獲取當前線程對象的名稱
4、setName():用於給當前線程對象設置名稱或者調用Thread類的構造方法設置當前線程對象的名稱
class Demo extends Thread {
public Demo(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 10; i++) {
// 通過調用this.getName()方法獲取該線程對象的名稱
System.out.println(this.getName() + " run…" + i);
// 通過靜態方法currentThread()方法獲取當前線程對象
// 通過currentThread.getName()方法獲取當前線程對象名稱
System.out.println(Thread.currentThread().getName() + " run…" + i);
}
}
}
class ThreadTest {
public static void main(String[] args) {
Demo t1 = new Demo("線程1");
Demo t2 = new Demo("線程2");
t1.start();
t2.start();
}
}
多線程的安全問題
class Ticket implements Runnable {
private int ticket = 100;
public void run() { // 覆蓋Runnable接口中的run()方法
while (true) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()
+ " sale..." + ticket--);
}
}
}
}
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket t = new Ticket();
// 創建線程
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
// 開啓線程
t1.start();
t2.start();
t3.start();
t4.start();
}
}//運行結果出現了0,-1,-2等的錯票,多線程出現了安全問題
問題原因:
當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行,導致共享數據錯誤。
解決辦法:
對多條操作共享數據的語句,只能讓一個線程執行完,在執行過程中,其
他線程不可以參與執行。
Java對於多線程的安全問題提供了專業的解決方案:同步代碼快
Synchronized(對象){
需要同步的代碼
}
代碼應修改爲:
class Ticket implements Runnable {
private int ticket = 100;
public void run() { // 覆蓋Runnable接口中的run()方法
Object obj = new Object();
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()
+ " sale..." + ticket--);
}
}
}
}
}
多線程同步代碼塊
Synchronized(對象){
需要同步的代碼
}
對象如同鎖,持有鎖的線程可以在同步中執行,沒有持有鎖的線程即使擁有cpu
的執行權,也進不去,因爲沒有持有鎖。
同步的前提:
-
必須有兩個或有兩個以上的線程
-
必須是多個線程使用同一個鎖
好處:解決了多線程的安全問題
弊端:多個線程需要判斷鎖,較爲消耗資源
多線程同步函數
Public synchronized 返回值類型方法阿明(){方法體}
class Bank {
private int sum;
public synchronized void add(int n) {
sum = sum + n;
System.out.println("sum=" + sum);
}
}
class Cus implements Runnable {
private Bank b = new Bank();
public void run() {
for (int i = 0; i < 3; i++) {
b.add(100);
}
}
}
class BankDemo {
public static void main(String[] args) {
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
同步函數用的鎖是this
因爲函數需要被對象調用,那麼函數都有一個所屬的對象引用,就是this
驗證同步函數用的鎖是this:
思路:使用兩個線程來買票,一個線程在執行同步代碼快中所用的鎖是obj,一個線程在執行同步函數中,兩個線程都在執行買票動作。
class Ticket implements Runnable {
private int tick = 100;
Object obj = new Object();
public Boolean f = true; // 用於區分第一個線程執行同步代碼塊,第二
// 個線程在執行同步函數
public void run() {
if (f) {
while (true) {
synchronized (obj) {
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()
+ "…code " + tick--);
}
}
}
} else {
while (true)
show();
}
}
public synchronized void show() {
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "…show…"
+ tick--);
}
}
}
class ThisLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (Exception e) {
}
t.f = false;
t2.start();
}
}
運行結果出現了0號票,說明程序不安全,兩個線程所用的鎖不是同一個鎖
而把同步代碼塊中對象參數改爲’this’,則運行結果沒有出現不符合的票,說明
兩個線程用的是同一個鎖,所以同步函數所用的鎖是this
靜態同步函數所用的鎖是該方法所在類字節碼文件對象,即類名.class
如果把上面代碼中的同步函數用static修飾,則運行結果又會出現0號票,說
明靜態同步函數所用的鎖不是this,而把同步代碼塊中的對象參數改
爲’Ticket.class’後,運行結果沒有出現不合法的票,所以說明兩個線程用的鎖是
同一個鎖,所以靜態同步函數所用的鎖是本類類名.class