java中的線程:java中,每個線程都有一個調用棧存放在線程棧之中,一個java應用總是從main()函數開始運行,被稱為主線程。一旦創建一個新的線程,就會產生一個線程棧。線程總體分為:用戶線程和守護線程,當所有用戶線程執行完畢的時候,JVM自動關閉。
但是守候線程卻不獨立於JVM,守候線程一般是由操作系統或者用戶自己創建的。
線程的生命周期:當一個線程被創建之后,進入新建狀態,JVM則給他分配內存空間,並進行初始化操作。當線程對象調用了start()方法,該線程就處於就緒狀態(可執行狀態),JVM會為其創建方法調用棧、和程序計數器,處於可執行狀態下的線程隨時可以被cpu調度執行。
CPU執行該線程的時候,該線程進入執行狀態。執行過程中,該線程遇倒像wait()等待阻塞、以及synchronized鎖同步阻塞或者調用線程的sleep()方法等進入一個阻塞狀態,阻塞之后通過notify()或者notifyAll()方法喚醒重新獲取對象鎖之后再行進入就緒狀態,
等待cpu執行進去執行狀態、當線程執行完或者return則線程正常結束,如果發生處理的運行時異常,則線程因為異常而結束。這是一個線程的整個運行的生命周期。如下圖所示:
線程的幾種實現:
1、繼承Thread類,重寫該類的run方法
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Thread myThread1 = new MyThread(); // 創建一個新的線程 myThread1 此線程進入新建狀態
myThread1 .start(); // 調用start()方法使得線程進入可執行狀態
}
}
2、實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,並以此實例作為Thread類的target來創建Thread對象,該Thread對象才是真正的線程對象
class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable(); // 創建一個Runnable實現類的對象
Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target創建新的線程
thread1.start(); // 調用start()方法使得線程進入就緒狀態
}
}
}
3、使用Callable和Future接口創建線程。具體是創建Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作為Thread對象的target來創建線程
public class ThreadTest {
public static void main(String[] args) {
Callable<Integer> myCallable = new MyCallable(); // 創建MyCallable對象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread thread = new Thread(ft); //FutureTask對象作為Thread對象的target創建新的線程
thread.start(); //線程進入到就緒狀態
}
}
System.out.println("主線程for循環執行完畢..");
try {
int sum = ft.get(); //取得新創建的新線程中的call()方法返回的結果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
private int i = 0;
// 與run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
繼承Thread和實現Runnable接口實現多線程的區別:
繼承Thread類、實現Runnable接口,在程序開發中只要是多線程,肯定永遠以實現Runnable接口為主,因為實現Runnable接口相比繼承Thread類有如下優勢:
1、可以避免由於Java的單繼承特性而帶來的局限;
2、增強程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的;
3、適合多個相同程序代碼的線程區處理同一資源的情況。
線程的優先級別:
java線程可以通過setPriority()方法對其設定一個優先級別,高優先級別的線程比低優先級別的線程有更高的幾率得到先執行,優先級可以用0到10的整數表示,0為最低優先級別、10為最高優先級別。當線程調度器決定那個線程需要調度時,會根據這個優先級進行調度選擇;1)Thread類有三個優先級靜態常量:MAX_PRIORITY為10,為線程最高優先級;MIN_PRIORITY取值為1,為線程最低優先級;NORM_PRIORITY取值為5,為線程中間位置的優先級。默認情況下,線程的優先級為NORM_PRIORITY。2)一般來說優先級別高的線程先獲取cpu資源先運行,但特殊情況下由於現在的計算器都是多核多線程的配置,有可能優先級低的線程先執行,具體的執行還是看JVM調度來決定。
幾種線程同步的方法:
1、使用synchronized獲取對象互斥鎖:這種方式是最常用也比較安全的一種方式,采用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖叫做互斥鎖。每個對象都有一個monitor(鎖標記),當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個對象系統都會為其創建一個互斥鎖,這個鎖是為了分配給線程的,防止打斷原子操作。每個對象的鎖只能分配給一個線程,因此叫做互斥鎖。我們在使用同步的時候進來爸鎖的粒度控制的精細一點,有時候沒必要鎖整個方法,只需要鎖一個代碼塊即可達到我們的業務需求,這樣避免其他線程阻塞時間過長造成性能上的影響。
package per.thread;
import java.io.IOException;
public class Test {
private int i = 0;
private Object object = new Object();
public static void main(String[] args) throws IOException {
Test test = new Test();
Test.MyThread thread1 = test.new MyThread();
Test.MyThread thread2 = test.new MyThread();
thread1.start();
thread2.start();
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("線程"+Thread.currentThread().getName()+"進入睡眠狀態");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("線程"+Thread.currentThread().getName()+"睡眠結束");
i++;
System.out.println("i:"+i);
}
}
}
}
2、使用特殊域變量volatile實現線程同步:volatile修飾的變量是一種稍弱的同步機制,因為每個線程中的成員變量都會對這個對象的一個私有拷貝,每個線程獲取的數據都是從私有拷貝內存中獲取,而volatile修飾之后代表這個變量只能從共享內存中獲取,禁止私有拷貝。在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比synchronized關鍵字更輕量級的同步機制。從內存的可見性上來看,寫入volatile變量相當於退出同步代碼塊,而讀取volatile變量相當於進入同步代碼塊。但代碼中過度依賴於volatile變量來控制同步狀態,往往比使用鎖更加不安全,使用同步機制會更安全一些。當且僅當滿足以下所有條件時,才應該使用volatile變量: 1、對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。 2、該變量沒有包含在具有其他變量的不變式中。
class Bank {
//需要同步的變量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//這里不再需要synchronized
public void save(int money) {
account += money;
}
}
3、使用重入鎖Lock實現線程同步: 在jdk1.5以后java.util.concurrent.locks包下提供了這一種方式來實現同步訪問。因為synchronized同步之后會存在一個阻塞的過程,如果這個阻塞的時間過久,嚴重影響我們代碼的質量以及帶來系統性能上的問題。因為我們需要一種機制,讓等待的線程到達一定時間之后能夠響應中斷,這就是Lock的作用。另外lock還可以知道線程有沒有成功獲取到對象鎖,synchronized無法做到。Lock比synchronized提供更多的功能。但要注意的是:1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
package com.dylan.thread.ch2.c04.task;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class simulates a print queue
*
*/
public class PrintQueue {
/**
* 創建一個ReentrantLock實例
*/
private final Lock queueLock=new ReentrantLock();
/**
* Method that prints a document
* @param document document to print
*/
public void printJob(Object document){
//獲得鎖
queueLock.lock();
try {
Long duration=(long)(Math.random()*10000);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),(duration/1000));
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放鎖
queueLock.unlock();
}
}
}
注:關於Lock對象和synchronized關鍵字的選擇:
a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助用戶處理所有與鎖相關的代碼。
b.如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼
c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖
4、使用ThreadLocal管理變量:如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響;ThreadLocal 類的常用方法:
ThreadLocal() : 創建一個線程本地變量
get() : 返回此線程局部變量的當前線程副本中的值
initialValue() : 返回此線程局部變量的當前線程的"初始值"
set(T value) : 將此線程局部變量的當前線程副本中的值設置為value
public class Bank{
//使用ThreadLocal類管理共享變量account
private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
}
5、使用阻塞隊列實現線程同步:自從Java 1.5之后,在java.util.concurrent包下提供了若干個阻塞隊列,或Redis消息隊列等來實現同步等等。。。
java多線程並發的業務場景:在互聯網的大環境下很多場景都對並發要求越來越高,像天貓雙十一秒殺、春運火車票搶票、微信搶紅包、以及一些業務對某種資源的請求數量的控制、以及一些業務需要整個系統的輸入輸出順序一致性等問題,這些都需要考慮到並發導致的數據安全性問題。