傳統線程機制
一. 傳統使用類Thread和接口Runnable實現
Java中線程的創建有兩種方式:
① 通過繼承Thread類,重寫Thread的run()方法,將線程運行的邏輯放在其中。
② 通過實現Runnable接口,實例化Thread類。
⒈ 通過Thread類實現
⑴ 第一種寫法:繼承Thread類
// 繼承Thread類
private static class MyThread extends Thread
{
String name = null;
int ticket = 0;
public MyThread(String name)
{
this.name = name;
}
public synchronized void run()
{
for (int i = 0; i < 5; i++ )
{
System.out.println(
Thread.currentThread().getName() + this.name + " ticket:" + ticket++ );
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
// 測試
public static void main(String[] args)
{
MyThread mThread1 = new MyThread("線程一");
MyThread mThread2 = new MyThread("線程二");
mThread1.start();
mThread2.start();
}
⑵ 第二種寫法:在Thread子類重寫run()方法中編寫運行代碼
new Thread()
{
@Override
public void run()
{
while (true)
{
try
{
Thread.sleep(2000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}.start();
⒉ 通過接口Runnable類實現
⑴ 第一種寫法:實現Runnable接口
// 實現Thread類
private static class RunThread implements Runnable
{
int Counter = 0;
@Override
public synchronized void run()
{
for (int i = 0; i < 5; i++ )
{
System.out.println(Thread.currentThread().getName() + "count:" + Counter++ );
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
// 測試
public static void main(String[] args)
{
RunThread rThread = new RunThread();
Thread t1 = new Thread(rThread, "線程一");
Thread t2 = new Thread(rThread, "線程二");
t1.start();
t2.start();
}
⑵ 第二種寫法:在傳遞給Thread對象的Runnable對象的run()方法中編寫代碼
new Thread(new Runnable(){
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}).start();
二. 定實現時器 Timer 和 TimerTask
Timer 在實際開發中應用場景不多,一般來說都會用其他第三方庫來實現。但有時會在一些面試題中出現。下面我們就針對一道面試題來使用 Timer 定時類。
要求:模擬寫出雙重定時器。使用定時器,間隔 4 秒執行一次,再間隔 2 秒執行一次,以此類推執行。
package com.ithao.main;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TraditionalTimerTest
{
private static int count = 0;
public static void main(String[] args)
{
class MyTimerTask extends TimerTask
{
public void run()
{
count = (count + 1) % 2;
System.out.println("bombing!");
new Timer().schedule(new MyTimerTask(), 2000 + 2000 * count);
}
}
new Timer().schedule(new MyTimerTask(), 2000);
while (true)
{
System.out.println(new Date().getSeconds());
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
三. 線程互斥與同步
在引入多線程後,由於線程執行的異步性,會給系統造成混亂,特別是在急用臨界資源時,如多個線程急用同一臺打印機,會使打印結果交織在一起,難於區分。當多個線程急用共享變量,表格,鏈表時,可能會導致數據處理出錯,因此線程同步的主要任務是使併發執行的各線程之間能夠有效的共享資源和相互合作,從而使程序的執行具有可再現性。當線程併發執行時,由於資源共享和線程協作,使用線程之間會存在以下兩種制約關係。
間接相互制約。一個系統中的多個線程必然要共享某種系統資源,如共享 CPU,共享 I/O 設備,所謂間接相互制約即源於這種資源共享,打印機就是最好的例子,線程A在使用打印機時,其它線程都要等待。
直接相互制約。這種制約主要是因爲線程之間的合作,如有線程 A 將計算結果提供給線程 B 作進一步處理, 那麼線程 B 在線程 A 將數據送達之前都將處於阻塞狀態。
間接相互制約可以稱爲互斥,直接相互制約可以稱爲同步。對於互斥可以這樣理解,線程 A 和線程 B 互斥訪問某個資源則它們之間就會產個順序問題——要麼線程 A 等待線程 B 操作完畢,要麼線程 B 等待線程操作完畢,這其實就 是線程的同步了。因此同步包括互斥,互斥其實是一種特殊的同步。
package com.ithao.main;
/**
* 子線程運行執行10次後,主線程再運行5次。這樣交替執行三遍
*/
public class DemoTest
{
public static void main(String[] args)
{
final Business bussiness = new Business();
// 子線程
new Thread(new Runnable()
{
public void run()
{
for (int i = 0; i < 3; i++ )
{
bussiness.subMethod();
}
}
}).start();
// 主線程
for (int i = 0; i < 3; i++ )
{
bussiness.mainMethod();
}
}
}
// 這個類是執行任務的類
class Business
{
private boolean flag = false;
// flag爲true,主線程執行
public synchronized void mainMethod()
{
// 是不是子線程在執行?是,繼續等子線程執行完
while (false == flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 執行主線程任務
for (int i = 0; i < 5; i++ )
{
System.out.println(Thread.currentThread().getName() + i);
}
// 輪到子線程執行了
flag = false;
// 喚醒子線程。(一共就兩個線程,子線程和主線程,所以喚醒的只能是子線程。)
notify();
}
// flag爲false,子線程執行
public synchronized void subMethod()
{
// 是不是主線程在執行?是,繼續等主線程執行完
while (true == flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 執行子線程任務
for (int i = 0; i < 10; i++ )
{
System.out.println(Thread.currentThread().getName() + i);
}
// 輪到主線程執行了
flag = true;
// 喚醒主線程。(一共就兩個線程,子線程和主線程,所以喚醒的只能是主線程。)
notify();
}
}
四. 線程局部變量ThreadLocal
1. ThreadLocal 的作用和目的
用於實現線程內的數據共享,即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。
每個線程調用全局 ThreadLocal 對象的 set 方法,在 set 方法中,首先根據當前線程獲取當前線程的 ThreadLocalMap 對象,然後往這個 map 中插入一條記錄,key 其實是 ThreadLocal 對象,value 是各自的 set 方法傳進去的值。也就是每個線程其實都有一份自己獨享的 ThreadLocalMap 對象,該對象的 Key 是 ThreadLocal 對象,值是用戶設置的具體值。在線程結束時可以調用 ThreadLocal.remove()方法,這樣會更快釋放內存,不調用也可以,因爲線程結束後也可以自動釋放相關的 ThreadLocal 變量。
2. ThreadLocal 的應用場景
2.1. 訂單處理包含一系列操作
減少庫存量、增加一條流水臺賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個線程中進行處理,如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的數據庫連接對象,而這些操作的代碼分別位於不同的模塊類中。
2.2. 銀行轉賬包含一系列操作
把轉出帳戶的餘額減少,把轉入帳戶的餘額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的數據庫連接對象,轉入和轉出操作的代碼分別是兩個不同的帳戶對象的方法。
2.3. 例如 Strut2 的 ActionContext
同一段代碼被不同的線程調用運行時,該代碼操作的數據是每個線程各自的狀態和數據,對於不同的線程來說,getContext 方法拿到的對象都不相同,對同一個線程來說,不管調用 getContext 方法多少次和在哪個模塊中 getContext 方法,拿到的都是同一 個。
3. ThreadLocal 的使用方式
ThreadLocal用於保存某個線程共享變量:對於同一個static ThreadLocal,不同線程只能從中get,set,remove自己的變量,而不會影響其他線程的變量。
3.1. 在關聯數據類中創建 private static ThreadLocal
在下面的類中,私有靜態 ThreadLocal 實例(serialNum)爲調用該類的靜態 SerialNum.get() 方法的每個線程維護了一個“序列號”,該方法將返回當前線程的序列號。(線程的序列號是在第一次調用 SerialNum.get() 時分配的,並在後續調用中不會更改。)
package com.ithao.main;
public class SerialNum {
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
其他案例:
public class ThreadContext {
private String userId;
private Long transactionId;
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected ThreadContext initialValue() {
return new ThreadContext();
}
};
public static ThreadContext get() {
return threadLocal.get();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Long getTransactionId() {
return transactionId;
}
public void setTransactionId(Long transactionId) {
this.transactionId = transactionId;
}
}
3.2. 在Util類中創建ThreadLocal
這是上面用法的擴展,即把ThreadLocal的創建放到工具類中。
【例】例如Hibernate的工具類:
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定義SessionFactory
static {
try {
// 通過默認配置文件hibernate.cfg.xml創建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失敗!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//創建線程局部變量session,用來保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 獲取當前線程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session還沒有打開,則新開一個Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //將新開的Session保存到線程局部變量中
}
return s;
}
public static void closeSession() throws HibernateException {
//獲取線程局部變量,並強制轉換爲Session類型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
3.3. 在Runnable中創建ThreadLocal
還有一種用法是在線程類內部創建ThreadLocal,基本步驟如下:
1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象爲null時候,應該new()一個隔離訪問類型的對象,並強制轉換爲要應用的類型。
3、在ThreadDemo類的run()方法中,通過調用getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
public class ThreadLocalTest implements Runnable{
ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Studen studen = getStudent(); //通過這個方法,爲每個線程都獨立的new一個student對象,每個線程的的student對象都可以設置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent() {
Studen studen = studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Studen{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
五. 多線程共享數據
在 Java 傳統線程機制中的共享數據方式,大致可以簡單分兩種情況:
多個線程行爲一致,共同操作一個數據源。也就是每個線程執行的代碼相同,可以使用同一個 Runnable對象,這個Runnable 對象中有那個共享數據,例如,賣票系統就可以這麼做。
多個線程行爲不一致,共同操作一個數據源。也就是每個線程執行的代碼不同,這時候需要用不同的Runnable 對象。例如,銀行存取款。
1. 多個線程行爲一致共同操作一個數據
如果每個線程執行的代碼相同,可以使用同一個 Runnable 對象,這個 Runnable 對象中有那個共享數據。
package com.ithao.main;
public class SellTickets {
public static void main(String[] args) {
ShareData shareData = new ShareData();//共享的數據對象
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData), "Thread " + i).start();
}
}
}
/**
* 共享數據類
**/
class ShareData {
private int num = 10;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()
+ ": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
*多線程類
**/
class RunnableCusToInc implements Runnable {
private ShareData shareData;//共享的數據對象
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
2. 多個線程行爲不一致共同操作一個數據
如果每個線程執行的代碼不同,這時候需要用不同的 Runnable 對象,有如下兩種方式來實現這些 Runnable對象之間的數據共享:
2.1. 將共享數據封裝在另外一個對象中
然後將這個對象逐一傳遞給各個 Runnable 對象。每個線程對共享數據的操作方法也分配到那個對象身上去完成,這樣容易實現針對該數據進行的各個操作的互斥和通信。
package com.ithao.main;
public class BankWithdrawals {
public static void main(String[] args) {
ShareData2 shareData = new ShareData2();
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
new Thread(new RunnableCusToInc2(shareData), "Thread " + i).start();
} else {
new Thread(new RunnableCusToInc3(shareData), "Thread " + i).start();
}
}
}
}
/**
* 共享數據類
**/
class ShareData2 {
private int num = 10;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()
+ ": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 多線程類
**/
class RunnableCusToInc2 implements Runnable {
private ShareData2 shareData;
public RunnableCusToInc2(ShareData2 data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
class RunnableCusToInc3 implements Runnable {
private ShareData2 shareData;
public RunnableCusToInc3(ShareData2 data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
System.out.println("33333333333333333333333");
}
}
}
2.2. 將這些 Runnable 對象作爲某一個類中的內部類
共享數據作爲這個外部類中的成員變量,每個線程對共享數據的操作方法也分配給外部類,以便實現對共享數據進行的各個操作的互斥和通信,作爲內部類的各個Runnable 對象調用外部類的這些方法。
package com.ithao.main;
public class BankWithdrawals {
public static void main(String[] args) {
// 公共數據
final ShareData shareData=new ShareData();
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}, "Thread " + i).start();
} else {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
shareData.dec();
}
}
}, "Thread " + i).start();
}
}
}
static class ShareData {
private int num = 10;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()
+ ": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void dec() {
num--;
System.err.println(Thread.currentThread().getName()
+ ": invoke dec method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
補充:上面兩種方式的組合:將共享數據封裝在另外一個對象中,每個線程對共享數據的操作方法也分配到那個對象身上去完成,對象作爲這個外部類中的成員變量或方法中的局部變量,每個線程的 Runnable 對象 作爲外部類中的成員內部類或局部內部類。
總之,要同步互斥的幾段代碼最好是分別放在幾個獨立的方法中,這些方法再放在同一個類中,這樣比較容 易實現它們之間的同步互斥和通信。