多線程併發庫<一>
前言:
學習不斷積累的過程,有些知識雖然現在還不知道有什麼用,但卻可以爲以後學習新知識打下基礎,並在關鍵時候能解決大問題,更重要的是可以增強自信心!
一、傳統線程機制的回顧
按照傳統的方式,創建一個線程有兩種方式:
1、創建一個類繼承Thread類,即創建一個Thread類的子類;
2、創建一個類實現Runnable接口,並把該類作爲Thread類的構造參數傳入;
在現實的開發中,一般使用的是第二種創建線程的方式,因爲它可以避免的Java中單繼承的侷限性,並且更加體現了面向對象的思想。
當創建一個線程時,往往是要啓動該線程執行特定的任務,那麼把要執行的任務寫在run()方法中,並且調用對象的start()方法啓動線程,即可。
下面代碼體現:
package itheima.thread.day01;
public class TranditionThread {
public static void main(String[] args) {
// 創建線程的第一中形式!
// 匿名內部類的形式
Thread thread = new Thread(){
@Override
public void run() {
for(int x=0;x<100;x++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
};
// 啓動線程
thread.start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"..........");
}
// 創建線程的第二種形式
Thread thread2 =new Thread(new Runnable(){
@Override
public void run() {
for(int x=0;x<100;x++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
});
// 啓動線程
thread2.start();
}
}
二、定時器
在JDK1.5以後,Java提供了定時器的API,對應的類爲Timer。Timer一種工具,線程用其安排以後在後臺線程中執行的任務。可安排任務執行一次,或者定期重複執行。
創建定時器,然後調用Schedule方法即可;TimerTask類用於存放定時器要執行的任務,並且把該任務作爲參數傳給Schedule方法即可。
下面代碼體現:
package itheima.thread.day01;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TraditionalTimerTest {
private static int count =0;
public static void main(String[] args) {
// 定時器,然後調度 任務Task、開始時間、間隔時間
/* new Timer().schedule(new TimerTask(){
@Override
public void run() {
System.out.println(new Date().getSeconds());
System.out.println("bombing.....");
}
}, 10,1000);//連環炸
*/
/* while(true){
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
// 連環炸彈,每隔2、4秒後爆炸,不斷循環
class MyTimerTask extends TimerTask{
@Override
public void run() {
count =(count+1)%2;
System.out.println(new Date().getSeconds());
System.out.println("bombing");
// 創建一個新的炸彈
new Timer().schedule(new MyTimerTask(),2000+2000*count);
}
}
new Timer().schedule(new MyTimerTask(), 2000);
}
}
三、傳統線程的同步互斥與通信
同步:在多線程中,當創建一個線程時,往往要給與該線程特定的執行任務,那麼這些執行任務的代碼就寫在run方法中;但是當多個線程同時操作同一份數據時,卻很容易出現線程的安全問題,比如:在售票系統中,有多個窗口同時售票,那麼每個窗口對應的是一個獨立的線程,票就是需要共同操作的數據,爲了不賣出相同票號的多張票,就必須要確定一個線程操作完了票數據之後另一個線程才能操作票數據,即票數據不能被多個線程同時操作。
同步互斥技術能解決這種線程安全問題。同步互斥可以通過同步函數與同步代碼塊體現,當被同步的函數是靜態函數時,使用的鎖是本類的字節碼,非靜態函數所使用的鎖是當前對象,即this。在同步代碼塊中,鎖可以任意指定,但要實現同步互斥,鎖必須是同一個鎖。
下面代碼體現:
package itheima.thread.day01;
public class TraditionalThreadSynchronized {
public static void main(String[] args) {
new TraditionalThreadSynchronized().init();
}
private void init(){
final Outputer outputer = new Outputer();
new Thread(new Runnable(){
@Override
public void run() {
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("22222222");
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("1111111111111");
}
}
}).start();
}
class Outputer{
// 當多個線程同時操作同一段代碼時,有可能一個線程還沒執行完,
// 另一個線程去搶着去執行,這,就會出現多線程的不安全問題!
// 同步函數:鎖:this,當前對象;靜態函數:本類的字節碼
// 同步代碼塊:必須保證各個線程使用的是同一鎖,
public synchronized void output(String name){
int len = name.length();
for(int i=0;i<len;i++){
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
通信:其實就是多個線程同時在操作一份數據,但是操作的動作不一樣,所以才需要線程之間的進行通信。通信必須以同步互斥爲前提,通信可以通過等待喚醒機制完成,wait()、notify()、notifyAll()。
下面代碼體現:
package itheima.thread.day01;
/*
* 子線程循環10次,接着主線程循環100,接着又回到子線程循環10次,
* 接着再回到主線程又循環100,如此循環50次
* */
public class TraditionalThreadCommunication {
public static void main(String[] args) {
final Bussiness business = new Bussiness();
// 創建一個線程
new Thread(new Runnable(){
@Override
public void run() {
for(int i=1;i<=50;i++){
business.sub(i);
}
}
}).start();
// 主線程
for(int i=1;i<=50;i++){
business.main(i);
}
}
}
class Bussiness{
private boolean bShouldSub = true;
// 互斥
public synchronized void sub(int i){
while(bShouldSub){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int j=1;j<=10;j++){
System.out.println("sub thread sequece of"+j+", loop of "+i);
}
bShouldSub = true;
this.notifyAll();//喚醒的是其中一個線程
}
// 互斥
public synchronized void main(int i){
while(!bShouldSub){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int j=1;j<=100;j++){
System.out.println("main thread sequece of"+j+", loop of "+i);
}
bShouldSub = false;
this.notifyAll();//喚醒的是其中一個線程
}
}
四、多個線程訪問共享對象和數據的方式
如果每個線程執行代碼相同,可以使用同一個Runnable對象,這個Runnable對象中存儲的就是那個共享數據。
當每個線程執行的代碼不同,可以用不同的Runnable對象,有以下兩種方式實現這些Runnable對象之間的數據共享:
1、將共享數據封裝在另外一個對象中,然後將這個對象逐一傳遞給各個Runnable對象。每個線程對共享數據的操作方法也分配到那個對象上去完成,這樣容易實現針對該數據進行的各個操作的互斥和通信。
2、這些Runnable對象作爲某一個類中的內部類,共享數據作爲這個外部類中的成員變量,每個線程對共享數據的操作方法也分配給外部類,以便實現對共享數據進行的各個操作的互斥和通信,作爲內部類的各個Runnable對象調用外部類的這些方法。
上面兩種方式的組合:將共享數據封裝在另外一個對象中,每個線程對共享數據的操作方法也分配到那個對象身上去完成,對象作爲這個外部類中的成員變量或方法中的局部變量,每個線程的Runnable對象作爲外部類中的成員內部類或局部內部類。
總之:要同步互斥的幾段代碼最好是分別放在幾個獨立的方法中,這些方法再放在同一個類中,這樣比較容易實現它們之間的同步互斥和通信。
下面代碼體現:
package itheima.thread.day01;
public class MultiThreadShareData {
// 設計4個線程,其中兩個線程每次j增加1,另外兩個線程對j每次減少1,
public static void main(String[] args) {
// 共享數據
final ShareData1 data1 = new ShareData1();
// 兩個線程同時操作一個數據
new Thread(new MyRunnable1(data1)).start();
new Thread(new MyRunnable2(data1)).start();
new Thread(new Runnable(){
@Override
public void run() {
data1.decrement();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
data1.increment();
}
}).start();
}
}
class MyRunnable1 implements Runnable{
// 共享數據
private ShareData1 data1;
public MyRunnable1(ShareData1 data1){
this.data1 = data1;
}
@Override
public void run() {
data1.decrement();
}
}
class MyRunnable2 implements Runnable{
// 共享數據
private ShareData1 data1;
public MyRunnable2(ShareData1 data1){
this.data1 = data1;
}
@Override
public void run() {
data1.increment();
}
}
class ShareData1/* implements Runnable*/{
/*
private int count=100;
@Override
// 如果多個線程要執行的代碼相同,可以放在同一個Runnable對象中
public void run() {
while(true){
count--;
}
}*/
private int j=100;
// 要被多線程操作
public synchronized void increment(){
j++;
System.out.println(Thread.currentThread().getName()+" j++ "+j);
}
public synchronized void decrement(){
j--;
System.out.println(Thread.currentThread().getName()+" j-- "+j);
}
}
五、線程範圍的共享變量
線程範圍的共享變量:多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行的又是另外一份數據。可以先把要共享的數據與線程的名稱以鍵值對的形式存儲在Map集合中,然後再取出。原理如下圖所示:
下面代碼體現:
package itheima.thread.day01;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class ThreadScopeShareData {
// 線程範圍內的數據共享
private static int data =0;
// 用於存儲線程名、數據的Map集合
private static Map<Thread,Integer> threadData = new HashMap<Thread,Integer>();
public static void main(String[] args) {
// 創建兩個線程
for(int i=0;i<2;i++){
new Thread(new Runnable(){
@Override
public void run() {
int data = new Random().nextInt(10)+1;
System.out.println(Thread.currentThread().getName()
+" has put data : "+data);
// 將線程名、數據存放到集合中
threadData.put(Thread.currentThread(), data);
new A().get();
new B().get();
}
}).start();
}
}
// 靜態內部類
static class A{
public void get(){
int data = threadData.get(Thread.currentThread());
System.out.println("A from "+Thread.currentThread().getName()
+" get data : "+data);
}
}
static class B{
public void get(){
int data = threadData.get(Thread.currentThread());
System.out.println("B from "+Thread.currentThread().getName()
+" get data : "+data);
}
}
}
ThreadLocal類專門爲實現線程範圍的數據共享的API,每個線程調用全局ThreadLocal對象的set方法,就相當於往其內部的map中增加一條記錄,key分別是各自的線程,value是各自的set方法傳進去的值。在線程結束時可以調用ThreadLocal.clear()方法,這樣會更快釋放內存,不調用也可以,因爲線程結束後也可以自動釋放相關的ThreadLocal變量。
下面代碼演示:
package itheima.thread.day01;
import java.util.Random;
public class ThreadLocalTest {
private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
private static ThreadLocal<MyThreadScopeData> myThreadScopeData =
new ThreadLocal<MyThreadScopeData>();
public static void main(String[] args) {
for(int i=0;i<2;i++){
new Thread(new Runnable(){
@Override
public void run() {
int data = new Random().nextInt(10)+1;
System.out.println(Thread.currentThread().getName()
+" has put data : "+data);
// 把數據存到當前線程中
x.set(data);
/* MyThreadScopeData myData = new MyThreadScopeData();
myData.setName("name : "+data);
myData.setAge(data);
myThreadScopeData.set(myData);
*/
MyThreadScopeData.getThreadInstance().setName("name:"+data);
MyThreadScopeData.getThreadInstance().setAge(data);
new A().get();
new B().get();
}
}).start();
}
}
static class A{
public void get(){
// 取當前線程中的數據
int data =x.get();
System.out.println("A from "+Thread.currentThread().getName()
+" get data : "+data);
/*MyThreadScopeData myData = myThreadScopeData.get();
System.out.println("A from "+Thread.currentThread().getName()
+" getMyData: "+myData.getName()+" , "+
myData.getAge()
);*/
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("A from "+Thread.currentThread().getName()
+" getMyData: "+myData.getName()+" , "+
myData.getAge()
);
}
}
static class B{
public void get(){
// 取當前線程中的數據
int data = x.get();
System.out.println("B from "+Thread.currentThread().getName()
+" get data : "+data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("B from "+Thread.currentThread().getName()
+" getMyData: "+myData.getName()+" , "+
myData.getAge()
);
}
}
}
class MyThreadScopeData{
private String name;
private int age;
// 單例模式:1、構造函數私有化,2、對外提供獲取該對象的方法
private MyThreadScopeData(){}
public static MyThreadScopeData getThreadInstance(){
MyThreadScopeData instance = map.get();
if(instance == null){
instance = new MyThreadScopeData();
map.set(instance);
}
return instance;
}
// private static MyThreadScopeData instance = new MyThreadScopeData();
private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
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;
}
}