目錄
線程的同步方式(也叫同步鎖)
- 方式一: synchronized 代碼塊
synchronized(資源對象){// 實現鎖的對象資源
// 需要統一執行的原子資源
// 這段內容是不可分割的
}
- synchronized後面的資源對象,最好是線程需要競爭的唯一的
- 內部代碼塊中執行的內容是統一整體當結束代碼塊時候鎖被釋放掉
public class TestSynchronized{
public static void main(Stirng[] args){
ThreadOne one = new ThreadOne();
Thread th = new Thread(one);
Thread th1 = new Thread(one);
th.start();
th.start();
}
}
// 資源
class ThreadOne implements Runnable{
public void run(){
// 同步代碼塊上鎖
synchronized(this){
for(int i=1;i < 100;i++){
System.out.print(Thread.currentThread().getName()+":"+i);
}
}
}
}
- 方式二: 同步方法
synchronized 返回值類型 方法名稱(形參列表0){
// 當前對象(this)加鎖
}
-
只有擁有對象互斥鎖標記的線程,才能進入該對象的同步方法中
-
線程退出同步方法時,會釋放相應的互斥鎖標記
-
已知的java jdk內庫中線程安全類有:StringBuffer、Vector、Hashtable、這幾個類中的公開方法,均爲synchronized修飾的方法
public class TestSynchronized{
public static void main(Stirng[] args){
ThreadOne one = new ThreadOne();
Thread th = new Thread(one);
Thread th1 = new Thread(one);
th.start();
th.start();
}
}
// 資源
class ThreadOne implements Runnable{
public void run(){
start();
}
// 方法的同步修飾
public synchronized void start(){
for(int i=1;i < 100;i++){
System.out.print(Thread.currentThread().getName()+":"+i);
}
}
}
同步規則
- 只有調用同步代碼塊的方法,或者同步方法時,才需要對象的鎖標記
- 如調用不包含同步代碼塊的方法,或普通方法時,則不需要鎖標記,可直接調用
加鎖的場景
- 寫(增、刪、改)操作時候加鎖
- 讀操作時候,不加鎖
死鎖
- 當第一個線程擁有對象A的鎖標記,並且等待B對象的鎖標記,同時第二個線程擁有B對象鎖標記,並等待A對象鎖標記時,產生死鎖問題。
- 一個線程可以同時擁有多個對象的鎖標記,當線程阻塞時,不會釋放已經擁有餓鎖標記,由此可能造成死鎖問題。
常見的死鎖具體問題
- 生產者與消費者問題
若干個生產者在生產產品,這些產品將提供給若干個消費者去消費,爲了使生產者和消費者能併發執行,在兩者之間設置一個能存儲多個產品的緩衝區,生產者將生產的產品放入緩衝區,消費者從緩衝區中取走產品進行消費,顯然生產者和消費者之間必須保持同步,即不允許消費者到一個空的緩衝區取走產品,也不允許生產者向一個滿的緩衝區中放入產品
線程通信
線程通信時解決思索地有效方法
- 等待
- public final void wait()
- public final void wait(long timeout)
- 必須在對obj加鎖的同步代碼塊中。在一個線程中,調用obj.wait()時,此線程會釋放其擁有的所有鎖標記。同時此線程阻塞在o的等待隊列中。釋放鎖,進入等待隊列。
- 通知
- public finall void notify()
- public final void notifyAll()
- 必須在對obj加鎖的同步代碼塊中。從obj的Waiting中釋放一個或全部線程。對自身沒有任何影響
以下是個簡單的案例
也就是李敢敢(丈夫)與趙巖巖(妻子) 銀行卡子母卡同時取錢的案例
public class TestWaitNotify {
public static void main(String[] args) {
//臨界資源,被共享的對象
//臨界資源對象只有一把鎖
Account acc =new Account("6002","1234",2000);
//兩個線程對象 共享同一銀行卡資源對象。
//給定任務後,第二個參數是對線程自定義命名
Thread husband = new Thread(new Husband(acc),"丈夫");
Thread wife = new Thread(new Wife(acc),"妻子");
// 啓動線程
husband.start();
wife.start();
}
}
class A extends Thread{
}
// 模擬現實世界的子母卡
class Husband implements Runnable{
Account acc;
public Husband(Account acc) {
this.acc = acc;
}
public void run() {
this.acc.withdrawal("6002","1234",1200);//整體式原子操作
}
}
class Wife implements Runnable{
Account acc;
public Wife(Account acc) {
this.acc = acc;
}
public void run() {
this.acc.withdrawal("6002","1234",1200);//整體式原子操作
}
}
class Account{
String cardNO;// 卡號
String password;// 密碼
double balance; // 餘額
public Account(String cardNO, String password, double balance) {
super();
this.cardNO = cardNO;
this.password = password;
this.balance = balance;
}
// 取款操作(整體是個原子操作,從插卡開始驗證,到取款成功的一系列步驟,不可以缺少或者打亂)
public synchronized void withdrawal(String no,String pwd,double money) {
// 等待! --->阻塞狀態
System.out.println(Thread.currentThread().getName()+"正在讀卡...");
if(no.equals(this.cardNO) && pwd.equals(this.password)) {
System.out.println(Thread.currentThread().getName()+"驗證成功...");
if(money <= this.balance) {
try {
Thread.sleep(1000);//模擬現實世界ATM機器查錢
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance - money;
System.out.println(Thread.currentThread().getName()+"取款成功!當前餘額爲:"+this.balance);
}else {
System.out.println(Thread.currentThread().getName()+"當前卡內餘額不足!");
}
}else {
System.out.println(Thread.currentThread().getName()+"卡號或密碼錯誤!");
}
}
}
高級多線程
線程池的原理
- 現有問題:
- 線程是寶貴的內存資源、單個線程約佔1MB空間,過多分配易造成內存溢出
- 頻繁的創建及銷燬線程會增加虛擬機的回收頻率、資源開銷、造成程序性能下降
所以就引入線程池來解決這個問題
- 線程池:
- 線程容器,可設定線程分配的數量上限
- 將預先創建的線程對象存入池中,並重用線程池中的線程對象
- 避免頻繁的創建和銷燬
將任務提交給線程池,有線程池分配線程、運行任務,並在當前任務結束後服用線程
- java.util.concurrent 所有的線程池類的祖先
併發編程中很常用的實用工具類
- Executor: 線程池的頂級接口
- ExecutorService:線程池接口(常用的更豐富也是個接口),可通過submit()(提交任務代碼)提交一個Runnable任務用於執行,並返回一個標識該任務的Future(結果)
- Executors工廠類:通過此類可以獲得一個線程池
- 通過static ExecutorService new FixedThreadPool(int nThreads) 獲取固定數量的線程池。參數:線程池中的線程的數量
- 通過newCachedThreadPool() 獲得動態數量的線程池,如果不夠則創建新的,沒有上限
public class TestThreadPool {
public static void main(String[] args) {
// 利用線程池的工廠方法來生產對象 參數3代表創建三個線程
ExecutorService es = Executors.newFixedThreadPool(3) ;//手動限定線程池裏的線程數量。
MyTask a = new MyTask();
//2.將任務提交到線程池,由線程池調度、執行
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
}
}
// 線程任務
class MyTask implements Runnable{
public void run() {
for(int i = 1;i<=50;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class TestThreadPool {
public static void main(String[] args) {
// 利用線程池的工廠方法來生產對象 參數3代表創建三個線程
ExecutorService es = Executors.newCachedThreadPool();//自動擴充線程池中線程數量
MyTask a = new MyTask();
//2.將任務提交到線程池,由線程池調度、執行
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
es.submit(a);
}
}
// 線程任務
class MyTask implements Runnable{
public void run() {
for(int i = 1;i<=50;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
newCachedThreadPool() 與 newFixedThreadPool(3) 的對比
- newCachedThreadPool()是動態創建線程,不用對程序進行線程創建數量的預估,而且用戶體檢感較好,但是最大的詬病是線程足夠多時會內存溢出
- newFixedThreadPool()給與評估過後的線程數量,可能會在用戶過多時候慢些,也可通過維護服務器時修改線程數量等方法增加擴充。
接口Callable
- 類似於Runnable,兩者都是爲了哪些實例可能被另一個線程執行的類設計的,但是Runnable沒有返回結果
public interface Callable<V>{
public V call() throws Exception;
}
- JDK5加入的,與Runnable接口類似,實現之後代表一個線程任務
- Callable具有泛型返回值、可以聲明異常
public class TestCallable {
public static void main(String[] args) {
// 創建一個線程池
ExecutorService es = Executors.newFixedThreadPool(3);
MyTask1 task = new MyTask1();
// 將任務放入線程池
es.submit(task);
}
}
class MyTask1 implements Callable<Integer>{
public Integer call() throws Exception{
for (int i = 0; i < 100; i++) {
if(i == 30) {
Thread.sleep((int)(Math.random()*1000));
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
return null;
}
}
Future接口
- 異步接收ExecutorService.submit()所返回的狀態結果,當中包含了call()返回值
- 方法:V get()以阻塞形式等待Future中的異步處理結果(call()的返回值)
示例:兩個線程,併發計算1 ~ 50、51 ~ 100的和,在進行彙總統計
public class TestFuture {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService es = Executors.newFixedThreadPool(3);
MyCall mc = new MyCall();
MyCall2 mc2 = new MyCall2();
//通過submit執行提交的任務,Future接受返回的結果
Future<Integer> result = es.submit(mc);
Future<Integer> result1 = es.submit(mc2);
//通過Future的get方法,獲得線程執行完畢後的結果.
Integer value = result.get();
Integer value2 = result1.get();
System.out.println(value + value2);
}
}
//計算1~50的和
class MyCall implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for(int i = 1;i<=50;i++) {
sum = sum + i;
}
return sum;
}
}
//計算51~100的和
class MyCall2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for(int i =51;i<=100;i++) {
sum = sum + i;
}
return sum;
}
}
擴充
- 同步
- 形容一次方法調用,同步一旦開始,調用者必須等待該方法返回,才能繼續。
- 單條執行路徑
- 異步
- 形容一次方法調用,異步一旦開始,像是一次消息傳遞,調用者告知之後立刻返回。二者競爭時間片,併發執行