Java並發編程之美之並發編程線程基礎
什麽是線程
進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,線程則是進程的一個執行路徑,一個進程至少有一個線程,進程的多個線程共享進程的資源。
java啟動main函數其實就是啟動了一個JVM的進程,而main函數所在的線程就是這個進程的一個線程,也稱主線程。
進程和線程關系
一個進程有多個線程,多個線程共享進程的堆和方法區資源,但是每個線程有自己的程序計數器和棧區域。
程序計數器是一塊內存區域,用來記錄線程當前要執行的指令地址。如果執行的是native方法,那麽pc計數器記錄的是undefined地址,只有執行java代碼時pc計數器記錄的才是下一條指令的地址;
進程的棧資源存儲該線程的局部變量,局部變量是該線程私有的,其它線程訪問不了,除此之外棧還用來存放線程的調用棧幀;
堆是進程中最大的一塊內存,堆是被進程中的所有線程共享的,是進程創建時分配的,堆裏面存放使用new操作創建的對象實例。
方法區則用來存放JVM加載的類,常量及靜態變量等信息,也是線程共享的。
線程創建和運行
創建方式:繼承Thread類 實現Runnable接口,使用FutureTask方式
1.繼承Thread類
public class ThreadTest {
public static class MyThread extends Thread{
@Override
public void run(){
System.out.println(" I am a child thread!") ;
}
public static void main(String[] args){
MyThread t = new MyThread();
t.start();
}
}
}
2.實現Runnable接口
public class RunnableTest {
public static class MyRunnable implements Runnable
{
@Override
public void run() {
System.out.println("I am a child thread!");
}
}
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
}
}
3.使用FutureTask
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
return true;
}
public static void main(String[] args) {
FutureTask<Boolean> f = new FutureTask<>(new CallableTest());
new Thread(f).start();
try{
Boolean r = f.get();
System.out.println(r);
}catch(Exception e)
{
e.printStackTrace();
}
}
}
小結:
使用繼承方式的好處是方便傳參,你可以在子類裏面添加成員變量,通過set方法設置參數或者構造函數傳遞參數,而如果使用Runnable方式,則只能使用主線程裏面被聲明為final的變量。不好的地方是Java不支持多繼承,如果繼承了Thread,那麽子類不能再繼承其它類,而Runnale則沒有這個限制。前兩種方式都沒辦法拿到任務的返回值,但是Futuretask方式可以。
線程通知與等待
java中的Object類是所有類的父類,鑒於繼承機制,java把所有類都需要的方法放到了Object類裏面,其中就包含通知與等待系列函數。
wait()函數
當一個函數調用共享變量的wait()方法時,該線程會被阻塞掛起,直到發生下面幾件事情之一才返回:1)其它線程調用了該共享對象的notify()或者notifyAll()方法;2)其它線程調用了該線程的interrupt()方法,該線程拋出InterruptedException異常返回;
如果調用wait()方法的線程事先沒有獲取該對象的監視器鎖,會拋出IllegalMonitorStateException異常;
獲取共享變量的監視器鎖
synchronized(共享變量){doSomething}
synchronized void add(int a,int b){doSomething}
虛假喚醒
一個線程可以從掛起狀態變為可以運行狀態(也就是被喚醒),即時該線程沒有被其它線程調用notify(),notifyAll()方法進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒。
生產者和消費者
/**
* 容器
* @author pc
*
*/
public class Box {
public final int MAX_SIZE = 10;
public LinkedList<Integer> box = new LinkedList<>();
public void add() throws Exception
{
while(true)
{
synchronized(box)
{
while(box.size() == MAX_SIZE)
{
box.wait();
}
box.push(1);
System.out.println("add ele and notifyall:"+box.size());
box.notifyAll();
}
}
}
public void remove() throws Exception
{
while(true)
{
synchronized(box)
{
while(box.size() == 0)
{
box.wait();
}
box.pop();
System.out.println("remove ele and notifyall:"+box.size());
box.notifyAll();
}
}
}
}
/**
* 生產者和消費者
* @author pc
*
*/
public class Program {
public Box box;
public Program(Box box) {
super();
this.box = box;
}
public class Consumer implements Runnable
{
@Override
public void run() {
try {
box.remove();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Producer implements Runnable
{
@Override
public void run() {
try {
box.add();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Program p = new Program(new Box());
Program.Consumer c = p.new Consumer();
Program.Producer pd = p.new Producer();
Thread t1 = new Thread(c);
Thread t2 = new Thread(pd);
t1.start();
t2.start();
}
}
當前線程調用共享變量的wait()方法後只會釋放當前共享變量上的鎖,如果當前線程還持有其它共享變量的鎖,則這些鎖時不會被釋放的。
public class Test {
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 try get resourceA lock...");
synchronized(resourceA)
{
System.out.println("t1 get resourceA lock...");
System.out.println("t1 try get resourceB lock...");
synchronized(resourceB)
{
System.out.println("t1 get resourceB lock...");
System.out.println("t1 unlock resourceA...");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);//等待1s保證t1將resourceA釋放出來
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 try get resourceA lock...");
synchronized(resourceA)
{
System.out.println("t2 get resourceA lock...");
System.out.println("t2 try get resourceB lock...");
synchronized(resourceB)
{
System.out.println("t2 get resourceB lock...");
System.out.println("t2 unlock resourceA...");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("main thread is over");
}
}
當一個線程調用共享對象的wait()方法被阻塞掛起後,如果其它線程中斷了該線程,則該線程會拋出InterruptedException異常並返回
public class InterruptedExceptionTest {
private static volatile Object resouce = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(resouce)
{
try {
resouce.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
System.out.println(" start interupt==");
t1.interrupt();
System.out.println(" end interupt==");
System.out.println("main is over");
}
}
wait(long timeout)函數
該方法相比wait()函數多了一個超時參數,如果線程調用共享變量過程中,沒有在指定的timeout時間內被其它線程調用該共享變量的notify()或者notifyAll()方法喚醒,那麽該函數還是會因為超時返回
timeout等於0就相當於wait()函數
timeout小於0會報錯IllegalArgumentException異常
public class WaitTimeOutTest {
private static Object resource = new Object();
public static void main(String[] args)throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(resource)
{
System.out.println("start wait()"+System.currentTimeMillis()/1000);
try {
resource.wait(1000L);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("end wait()"+System.currentTimeMillis()/1000);
}
}
});
t1.start();
t1.join();
}
wait(long timeout,int nanos)
內部調用的時wait(long timeout),只有在nanos>0時才使參數timeout遞增1;
notify()函數
一個線程調用共享變量的notify()方法後,會喚醒一個在該共享變量上調用wait系列方法後被掛起的線程。一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程時隨機的。
被喚醒的線程不會立即返回,需要先獲取共享變量的鎖。
類似wait系列方法,只有當前線程獲取到共享變量的監視器鎖之後,才可以調用共享變量的notify()方法,否則會拋出IllegalMonitorStateException異常。
notifyAll()函數
喚醒所有在該共享變量上由於調用wait系列方法而被掛起的線程
如果調用notifyAll()之後有線程調用了wait()方法,則該線程不能被喚醒。
public class NotifyTest {
private static Object resource = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(resource)
{
System.out.println("t1 start wait()");
try {
resource.wait();
System.out.println("t1 end wait()");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(resource)
{
System.out.println("t2 start wait()");
try {
resource.wait();
System.out.println("t2 end wait()");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}//等待1s
synchronized(resource)
{
System.out.println("t3 start notify()");
resource.notify();
//resource.notifyAll();
}
}
});
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
等待線程執行終止的join方法
ublic class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 end");
}
});
t1.start();t2.start();
System.out.println("wait t1,t2 over");
t1.join();t2.join();
}
}
讓線程睡眠的sleep方法
Thread類中的一個靜態sleep方法,當一個執行中的線程調用Thread的sleep方法後,調用線程會暫時讓出指定時間的使用權,也就是不參數與CPU調度,但是該線程擁有的監視器資源,比如鎖還是持有不讓出的。指定時間結束後該函數會正常返回。
如果在睡眠期間調用了該線程的interrupt()方法中斷了該線程,則線程會在調用sleep方法的地方拋出InterruptedException異常而返回。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SleepTest {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try
{
lock.lock();
System.out.println("t1 start sleep");
Thread.sleep(1000);
System.out.println("t1 end sleep");
}catch(Exception e)
{
e.printStackTrace();
}finally
{
lock.unlock();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try
{
lock.lock();
System.out.println("t2 start sleep");
Thread.sleep(1000);
System.out.println("t2 end sleep");
}catch(Exception e)
{
e.printStackTrace();
}finally
{
lock.unlock();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
輸出結果一直是t1或t2結束其中之一結束之後才開始執行下一線程,證明線程睡眠期間不會釋放鎖;
讓出CPU執行權的yield方法
Thread類中的靜態yield方法,當一個線程調用yield方法時,其實就是在暗示線程調度器當前線程請求讓出自己的CPU使用,但是線程調度器可以無條件忽略這個暗示。
當一個線程調用yield方法時,當前線程會讓出CPU使用權,然後處於就緒狀態,線程調度器會從線程就緒隊列裏面獲取一個線程優先級高的線程,當然也有可能調度到剛剛讓出CPU的線程;
線程中斷
java中線程中斷是一種線程間的協作模式,通過設置線程的中斷標誌並不能直接終止該線程的執行,而是被中斷的線程根據中斷狀態自行處理
void interrupt():中斷線程
boolean isInterrupted():檢測當前線程是否被中斷
boolean interrupted():檢測當前線程是否被中斷,中斷返回true,並清除中斷標誌
線程死鎖
死鎖是指兩個或兩個以上的線程執行過程中,因爭奪資源而做成的互相等待的現象。無外力作用的情況下,這些線程會一直互相等待下去而無法繼續運行下去
死鎖出現的條件
1)互斥條件:線程堆已經獲取的資源進行排他性使用,即該資源同時只能有一條線程占用。如果此時還有其它線程請求獲取該資源,請求者只能等待,直至占有的線程釋放該資源;
2)請求並持有條件:指一個線程已經持有了至少一個資源,但是又提出了新的資源請求,而新資源被其它線程占有,所以當前線程會被阻塞,但阻塞的同事並不釋放自己已經獲取的資源。
3)不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其它線程搶占,只有在自己使用完畢後才由自己釋放資源
4)環路等待條件:發生死鎖時,必然存在一個線程-資源環形鏈,即線程集合{T0,T1,T2,...Tn}中T0等待T1的資源,T1等待T2的資源,...Tn正在等待T0占用的資源
public class DeadThreadTest {
private volatile static Object resourceA = new Object();
private volatile static Object resourceB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 try get resourceA lock...");
synchronized(resourceA)
{
System.out.println("t1 get resourceA lock...");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 try get resourceB lock...");
synchronized(resourceB)
{
System.out.println("t1 get resourceB lock...");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t2 try get resourceB lock...");
synchronized(resourceB)
{
System.out.println("t2 get resourceB lock...");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 try get resourceA lock...");
synchronized(resourceA)
{
System.out.println("t2 get resourceA lock...");
}
}
}
});
t1.start();
t2.start();
}
}
輸出:
t2 try get resourceB lock...
t1 try get resourceA lock...
t1 get resourceA lock...
t2 get resourceB lock...
t1 try get resourceB lock...
t2 try get resourceA lock...
如何避免死鎖的發生
1)破壞掉至少一個構造死鎖的必要條件即可
2)使用資源的有序性原則
守護線程與用戶線程
線程分類:守護線程【daemon】,用戶線程【user】
main函數就是用戶線程
當最後一個用戶線程結束時,JVM會正常退出,只要有一個用戶線程沒有結束,正常情況下JVM就不會退出
public class DamonThreadTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(;;)
{
}
}
});
t1.setDaemon(false);
t1.start();
System.out.println("main is over");
}
ThreadLocal
多線程訪問同一共享變量時特別容易出現並發問題,特別時在多個線程需要對一個共享變量進行寫入時,為了保證線程安全,一般使用者在訪問共享變量時需要進行適當的同步
同步措施一般是加鎖,這就需要使用者對鎖有一定的了解
ThreadLocal提供了線程本地變量,也就是如果你創建了一個ThreadLocal變量,那麽訪問這個變量的每個線程都會有這個變量的一個本地副本。
public class ThreadLocalTest {
private static ThreadLocal<String> local = new ThreadLocal<>();
public static void print(String name)
{
System.out.println(name+"--"+local.get());
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
local.set("1234");
print(Thread.currentThread().getName());
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
local.set("5678");
print(Thread.currentThread().getName());
}
},"t2");
t1.start();
t2.start();
}
}
output:
t1--1234
t2--5678
線程上下文切換
多線程編程中,線程個數一般都大於CPU個數,而每個CPU同一時刻只能被一個線程使用,為了讓用戶感覺多個線程在同時運行,CPU采用采用了時間片輪轉的策略,也就是給每個線程分配一個時間片,線程在時間片內占用CPU執行任務,
當前線程使用完時間片後,就會處於就緒狀態並讓出CPU讓其它線程占用,這就是上下文切換。
切換時機:1)當前線程使用完CPU分配的時間片處於就緒狀態 2)當前線程被其它線程中斷
Java並發編程之美之並發編程線程基礎