多線程的實現及其安全問題
一、進程和線程概述
1、進程:進程是一個具有獨立功能的程序關於某個數據集合的一次運行活動,簡單來說開啟一個程序就開啟了一個進程;
如果開啟多個進程,它們之間是由於CPU的時間片在相互的切換;
2、線程:開啟一個進程的一個任務,對於多線程:每一個線程都在爭奪CPU的執行權(CPU的執行權具有隨機性);
如果一個程序的執行路徑有多條,那麽該線程是多線程;反之,就單線程線程;線程是依賴於進程存在的!
3、Jvm是多線程 —— 至少開啟了兩條線程
main方法 主線程
gc() 垃圾回收線程
二、多線程的實現方式
1、繼承Thread類
1)自定義一個類,該類繼承Thread類
2)重寫的run()方法
public class MyThread extends Thread{
public void run(){
耗時操作;
}
}
3)在主線程中創建該類對象
MyThread mt = new MyThread(); //創建幾個對象,就有幾個線程,去執行run()方法中的耗時操作
4)start()方法,啟動線程
mt.start();
2、實現Runnable接口
1)自定義一個類,該類實現Runnable接口 //數據共享
2)重寫的run()方法
public class MyRunnable implements Runnable{
耗時操作
}
}
3)在主線程中創建該類的對象
MyRunnable mr = new MyRunnable();
4)創建Thread類對象,將第三步的對象作為參數進行傳遞
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr,"名稱"); //可以設置線程名稱
5)start()方法,啟動線程
t1.start();
t2.start();
3、使用線程池(主要用於計算)
1)自定義一個類,實現Callable接口
public class MyCallable implements Callable{}
2)實現裏面的call方法
相當於run()方法,裏面放一些耗時操作;
3)主線程中創建線程對象
ExecutorService threadPool = Executors.newFixedThreadPool(2) ; 參數表示線程的個數
4)用線程池對象提交任務
threadPool.submit(new MyCallable());
threadPool.submit(new MyCallable());
5)提交後結束線程池
threadPool.shutdown();
三、Thread的常用方法(Thread實現Runnable接口)
public static Thread currentThread()返回對當前正在執行的線程對象的引用
public final String getName()返回該線程的名稱
public final void setName(String name)改變線程名稱
public final void setDaemon(boolean on)當參數為true時,設置該線程為守護線程,當正在運行的線程都是守護線程時Java虛擬機退出
//該方法必須在啟動線程前調用
public final void join()throws InterruptedException等待該線程終止 //等待該線程執行完畢,其他線程才能執行
public static void sleep(long millis)在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行)
public final void stop()強制停止執行 //已過時
public void interrupt()中斷線程 //中斷當前線程的這種狀態(打破了一種狀態)
public static void yield()暫停當前正在執行的線程對象,並執行其他線程
//線程的執行具有隨機性,下次有可能又是它搶占到了CPU執行權,還是它接著執行
四、多線程的安全問題
egg:共有100張票,而它有3個售票窗口售票,請設計一個程序模擬該電影院售票
public class SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket --;
}else{
break;
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
1、問題:
1)出現了同一張票被賣了多次
由於CPU的執行具有原子性(最簡單的操作)操作
2)出現了0票和負票
由於cpu的執行權具有隨機性和延遲性導致
2、線程安全的檢測條件
1)看我們當前的環境是否屬於多線程程序
2)當前的多線程程序中是否有共享數據
3)是否多條語句對共性數據進行操作
3、多線程的同步機制
為了解決上述安全問題java提供了一個同步機制 —— 關鍵詞sychronized
1)同步代碼塊的使用:
sychronized(任意對象){
多條語句對共享數據進行操作的代碼;
}
2)註意事項:
A:所有線程只能使用同一個對象(將任意對象當作一個鎖:使用的是同一把鎖),所以不能使用匿名對象當做參數
B:註意同步代碼塊所包的代碼不能將循環包進去,否則就不是多線程了
3)同步方法:
public static synchronized void 方法名(){
多條語句對共享數據進行操作的代碼;
} //同步方法使用鎖對象是this
靜態的同步方法的鎖對象是:類名.class(java中的反射機制)
4)Lock接口:
實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作
創建具體的鎖對象
private Lock lock = new ReentrantLock(); //Lock接口不能實例化,提供了子實現類:ReentrantLock
public void lock()獲取鎖
多條語句對共享數據進行操作的代碼;
public void unlock():釋放鎖
使用:一般使用try-finally將整個內容包起來,在finally中釋放鎖
4、修改後代碼:
public class SellTicket implements Runnable{
private static int ticket = 100;
@Override
public void run() {
while(true){
sellTicket();
if(ticket <= 0){
break;
}
}
}
public static synchronized void sellTicket(){
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket --;
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
5、和線程安全相關的類
StringBuffer sb = new StringBuffer();
Vector<T> v = new Vector<T>() ;
Hashtable<K, V> hs = new Hashtable<K,V>() ;
public static <T> List<T> synchronizedList(List<T> list)返回指定列表支持的同步(線程安全的)列表
List<T> list = Collections.synchronizedList(new ArrayList<T>());
五、死鎖的問題
1、線程安全的弊端:
1)執行效率低
2)容易產生死鎖
兩個或兩個以上的線程,搶占CPU的執行權,然後出現了互相等待的情況;
2、生產者消費者模式:
分別進行產生數據和消費數據兩條線程,針對同一資源進行操作
egg:
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
public class Student {
private String name;
private int age;
public Student() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class SetThread implements Runnable{
private Student s;
public SetThread(Student s) {
super();
this.s = s;
}
@Override
public void run() {
int i = 0;
while(true){
if(i % 2 == 0){
s.setName("張三");
s.setAge(30);
}else{
s.setName("李四");
s.setAge(40);
}
i ++;
}
}
}
/*
* 理想狀態:
* 張三=30
* 李四=40
* 張三=30
* 李四=40
* 循環...
*/
public class GetThread implements Runnable{
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while(true){
System.out.println(s.getName() + "=" + s.getAge());
}
}
}
3、問題:
1)出現了 張三=40 線程隨機性導致
解決方法:給代碼加上同步代碼塊
2)並沒有和理想狀態一樣,張三、李四依次出現,而是同一個數據被打印了多次
解決方法:等待喚醒機制
註意:同步鎖,不同的線程之間使用的是同一把鎖對象!
Object類中有關多線程的方法:
public final void wait()throws InterruptedException當前線程等待
public final void notify()喚醒在此對象監視器上等待的單個線程
public final void notifyAll()喚醒在此對象監視器上等待的所有線程
//這些方法一般是使用的鎖對象進行調用,鎖對象可以是任意對象,所以這幾個有關多線程的方法在Object類中
4、修改後代碼:
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
new Thread(st).start();
new Thread(gt).start();
}
}
public class Student {
private String name;
private int age;
private boolean falg;
public Student() {
super();
}
public synchronized void set(String name,int age){
if(falg == true){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.age = age;
falg = true;
this.notify();
}
public synchronized void get(){
if(falg == false){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name + "=" + this.age);
falg = false;
this.notify();
}
}
public class SetThread implements Runnable{
private Student s;
public SetThread(Student s) {
super();
this.s = s;
}
@Override
public void run() {
int i = 0;
while(true){
if(i % 2 == 0){
s.set("張三", 30);
}else{
s.set("李四", 40);
}
i++;
}
}
}
public class GetThread implements Runnable{
private Student s;
public GetThread(Student s) {
super();
this.s = s;
}
@Override
public void run() {
while(true){
s.get();
}
};
}
六、定時機制
1、定時器:可以進行任務的重復操作
可安排任務定時執行一次,或者定期重復執行
2、Timer類
1)Timer的使用
構造方法:public Timer()創建一個新計時器
方法: public void schedule(TimerTask task, Date time)安排在指定的時間執行指定的任務
參數1:task - 所要安排的任務
參數2:time - 執行該任務的時間毫秒值
public void schedule(TimerTask task, Date firstTime,long period)每隔多少毫秒進行重復性的任務操作
public boolean cancel()取消此計時器任務
2)TimerTask類
需要自定義類去繼承,並重寫其run()方法;作為所要安排的任務
3)執行一個定時器,3秒之後爆炸,並且每隔2秒繼續執行
public class TimerDemo{
public static void main(String[] args){
Timer t = new Timer();
t.schedule(new MyTask(), 3000, 2000);
}
}
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("bom,爆炸了");
}
}
多線程的實現及其安全問題