一、進程與線程
1、進程(process)
每個獨立運行着的程序稱爲一個進程,它是操作系統分配資源的基本單位,有獨立的內存空間和系統資源。
2、線程(thread)
線程是進程內部的一條執行路徑(path),Java虛擬機允許應用程序併發的運行多個執行路徑,它是進程中執行運算的最小單位,處理機分配給線程,即真正在處理機上運行的是線程。
3、進程和線程區別
進程有獨立的地址空間,一個進程崩潰後,不會對其它進程產生影響,而線程只是一個進程中的一個執行路徑,如有一條線程崩潰了,可能會影響同進程中的其他的線程。
線程有自己的棧和局部變量,多個線程共享同一進程的地址空間
一個進程至少有一個線程
4、線程的生命週期
線程的狀態:
要想實現多線程,必須在主線程中創建新的線程對象。任何線程都具有五種狀態:創建、就緒、運行、阻塞、終止。
5、線程的停止
如果線程的run()方法中執行的是一個重複執行的循環,可以提供一個標記來控制循環是否執行。
如果線程因爲執行sleep()或是wait()而進入了阻塞狀態,此時要想停止它,可以使用interrupt(),程序會拋出InterruptException異常。
如果程序因爲輸入/輸出的等待而阻塞,基本上必須等待輸入/輸出的動作完成才能離開阻塞狀態。無法用interrupt()方法來使得線程離開run()方法,要想離開,只能通過引發一個異常。
二、多線程的提出
多線程就是在一個進程中創建多個線程,每個線程完成一個任務
â優點
● 多線程技術使程序的響應速度更快
● 提高資源利用率
● 程序設計更簡單
â多線程執行特性
隨機性(異步執行)誰”搶”到cpu,誰執行宏觀上同時執行,微觀上同一時刻只能執行一個線程,多核除外
三、線程的創建與啓動
1、創建線程的第一種方式:繼承Thread類。
第一種方式:將類聲明爲Thread 的子類並重寫run()方法
class MyThread extends Thread{
public void run(){
線程具體執行的代碼
}
}
創建此線程類的實例並啓動:
MyThread thread1 = new MyThread();
thread1.start(); // 啓動線程
實例
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();
Thread t1 = new Thread(d);//創建線程
Thread t2 = new Thread(d);
t1.start();//啓動線程
t2.start();
}
}
1、創建線程的第二種方式:實現Runnable接口。
1,定義類實現Runnable接口。
2,覆蓋接口中的run方法,將線程的任務代碼封裝到run方法中。
3,通過Thread類創建線程對象,並將Runnable接口的子類對象作爲Thread類的構造函數的參數進行傳遞。
爲什麼?因爲線程的任務都封裝在Runnable接口子類對象的run方法中。
所以要在線程對象創建時就必須明確要運行的任務。
4,調用線程對象的start方法開啓線程。
實現Runnable接口的好處:
1,將線程的任務從線程的子類中分離出來,進行了單獨的封裝。
按照面向對象的思想將任務的封裝成對象。
2,避免了java單繼承的侷限性。
所以,創建線程的第二種方式較爲常用。
定義實現Runnable接口的類
Runnable接口中只有一個方法 public void run(); 用來定義線程運行體:
class MyRun implements Runnable{
public void run(){
線程執行的具體代碼
}
}
創建線程的實例的時候將這個類的實例作爲參數傳遞到線程實例內部。然後再啓動:
Thread thread1 = new Thread(new MyRun());
thread1.start();
實例:
class Demo implements Runnable//extends Fu //準備擴展Demo類的功能,讓其中的內容可以作爲線程的任務執行。
//通過接口的形式完成。
{
public void run()
{
show();
}
public void show()
{
for(int x=0; x<20; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
class Thread
{
private Runnable r;
Thread()
{
}
Thread(Runnable r)
{
this.r = r;
}
public void run()
{
if(r!=null)
r.run();
}
public void start()
{
run();
}
}
class ThreadImpl implements Runnable
{
public void run()
{
System.out.println("runnable run");
}
}
ThreadImpl i = new ThreadImpl();
Thread t = new Thread(i);
t.start();
class SubThread extends Thread
{
public void run()
{
System.out.println("hahah");
}
}
1、通過 Callable接口實現多線程
1. Callable接口介紹
java.util.concurrent.Callable是一個泛型接口,只有一個call()方法
call()方法拋出Exception異常,且返回一個指定的泛型類的對象
2. Callable接口實現線程的應用場景
當父線程想要獲取子線程的運行結果時
3.使用Callable接口實現多線程的步驟
第一步:創建Callable子類的實例化對象。
第二步:創建FutureTask 對象,並將Callable對象傳入FutureTask的構造方法中(注意:FutureTask實現了Runnable接口和Future接口)
第三步:實例化Thread對象,並在構造方法中傳入FutureTask對象
第四步:啓動線程
Object中的幾個方法支持
wait() 線程等待,當前線程進入調用對象的線程 等待池
notify() 喚醒1個等待線程
notifyAll() 喚醒全部等待的線程
注意:以上三個方法都必須在同步機制中調用
代碼示例:
public class AnonyCallable {
public static void main(String[] args) {
Callable<String> call=new Callable<String>(){
@Override
public String call() throws Exception {
String threadName=Thread.currentThread().getName();
System.out.println(threadName+"開始偷襲...");
System.out.println(threadName+"不幸遇到大股敵軍");
return "偷襲失敗,只剩我一個人了!";
}
};
FutureTask<String> task=new FutureTask<String>(call);
new Thread(task,"魏延線程").start();
System.out.println(Thread.currentThread().getName()+"進入休眠狀態(休眠5秒)...");
try {
Thread.sleep(5000);
String report=task.get(); // 獲取子線程的返回值
System.out.println("子線程的返回結果是:"+report);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package callable;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) {
GeneralCallable call=new GeneralCallable(); // 創建Callable子類的實例化對象
FutureTask<String> task=new FutureTask<String>(call); // 創建FutureTask 對象,並將Callable對象傳入FutureTask的構造方法中
Thread t=new Thread(task,"魏延線程"); // 實例化Thread對象,並在構造方法中傳入FutureTask對象
t.start(); // 啓動線程
System.out.println(Thread.currentThread().getName()+"進入休眠狀態(休眠5秒)...");
try {
Thread.sleep(5000);
String report=task.get(); // 獲取子線程的返回值
System.out.println("子線程的返回結果是:"+report);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package callable;
import java.util.concurrent.Callable;
public class GeneralCallable implements Callable<String>{
@Override
public String call() throws Exception {
String threadName=Thread.currentThread().getName();
System.out.println(threadName+"開始偷襲...");
System.out.println(threadName+"與小股敵人作戰");
return "偷襲成功!";
}
}
1、多線程安全與同步問題
當run()方法體內的代碼操作到了成員變量(共享數據)時,就可能會出現多線程安全問題(線程不同步問題)。
編程技巧
在方法中儘量少操作成員變量(在不需要共享資源時),多使用局部變量
2、線程的同步
在Java語言中,引入對象互斥鎖的概念,保證共享數據操作的完整性。每個對象都對應於一個可稱爲“互斥鎖”的標記,這個標記保證在任一時刻,只能有一個線程訪問對象。
關鍵字synchronized用來與對象的互斥鎖關聯。當某個對象用synchronized修飾時,表明該對象在任一時刻只能由一個線程訪問(這個對象就變成了同步對象)。
一個方法使用關鍵字synchronized修飾後,當一個線程A使用這個方法時,其他線程想使用這個方法就必須等待,直到線程A使用完該方法(前提是這些線程使用的是同一個同步對象)
3、同步方法的同步對象
1.對於非靜態方法來說,this當前對象充當了同步對象
2.對於靜態方法來說,同步對象是“類對象”,"類對象"代表的是這個類本身,所有通過類實例化的普通對象共享這個"類對象"
4、使用synchronized關鍵字實現同步(賣票問題)
synchronized的使用方法:
同步代碼塊:synchronized放在對象前面限制一段代碼的執行
synchronized(同步對象){
需要同步的代碼;
}
同步方法:synchronized放在方法聲明中,表示
public synchronized void method(){
…
}
實例:
package lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{
private int ticket=5;
private Lock lock=new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock(); // 獲取鎖(更常用的方法)
//lock.tryLock();
try{
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; System.out.println(Thread.currentThread().getName()+"賣了一張票,當前剩餘票數:"+ticket);
}
}finally{
lock.unlock(); // 釋放鎖
}
}
}
}
package lock;
public class TestLock {
public static void main(String[] args) {
TicketLock tl=new TicketLock();
new Thread(tl,"線程A").start();
new Thread(tl,"線程B").start();
new Thread(tl,"線程C").start();
}
}
1、加上同步機制後,效率低的原因:
1.會喪失Java多線程的併發優勢。在執行到同步代碼塊(或同步方法)時,只能有一個線程執行,其他線程必須等待執行同步代碼塊(同步方法)的線程釋放同步對象的鎖。
2.其他等待鎖釋放的線程會不斷檢查鎖的狀態。
注意:
同步函數的使用的鎖是this;
同步函數和同步代碼塊的區別:
同步函數的鎖是固定的this。
同步代碼塊的鎖是任意的對象。
建議使用同步代碼塊。
9、多線程狀態圖
多線程狀態下的單例設計模式:
/*
多線程下的單例
*/
//餓漢式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
//懶漢式
加入同步爲了解決多線程安全問題。
加入雙重判斷是爲了解決效率問題。
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
// -->0 -->1
s = new Single();
}
}
return s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
一、死鎖問題
1、死鎖原因:
線程1鎖住資源A等待資源B,線程2鎖住資源B等待資源A,兩個線程都在等待自己需要的資源,而這些資源被另外的線程鎖住,這些線程你等我,我等你,誰也不願意讓出資源,這樣死鎖就產生了。
2、哲學家進餐問題
解決哲學家進餐問題代碼示例:
package deadlock;
// 叉子類
public class Fork {
public void forkSay(){
System.out.println("我拿到叉子了,請給我刀子...");
}
}
package deadlock;
// 刀子類
public class Knife {
public void knifeSay(){
System.out.println("我拿到刀子了,請給我叉子...");
}
}
package deadlock;
// "哲學家進餐"
public class PhilosopherRunnable implements Runnable{
private boolean flag=false;
private static Knife knife=new Knife(); // 刀子資源
private static Fork fork=new Fork(); // 叉子資源
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized(knife){
knife.knifeSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(fork){
fork.forkSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完飯了......");
}else{
synchronized(fork){
fork.forkSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(knife){
knife.knifeSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完飯了......");
}
}
}
package deadlock;
public class Test {
public static void main(String[] args) {
PhilosopherRunnable p1=new PhilosopherRunnable();
p1.setFlag(true);
PhilosopherRunnable p2=new PhilosopherRunnable();
new Thread(p1,"尼采").start();
new Thread(p2,"蘇格拉底").start();
}
}
一、Lock實現同步
java.util.concurrent.locks包下的相關接口與類
1. Lock接口
通常使用Lock接口中的方法用來獲取鎖,其中lock()方法是使用得最多的一個方法。
2. ReentrantLock類(“可重入鎖”)
ReentrantLock是實現了Lock接口的類
3. ReadWriteLock接口
包括了兩個方法:
Lock readLock(); 用來獲取“讀鎖”
Lock writeLock(); 用來獲取“寫鎖”
4. ReentrantReadWriteLock類
是ReadWriteLock接口的實現類
注意:
關於線程的讀鎖和寫鎖:
如果有一個線程已經佔用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖;但其他線程申請讀鎖是可以的。
如果有一個線程已經佔用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
5.Synchronized與Lock
(1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
(2) synchronized在發生異常時,會自動釋放線程佔有的鎖;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
(3) Lock可以提高多個線程進行讀操作的效率
6.代碼示例(利用多線程進行操作讀寫鎖):
package rwlock;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class InfoRunnable implements Runnable {
private static int data;
private boolean flag=false;
private static ReadWriteLock rwl=new ReentrantReadWriteLock(); // 該屬性對象可以獲取"讀鎖"或"寫鎖"
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
for(int i=0;i<10;i++){
writeData();
}
}else{
for(int i=0;i<10;i++){
readData();
}
}
}
// 寫數據方法
public static void writeData(){
try{
rwl.writeLock().lock(); // 獲取"寫鎖"
System.out.println(Thread.currentThread().getName()+"準備寫數據...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data=new Random().nextInt(10); // 模擬"寫數據"
System.out.println(Thread.currentThread().getName()+"寫數據完畢。");
}finally{
rwl.writeLock().unlock(); // 釋放"寫鎖"
}
}
// 讀數據方法
public static void readData(){
try{
rwl.readLock().lock(); // 獲取"讀鎖"
System.out.println(Thread.currentThread().getName()+"準備讀取數據...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"讀取到的數據是:"+data);
}finally{
rwl.readLock().unlock(); // 釋放"讀鎖"
}
}
}
package rwlock;
public class TestInfo {
public static void main(String[] args) {
InfoRunnable info1=new InfoRunnable();
info1.setFlag(true);
new Thread(info1,"寫入線程1").start();
new Thread(info1,"寫入線程2").start();
InfoRunnable info2=new InfoRunnable();
new Thread(info2,"讀取線程1").start();
InfoRunnable info3=new InfoRunnable();
new Thread(info3,"讀取線程2").start();
}
}
一、通過 Callable實現多線程
1. Callable接口介紹
java.util.concurrent.Callable是一個泛型接口,只有一個call()方法
call()方法拋出Exception異常,且返回一個指定的泛型類的對象
2. Callable接口實現線程的應用場景
當父線程想要獲取子線程的運行結果時
3. 使用Callable接口實現多線程的步驟
第一步:創建Callable子類的實例化對象。
第二步:創建FutureTask 對象,並將Callable對象傳入FutureTask的 構造方法中(注意:FutureTask實現了Runnable接口和Future接口)
第三步:實例化Thread對象,並在構造方法中傳入FutureTask對象
第四步:啓動線程
4. 生產者-消費者問題
生產者線程不斷生產,消費者線程不斷取走生產者生產的產品。
Object中的幾個方法支持:
wait() 線程等待,當前線程進入調用對象的線程 等待池
notify() 喚醒1個等待線程
notifyAll() 喚醒全部等待的線程
注意:以上三個方法都必須在同步機制中調用
代碼示例(一對一):
package one2one.producer;
// 早餐基礎類
public class Breakfast {
private String food; // 吃的
private String drink; // 喝的
private boolean flag=false;
public synchronized void makeBreakfast(String food,String drink){
if(flag){
try {
wait(); // 生產者線程進入同步對象維護的“線程等待池”
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.food=food;
try {
Thread.sleep(1000); // 休眠,但不釋放“鎖”
} catch (InterruptedException e) {
e.printStackTrace();
}
this.drink=drink;
flag=true;
notify();
}
public synchronized void eatBreakfast(){
if(!flag){
try {
wait(); // 消費者線程進入同步對象維護的“線程等待池”,而且當前線程釋放"鎖"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.food+"=============>"+this.drink);
flag=false;
notify();
}
}
package one2one.producer;
// 消費者線程
public class Consumer implements Runnable{
private Breakfast bf;
public Consumer(Breakfast bf){
this.bf=bf;
}
@Override
public void run() {
for (int i = 1; i <=7; i++) {
this.bf.eatBreakfast();
}
}
}
package one2one.producer;
// 生產者線程
public class Producer implements Runnable{
private Breakfast bf;
public Producer(Breakfast bf){
this.bf=bf;
}
@Override
public void run() {
for (int i = 1; i <=7; i++) {
if(i%2==0){
this.bf.makeBreakfast("bread","milk");
}else{
this.bf.makeBreakfast("饅頭","稀飯");
}
}
}
}
package one2one.producer;
public class Test {
public static void main(String[] args) {
Breakfast bf=new Breakfast();
new Thread(new Producer(bf)).start(); // 啓動生產者線程
new Thread(new Consumer(bf)).start(); // 啓動消費者線程
}
}
5.多線程下載(複製)文件(代碼示例)
package download;
import java.io.*;
public class DownloadRunnable implements Runnable{
private File srcFile; // 源文件路徑
private long startPos; // 每個線程的開始下載位置
private long partTask; // 每個線程的下載任務
private RandomAccessFile raf; // 用來寫入
public DownloadRunnable(File srcFile,long startPos,long partTask,RandomAccessFile raf){
this.srcFile=srcFile;
this.startPos=startPos;
this.partTask=partTask;
this.raf=raf;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"準備從第"+startPos+"個字節開始讀...");
InputStream input=null;
try {
input=new FileInputStream(srcFile);
input.skip(startPos); // 跳過輸入流的startPos個字節
byte[] b=new byte[1024*1024*10];
int len=0;
int count=0; // 用來記錄已經讀寫的字節數
while((len=input.read(b))!=-1 && count<partTask){
raf.write(b, 0, len);
count+=len;
}
System.out.println(Thread.currentThread().getName()+"已經寫入了"+count+"個字節");
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
input.close();
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package download;
import java.io.File;
import java.io.RandomAccessFile;
public class Client {
public static void main(String[] args)throws Exception {
File srcFile=new File("f:"+File.separator+"qmshy.mpg");
long partTask=srcFile.length()/6; // 準備使用6個線程,計算每個線程下載的任務
for(int i=0;i<6;i++){
RandomAccessFile raf=new RandomAccessFile("c:"+File.separator+srcFile.getName(),"rw");
long startPos=i*partTask; // 計算每條線程的讀寫起始位置
raf.seek(startPos); // 設置每條線程的文件指針偏移量
new Thread(new DownloadRunnable(srcFile,startPos,partTask,raf),i+"線程").start();
}
}
}