java多線程是java高級階段的知識點,也是java中比較難學的一部分,今天我們來初步學習一下java的多線程知識。說在前面,只是簡單的入門,更高深的講解可以參考其它大牛博文。
一、首先對於學習多線程有必要先了解一些概念:
1、什麼是並行?什麼是併發?
並行是指真正意義上的在同一時刻同時執行多個任務,比如多處理機的電腦就可以在同一時刻處理多個任務。
併發是虛擬上的同時執行,比如我們平時玩電腦,同時聽歌,玩遊戲,看電視,貌似是同時進行的,但是其實沒有,只是cpu工作切換的時間很快,我們根本無法感受到,導致我們認爲是同時進行的。併發的意義就在於充分利用cpu資源。對於java中的多線程就是併發程序。在某一刻只能有一個任務執行,cpu調度任務執行。
2、進程、線程、程序的區別?
首先說程序,它是靜態的,它只是一連串的字符組成的代碼塊
進程:進程是操作系統的資源分配的基本空間,操作系統會爲其分配對應的內存空間,而且進程有自己的代碼和數據空間。不存在沒有線程的進程
線程:線程是cpu調度的基本單位,沒有自己的內存空間,線程可以理解爲輕量級的進程,一個進程中可以有多個線程。
二、多線程實現的兩者方式:
繼承Thread類和實現Runnable接口
package com.yxc.thread;
/**
* 多線程的學習
* 創建多線程的兩種方式
* 第一繼承Thread類
* 第二種實現Runable接口
*/
public class MyThread extends Thread{
private String name;
//通過構造方法進行初始化工作
public MyThread(String name){
this.name=name;
}
//繼承了Thread類,可以重寫裏面的run方法,這個方法是多線程的核心方法
public void run(){
//這個線程的工作根簡單,就是從1打印到19
System.out.println(name+"開始執行");
for(int i=0;i<20;i++){
System.out.println(i);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"結束執行");
}
}
package com.yxc.thread;
/**
* 通過實現接口實現多線程
*/
public class MyThread2 implements Runnable{
private String name;
public MyThread2(String name){
this.name=name;
}
@Override
public void run() {
System.out.println(name+"開始執行");
for(int i=0;i<10;i++){
System.out.println("java多線程的學習 使用runnable實現");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"結束執行");
}
}
package com.yxc.thread;
/**
* 大部分情況下,我們都使用第二種方式實現多線程
* 因爲在java中繼承是有限制的,那就是單繼承,如果繼承了這個類以後,如果這個
* 類還想繼承其它的類,那麼就不可以,但是實現接口可以實現多個。
*/
public class TestThread {
public static void main(String[] args) {
//下面是實現兩者多線程方式的測試代碼
MyThread myThread = new MyThread("打印線程");
//對於實現接口來說。最終還是要通過Thread類來創建執行多線程的對象
MyThread2 myThread2=new MyThread2("輸出線程");
Thread thread=new Thread(myThread2);
//開啓線程一定是調用start()方法,而不是通過對象調用run方法
myThread.start();
thread.start();
//最後的執行效果的肯定是沒有規律的,因爲多線程的執行情況我們根本無法預知,是操作系統隨機調度的
//後面的學習中還會涉及到這個概念
}
}
這裏面涉及到幾個常用的多線程中的方法:
start():開啓線程,注意開啓線程不一定就開始執行了,只是進入就緒態,要想真正執行需要cpu的調度
Sleep():休眠,傳入一個毫秒數,表示當前線程休眠一定時間後又進入就緒態
三、線程的五種狀態
下面通過一段案例來了解這幾種狀態的具體情況:
package com.yxc.thread;
/**
* 線程的五種狀態的瞭解
*/
public class ThreadStatusDemo {
public static void main(String[] args) {
PrintStar printStar=new PrintStar();
System.out.println("線程新建狀態");
Thread thread=new Thread(printStar,"數星星線程");
thread.start();
System.out.println("線程進入就緒轉狀態");
//這個時候還不一定指定,要等待cpu調用才進入真正的運行態
}
}
//一個數星星的線程
class PrintStar implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"線程進入運行態");
for(int i=1;i<100;i++){
System.out.println("第"+i+"顆星星");
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"線程進入阻塞態");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程阻塞結束進入就緒態");
}
System.out.println(Thread.currentThread().getName()+"線程結束");
}
}
4、幾種方法的學習以及對比:
sleep():讓當前線程休眠指定的時間,線程進入阻塞態,時間到以後又進入就緒態,開始搶佔cpu資源
yield ():這個方法比較有意思,表面上是讓出,讓其它線程執行,但是它讓出以後又馬上進入就緒態,同其它的線程搶佔資源,可以理解爲給你們個機會,公平競爭。
join(); 等待其它的線程執行完以後再執行,前面的兩個方法都是屬於類方法,這個是實例方法,要通過對象來調用的,這個方法是用在其它線程裏面的,通過單詞join(加入)也可以理解,強行加入到某一個線程中去執行
wait():實例方法,調用這個方法以後線程進入阻塞狀態,而且需要等待其它線程的喚起才能進入就緒態,而且調用它,資源會被釋放
package com.yxc.thread;
public class MthodDemo {
public static void main(String[] args) {
Thread thread=new Thread(new MyThread1());
thread.start();
for(int i=100;i<120;i++){
if(i==105) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i);
}
}
}
class MyThread1 implements Runnable{
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println(i);
}
}
}
在代碼中join()方法加入到了main線程中去,當main線程中的執行到105的時候將不再執行,線程將強行加入到main線程中執行。
5、經典的生產者與消費者模式:
下面通過代碼實現多線程最經典的生產者與消費者模式,通過兩個線程之前的通知等待來實現,也學習一下wait方法和notify()、notifyAll()三個方法的使用。我們寫一個簡單的,一個機器負責生產和消費,然後兩個線程去執行,話不多說,直接上代碼。
package com.yxc.producerAndConsumer;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* 這個類是生產和消費冰淇淋的機器類
*/
public class IceMachine {
//定義一個機器中最大的容量
private static final int MAX_BUFFER=20;
//定義一個隊列來存放冰淇淋
Queue<Integer> IceQueue=new ArrayDeque<>();
/**生產者生產冰淇淋的方法*/
public synchronized void produceIce(){
//如果機器中的數量達到了上線,那麼通知生產者暫時不要生產
//直接調用wait()方法就可以
while(IceQueue.size()==MAX_BUFFER){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int count=0;
//如果機器中數量沒有達到限定,那麼就不斷的生產
while(IceQueue.size()<MAX_BUFFER){
//隨機產生一個1~100整數
int i = (int)(Math.random()*100+1);
//將編號爲I的冰淇淋添加到隊列中,等待消費者的消費
IceQueue.offer(i);
count++;
}
System.out.println("入庫了"+count);
//通知消費者可以消費了
notify();
}
/**消費者消費方法*/
public synchronized int ConsumeIce(){
//如果隊列中的產品數量爲零,那麼通知消費者等待
while(IceQueue.size()==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer i = IceQueue.poll();
System.out.println(i+"號產品被消費");
return i;
}
}
package com.yxc.producerAndConsumer;
/**
* 消費者線程
*/
public class Consumer implements Runnable{
//將生產機器傳進來
private IceMachine iceMachine=null;
//通過構造方法初始化
public Consumer(IceMachine iceMachine){
this.iceMachine=iceMachine;
}
@Override
public void run() {
System.out.println("消費者開始消費產品");
while(true){
try {
iceMachine.ConsumeIce();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.yxc.producerAndConsumer;
/**
* 生產者線程
*/
public class Producer implements Runnable{
//將生產機器傳進來
private IceMachine iceMachine=null;
//通過構造方法初始化
public Producer(IceMachine iceMachine){
this.iceMachine=iceMachine;
}
@Override
public void run() {
System.out.println("生產者開始生產產品");
while(true){
try {
iceMachine.produceIce();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.yxc.producerAndConsumer;
/**
* 測試類
*/
public class TestMain {
public static void main(String[] args) {
IceMachine iceMachine=new IceMachine();
Producer produce=new Producer(iceMachine);
Consumer consumer=new Consumer(iceMachine);
new Thread(produce).start();
new Thread(consumer).start();
}
}
部分實現的結果圖:
6、線程池
*概念:*池顧名思義就是由很多線程組成的一個集合,系統啓動時系統就創建了很多空的線程保存在線程池中,程序將一個任務傳給線程池時,系統就會在線程池中尋找空閒的線程來執行這個任務。
優勢:大量的創建線程、釋放線程需要花費很多的系統資源,有了線程池以後,有效的提高了性能。減少了資源的消耗
線程池工作原理:任務直接提交給線程池,又線程池尋找空閒線程執行任務,執行完以後,線程又返回空的狀態,返回線程池中,等待下一個任務的執行。一個任務只能有分配一個線程,但是可以同時向線程池發送多個任務。
下面通過代碼實現集中常見的創建線程池的方法
爲了方便,我將四種常見的創建方法以靜態方法的形式實現,然後在主方法中直接調用四種方式,每一種都寫了註釋。
package com.yxc.threadPool;
import java.util.concurrent.*;
/**
* 線程池的學習使用
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
test1();
test2();
test3();
test4();
}
/**創建指定個數的線程池*/
public static void test1(){
//創建一個容量爲3的線程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"線程被執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**執行線程池的個數,而且有定時效果的 */
public static void test2(){
//創建一個這樣的線程池,可以定時以及週期性執行任務
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("延遲一秒以後每兩秒執行一次");
}
},1,2,TimeUnit.SECONDS);//這裏兩個參數 延時時間和週期時間
}
/**創建一個單例線程,線程池中只有一個線程可以執行任務,這樣符合先來先執行的規則*/
public static void test3(){
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
int index=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**帶緩存的線程池,如果這個線程被用過,直接分配,如果沒有被用過,那麼就生成一個新的線程分配給任務,然後將線程添加到線程池中*/
public static void test4(){
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"在被執行");
}
});
}
}
}
代碼執行結果:
test1:
test3:
test4:
對於測試2,代碼延遲一秒執行,然後之後每兩秒執行一次,無限循環下去
當然對於多線程這一章,代碼一樣,很可能執行的結果是不一樣的,總結它是一個不靠譜的東西。這一章還有一些方法,沒有說明,比如設置權限,設置權限只能優先級高一點,但是不能保證一定被執行。還有設置守護線程等等。