代理模式UML類圖

代理模式
1. 靜態代理
-
-
-
-
- public interface IHello {
- public void sayHello();
- }
-
-
-
-
- public class Hello implements IHello
- {
- public void sayHello(){
- System.out.println("被代理的方法");
- }
- }
-
-
-
-
-
- public class StaticProxy implements IHello {
- IHello hello;
- public StaticProxy(IHello hello) {
- this.hello = hello;
- }
-
-
-
- public void sayHello() {
- System.out.println("在被代理的對象之前執行");
-
- hello.sayHello();
- System.out.println("在被代理的對象之前執行");
- }
- }
-
-
-
-
- public class Test {
- public static void main(String[] args) {
-
-
- IHello hello = new Hello();
-
- StaticProxy sp = new StaticProxy(hello);
-
- sp.sayHello();
- }
- }
/**
* 為被代理的類提供一個接口,是為了提高代理的通用性,凡是實現了該接口的類,都可以被代理
* 這里其實就是運用了java面向對象的多態性
*/
public interface IHello {
public void sayHello();
}
/**
* 被代理的類,最根本的想法就是想用另外一個類來代理這個類,給這個類添加一些額外的東西
* 我們只需要創建另外一個類引用這個類就行了
*/
public class Hello implements IHello
{
public void sayHello(){
System.out.println("被代理的方法");
}
}
/**
* 靜態代理類,其實就是(被代理類的)接口的另外一種實現,
* 用來代替原來的被代理類
* @author qiuxy
*/
public class StaticProxy implements IHello {
IHello hello;
public StaticProxy(IHello hello) {
this.hello = hello;
}
/**
* 重新實現了sayHello()方法,這種實現其實就是在被代理類實現該方法中添加一些額外的東西, 以實現代理的作用
*/
public void sayHello() {
System.out.println("在被代理的對象之前執行");
// 被代理的對象執行方法
hello.sayHello();
System.out.println("在被代理的對象之前執行");
}
}
/**
* 測試類
* @author qiuxy
*/
public class Test {
public static void main(String[] args) {
//產生一個被代理對象,只要實現了Ihello接口的對象,都可以成為被代理對象
//這里就是利用接口的好處,但這個也有局限性,就是只限于某種接口
IHello hello = new Hello();
//產生一個代理對象
StaticProxy sp = new StaticProxy(hello);
//執行代理對象的sayHello()方法,這個方法在被代理的方法前后添加了其他代碼
sp.sayHello();
}
}
2. 動態代理
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
-
-
-
-
-
- public class MyProxyHandler implements InvocationHandler {
-
- Object delegate;
-
- public MyProxyHandler(Object delegate) {
- this.delegate = delegate;
- }
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println("我在被代理的方法之前執行");
-
- method.invoke(delegate, args);
- System.out.println("我在被代理的方法之后執行");
- return null;
- }
- }
- import java.lang.reflect.Proxy;
-
-
-
- public class Test
- {
- public static void main(String[] args)
- {
- Hello hello = new Hello();
-
- MyProxyHandler mph = new MyProxyHandler(hello);
-
-
- IHello myProxy = (IHello)Proxy.newProxyInstance(hello.getClass().getClassLoader() , hello.getClass().getInterfaces(), mph);
-
- myProxy.sayHello();
- }
-
- }
代理模式
代理模式的作用是:為其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個客戶不想或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用。
代理模式一般涉及到的角色有:
抽象角色:聲明真實對象和代理對象的共同接口;
代理角色:代理對象角色內部含有對真實對象的引用,從而可以操作真實對象,同時代理對象提供與真實對象相同的接口以便在任何時刻都能代替真實對象。同時,代理對象可以在執行真實對象操作時,附加其他的操作,相當于對真實對象進行封裝。
真實角色:代理角色所代表的真實對象,是我們最終要引用的對象。(參見文獻1)
以下以《Java與模式》中的示例為例:
抽象角色:
abstract public class Subject
{
abstract public void request();
}
真實角色:實現了Subject的request()方法。
public class RealSubject extends Subject
{
public RealSubject()
{
}
public void request()
{
System.out.println("From real subject.");
}
}
代理角色:
public class ProxySubject extends Subject
{
private RealSubject realSubject; //以真實角色作為代理角色的屬性
public ProxySubject()
{
}
public void request() //該方法封裝了真實對象的request方法
{
preRequest();
if( realSubject == null )
{
realSubject = new RealSubject();
}
realSubject.request(); //此處執行真實對象的request方法
postRequest();
}
private void preRequest()
{
//something you want to do before requesting
}
private void postRequest()
{
//something you want to do after requesting
}
}
客戶端調用:
Subject sub=new ProxySubject();
Sub.request();
由以上代碼可以看出,客戶實際需要調用的是RealSubject類的request()方法,現在用ProxySubject來代理RealSubject類,同樣達到目的,同時還封裝了其他方法(preRequest(),postRequest()),可以處理一些其他問題。
另外,如果要按照上述的方法使用代理模式,那么真實角色必須是事先已經存在的,并將其作為代理對象的內部屬性。但是實際使用時,一個真實角色必須對應一個代理角色,如果大量使用會導致類的急劇膨脹;此外,如果事先并不知道真實角色,該如何使用代理呢?這個問題可以通過Java的動態代理類來解決。
2.動態代理類
Java動態代理類位于Java.lang.reflect包下,一般主要涉及到以下兩個類:
(1). Interface InvocationHandler:該接口中僅定義了一個方法Object:invoke(Object obj,Method method, J2EEjava語言JDK1.4APIjavalangObject.html">Object[] args)。在實際使用時,第一個參數obj一般是指代理類,method是被代理的方法,如上例中的request(),args為該方法的參數數組。這個抽象方法在代理類中動態實現。
(2).Proxy:該類即為動態代理類,作用類似于上例中的ProxySubject,其中主要包含以下內容:
Protected Proxy(InvocationHandler h):構造函數,估計用于給內部的h賦值。
Static Class getProxyClass (ClassLoader loader, Class[] interfaces):獲得一個代理類,其中loader是類裝載器,interfaces是真實類所擁有的全部接口的數組。
Static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h):返回代理類的一個實例,返回后的代理類可以當作被代理類使用(可使用被代理類的在Subject接口中聲明過的方法)。
所謂Dynamic Proxy是這樣一種class:它是在運行時生成的class,在生成它時你必須提供一組interface給它,然后該class就宣稱它實現了這些interface。你當然可以把該class的實例當作這些interface中的任何一個來用。當然啦,這個Dynamic Proxy其實就是一個Proxy,它不會替你作實質性的工作,在生成它的實例時你必須提供一個handler,由它接管實際的工作。(參見文獻3)
在使用動態代理類時,我們必須實現InvocationHandler接口,以第一節中的示例為例:
抽象角色(之前是抽象類,此處應改為接口):
public interface Subject
{
abstract public void request();
}
具體角色RealSubject:同上;
代理角色:
import java.lang.reflect.Method;
import java.lang.reflect.InvocationHandler;
public class DynamicSubject implements InvocationHandler {
private Object sub;
public DynamicSubject() {
}
public DynamicSubject(Object obj) {
sub = obj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before calling " + method);
method.invoke(sub,args);
System.out.println("after calling " + method);
return null;
}
}
該代理類的內部屬性為Object類,實際使用時通過該類的構造函數DynamicSubject(Object obj)對其賦值;此外,在該類還實現了invoke方法,該方法中的
method.invoke(sub,args);
其實就是調用被代理對象的將要被執行的方法,方法參數sub是實際的被代理對象,args為執行被代理對象相應操作所需的參數。通過動態代理類,我們可以在調用之前或之后執行一些相關操作。
客戶端:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Client
{
static public void main(String[] args) throws Throwable
{
RealSubject rs = new RealSubject(); //在這里指定被代理類
InvocationHandler ds = new DynamicSubject(rs); //初始化代理類
Class cls = rs.getClass();
//以下是分解步驟
/*
Class c = Proxy.getProxyClass(cls.getClassLoader(),cls.getInterfaces()) ;
Constructor ct=c.getConstructor(new Class[]{InvocationHandler.class});
Subject subject =(Subject) ct.newInstance(new Object[]{ds});
*/
//以下是一次性生成
Subject subject = (Subject) Proxy.newProxyInstance(cls.getClassLoader(),
cls.getInterfaces(),ds );
subject.request();
}
通過這種方式,被代理的對象(RealSubject)可以在運行時動態改變,需要控制的接口(Subject接口)可以在運行時改變,控制的方式(DynamicSubject類)也可以動態改變,從而實現了非常靈活的動態代理關系(參見文獻2)。
代理和AOP一.起源
有時,我們在寫一些功能方法的時候,需要加上特定的功能.比如說在方法調用的前后加上日志的操作,或者是事務的開啟與關閉.對于一個方法來說,很簡單,只要在需要的地方增加一些代碼就OK.但是如果有很多方法都需要增加這種特定的操作呢?
沒錯,將這些特定的代碼抽象出來,并且提供一個接口供調用者使用:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("記錄日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 記錄日志的操作
System.out.println("記錄日志...");
}
}
那么在其他的方法中,就可以使用RecordLog.recordLog()方法了.但你會發現,這仍不是個好的設計,因為在我們的代碼里到處充塞著
RecordLog.recordLog()這樣的語句:
- public class A
- {
- public void a()
- {
-
- RecordLog.recordLog();
-
-
- }
- }
- public class B
- {
- public void b()
- {
-
- RecordLog.recordLog();
-
-
- }
- }
- ......
public class A
{
public void a()
{
// 1.記錄日志
RecordLog.recordLog();
// 2.類A的方法a的操作
}
}
public class B
{
public void b()
{
// 1.記錄日志
RecordLog.recordLog();
// 2.類B的方法b的操作
}
}
......
這樣雖然會在一定程度減輕代碼量,但你會發現,仍有大量的地方有重復的代碼出現!這絕對不是優雅的寫法!
為了避免這種吃力不討好的現象發生,“代理”粉墨登場了.
二.傳統的代理.靜態的代理.面向接口編程
同樣為了實現以上的功能,我們在設計的時候做了個小小的改動.
2.1 抽象出來的記錄日志的類:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("記錄日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 記錄日志的操作
System.out.println("記錄日志...");
}
}
2.2 設計了一個接口:
- public interface PeopleInfo
- {
- public void getInfo();
- }
public interface PeopleInfo
{
public void getInfo();
}
該接口只提供了待實現的方法.
2.3 實現該接口的類:
- public class PeopleInfoImpl implements PeopleInfo
- {
- private String name;
-
- private int age;
-
-
- public PeopleInfoImpl(String name, int age)
- {
- this.name = name;
- this.age = age;
- }
-
- public void getInfo()
- {
-
- System.out.println("我是" + name + ",今年" + age + "歲了.");
- }
- }
public class PeopleInfoImpl implements PeopleInfo
{
private String name;
private int age;
// 構造函數
public PeopleInfoImpl(String name, int age)
{
this.name = name;
this.age = age;
}
public void getInfo()
{
// 方法的具體實現
System.out.println("我是" + name + ",今年" + age + "歲了.");
}
}
這個類僅僅是實現了PeopleInfo接口而已.平平實實.好了.關鍵的地方來了.就在下面!
2.4 創建一個代理類:
- public class PeopleInfoProxy implements PeopleInfo
- {
-
- private PeopleInfo peopleInfo;
-
-
- public RecordLogProxy(PeopleInfo peopleInfo)
- {
- this.peopleInfo = peopleInfo;
- }
-
-
- public void record()
- {
-
- RecordLog.recordLog();
-
-
- peopleInfo.getInfo();
- }
- }
public class PeopleInfoProxy implements PeopleInfo
{
// 接口的引用
private PeopleInfo peopleInfo;
// 構造函數 .針對接口編程,而非針對具體類
public RecordLogProxy(PeopleInfo peopleInfo)
{
this.peopleInfo = peopleInfo;
}
// 實現接口中的方法
public void record()
{
// 1.記錄日志
RecordLog.recordLog();
// 2.方法的具體實現
peopleInfo.getInfo();
}
}
這個是類是一個代理類,它同樣實現了PeopleInfo接口.比較特殊的地方在于這個類中有一個接口的引用private PeopleInfo peopleInfo;通過
這個引用,可以調用實現了該接口的類的實例的方法!
而不管是誰,只要實現了PeopleInfo這個接口,都可以被這個引用所引用.也就是說,這個代理類可以代理任何實現了接口的PeopleInfo的類.具體
如何實現,請看下面:
2.5 Main
- public class Main
- {
- public static void main(String[] args)
- {
-
- PeopleInfoImpl peopleInfoImpl = new PeopleInfoImpl("Rock",24);
-
-
- PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy(PeopleInfoImpl);
-
-
- peopleInfoProxy.getInfo();
- }
- }
public class Main
{
public static void main(String[] args)
{
// new了一個對象
PeopleInfoImpl peopleInfoImpl = new PeopleInfoImpl("Rock",24);
// 代理該對象
PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy(PeopleInfoImpl);
// 調用代理類的方法.輸入的是目標類(即被代理類的方法的實現)
peopleInfoProxy.getInfo();
}
}
這樣,輸出的結果將是:
記錄日志...
我是Rock,今年24歲了.
由這個例子可見,這么做了之后不但省略了很多代碼,而且不必要知道具體是由哪個類來執行方法.只需實現了特定的接口,代理類就可以打點一切
了.這就是面向接口的威力!HOHO...
三.動態代理.Java的動態機制.
面向接口的編程確實讓我們省了不少心,只要實現一個特定的接口,就可以處理很多的相關的類了.
不過,這總是要實現一個“特定”的接口,如果有很多很多這樣的接口需要被實現...也是件比較麻煩的事情.
好在,JDK1.3起,就有了動態代理機制,主要有以下兩個類和一個接口:
- java.lang.reflect.Proxy
- java.lang.reflect.Method
- java.lang.reflect.InvocationHandler
java.lang.reflect.Proxy
java.lang.reflect.Method
java.lang.reflect.InvocationHandler
所謂動態代理,就是JVM在內存中動態的構造代理類.說的真是玄,還是看看代碼吧.
3.1 抽象出來的記錄日志的類:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("記錄日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 記錄日志的操作
System.out.println("記錄日志...");
}
}
3.2 設計了一個接口:
- public interface PeopleInfo
- {
- public void getInfo();
- }
public interface PeopleInfo
{
public void getInfo();
}
該接口只提供了待實現的方法.
3.3 實現該接口的類:
- public class PeopleInfoImpl implements PeopleInfo
- {
- private String name;
-
- private int age;
-
-
- public PeopleInfoImpl(String name, int age)
- {
- this.name = name;
- this.age = age;
- }
-
- public void getInfo()
- {
-
- System.out.println("我是" + name + ",今年" + age + "歲了.");
- }
- }
public class PeopleInfoImpl implements PeopleInfo
{
private String name;
private int age;
// 構造函數
public PeopleInfoImpl(String name, int age)
{
this.name = name;
this.age = age;
}
public void getInfo()
{
// 方法的具體實現
System.out.println("我是" + name + ",今年" + age + "歲了.");
}
}
一直到這里,都和第二節沒區別,好嘛,下面就是關鍵喲.
3.4 創建一個代理類,實現了接口InvocationHandler:
- public class PeopleInfoProxy implements InvocationHandler
- {
-
- private Object target;
-
-
- public Object bind(Object targer)
- {
- this.target = target;
-
-
- return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
- }
-
-
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
- {
- Object result = null;
-
-
- RecordLog.recordLog();
-
-
- result = method.invoke(target, args);
-
-
-
-
- return result;
- }
- }
public class PeopleInfoProxy implements InvocationHandler
{
// 定義需要被代理的目標對象
private Object target;
// 將目標對象與代理對象綁定
public Object bind(Object targer)
{
this.target = target;
// 調用Proxy的newProxyInstance方法產生代理類實例
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
// 實現接口InvocationHandler的invoke方法
// 該方法將在目標類的被代理方法被調用之前,自動觸發
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Object result = null;
// 1.目標類的被代理方法被調用之前,可以做的操作
RecordLog.recordLog();
// 2.方法的具體實現
result = method.invoke(target, args);
// 3.還可以在方法調用之后加上的操作
// 自己補充
return result;
}
}
關于Proxy, Method, InvocationHandler的具體說明,請參見JDK_API.
只對代碼中關鍵部分做些解釋說明:
3.4.1
- Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
表示生成目標類的代理類,傳入的參數有目標類的ClassLoader, 目標類的接口列表, 和實現了接口InvocationHandler的代理類.
這樣,bind方法就得到了目標類的代理類.
3.4.2
- method.invoke(target, args);
method.invoke(target, args);
目標類的被代理方法在被代用前,會自動調用InvocationHandler接口的invoke方法.
在該方法中,我們可以對目標類的被代理方法進行加強,比如說在其前后加上事務的開啟和關閉等等.
這段代碼才是真正調用目標類的被代理方法.
就這樣,我們不用實現其他任何的接口,理論上就能代理所有類了.調用的方式如下:
3.5 Main:
- public class Main
- {
- public static void main(String[] args)
- {
- PeopleInfo peopleInfo = null;
-
- PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy();
-
-
- Object obj = peopleInfoProxy.bind(new PeopleInfoImpl("Rock", 24));
-
- if(obj instanceof PeopleInfo)
- {
- peopleInfo = (PeopleInfo)obj;
- }
- peopleInfo.getInfo();
- }
- }
public class Main
{
public static void main(String[] args)
{
PeopleInfo peopleInfo = null;
PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy();
// 傳入的參數是目標類實例,生成代理類實例,類型為Object
Object obj = peopleInfoProxy.bind(new PeopleInfoImpl("Rock", 24));
if(obj instanceof PeopleInfo)
{
peopleInfo = (PeopleInfo)obj;
}
peopleInfo.getInfo();
}
}
執行結果和上一節一樣.
這就是使用Java動態代理機制的基本概述.而下一節,將要把Dynamic Proxy(動態代理)和AOP聯系起來.
四.AOP概述.Spring的AOP.
AOP(Aspect Oriented Programming)面向切面編程.是一種比較新穎的設計思想.是對OOP(Object Orientd Programming)面向對象編程的一種有益的補充.
4.1 OOP和AOP
OOP對業務處理過程中的實體及其屬性和行為進行了抽象封裝,以獲得更加清晰高效果的邏輯劃分.研究的是一種“靜態的”領域.
AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段.研究的是一種“動態的”領域.
舉例說,某個網站(5016?)用戶User類又可分為好幾種,區長,管理員,斑竹和普通水友.我們把這些會員的特性進行提取進行封裝,這是OOP.
而某一天,區長開會了,召集斑竹等級以上的會員參與,這樣,普通水友就不能訪問相關資源.
我們怎么做到讓普通水友訪問不了資源,而斑竹等級以上會員可以訪問呢.
權限控制.對,權限.當水友們進行操作的時候,我們給他的身份進行權限的判斷.
請注意,當且僅需水友門執行了操作的時候,我們才需要進行權限判斷,也就是說,這是發生在一個業務處理的過程中的一個片面.
我們對這一個片面進行編程,就是AOP!
我這樣,你應該能理解吧.
4.2 AOP的基本術語
4.2.1 切面Aspect
業務處理過程中的一個截面.就像權限檢查.
通過切面,可以將不同層面的問題隔離開:瀏覽帖子和權限檢查兩者互不相干.
這樣一來,也就降低了耦合性,我們可以把注意力集中到各自的領域中.
上兩節的例子中,getInfo()和recordLog()就是兩個領域的方法,應該處于切面的不同端.哎呀,不知不覺間,我們就用了AOP.呵呵...
4.2.2 連接點JoinPoint
程序運行中的某個階段點.如某個方法的調用,或者異常的拋出等.
在前面,我們總是在getInfo()的前后加了recordLog()等操作,這個調用getInfo()就是連接點.
4.2.3 處理邏輯Advice
在某個連接點采取的邏輯.
這里的邏輯有三種:
I. Around 在連接點前后插入預處理和后處理過程.
II. Before 在連接點前插入預處理過程.
III.Throw 在連接點拋出異常的時候進行異常處理.
4.2.4 切點PointCut
一系列連接點的集合,它指明處理邏輯Advice將在何在被觸發.
4.3 Spring中的AOP
Spring提供內置AOP支持.是基于動態AOP機制的實現.
所謂動態AOP,其實就是動態Proxy模式,在目標對象的方法前后插入相應的代碼.(比如說在getInfo()前后插入的recordLog())
Spring AOP中的動態Proxy模式,是基于Java Dynamic Proxy(面向Interface)和CGLib(面向Class)的實現.
為什么要分面向接口和面向類呢.
還記得我們在生成代理類的代碼嗎:
- Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
這里面的參數不許為空,也就是說:obj.getClass().getInterfaces()必有值,即目標類一定要實現某個接口.
有了這些,JVM在內存中就動態的構造出代理出來.
而沒有實現任何接口的類,就必須使用CGLib來動態構造代理類.值得一提的是,CGLib構造的代理類是目標類的一個子類.
4.4 相關工程簡解
Spring的相關知識不應該在這里講,難度系數過大.這里只給個簡單例子.供參考.
4.4.1 準備工作
打開Eclipse.新建Java工程,取名為AOP_Proxy.完成.
復制spring-2.0.jar.粘貼到AOP_Proxy下.
右擊AOP_Proxy-->屬性-->Java構建路徑-->庫-->添加JAR-->找spring-2.0.jar-->添加確定.
復制commons-logging.jar.粘貼到AOP_Proxy下.
右擊AOP_Proxy-->屬性-->Java構建路徑-->庫-->添加JAR-->找commons-logging.jar-->添加確定.
4.4.2 寫代碼
代碼略.配置文件略.
4.4.3 導入工程的步驟
新建工程AOP_Pro
/Files/qileilove/AOP_Proxy.rarxy-->完成-->右擊AOP_Proxy-->導入-->常規-->文件系統-->找到項目文件,導入完成.
兩個jar包和項目文件(項目文件需要先解壓).
摘要: 不同的平臺,內存模型是不一樣的,但是jvm的內存模型規范是統一的。其實java的多線程并發問題最終都會反映在java的內存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改??偨Yjava的內存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器并不是每次處理數據都是取內存的。JVM定義了自己的內存模型,屏蔽了底層平臺內存管理細節,對于ja...
閱讀全文
我們可以在計算機上運行各種計算機軟件程序。每一個運行的程序可能包括多個獨立運行的線程(Thread)。
線程(Thread)是一份獨立運行的程序,有自己專用的運行棧。
線程有可能和其他線程共享一些資源,比如,內存,文件,數據庫等。
當多個線程同時讀寫同一份共享資源的時候,可能會引起沖突。這時候,我們需要引入線程“同步”機制,即各位線程之間要有個先來后到,不能一窩蜂擠上去搶作一團。
同步這個詞是從英文synchronize(使同時發生)翻譯過來的。我也不明白為什么要用這個很容易引起誤解的詞。既然大家都這么用,咱們也就只好這么將就。
線程同步的真實意思和字面意思恰好相反
。線程同步的真實意思,其實是“排隊”:幾個線程之間要排隊,一個一個對共享資源進行操作,而不是同時進行操作。 因此,關于線程同步,需要牢牢記住的第一點是:
線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程“同步”執行。這可真是個無聊的繞口令。 關于線程同步,需要牢牢記住的第二點是 “
共享”這兩個字。只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那么就根本沒有同步的必要。
關于線程同步,需要牢牢記住的第三點是,
只有“變量”才需要同步訪問。如果共享的資源是固定不變的,那么就相當于“常量”,線程同時讀取常量也不需要同步。至少一個線程修改共享資源,這樣的情況下,線程之間就需要同步。
關于線程同步,需要牢牢記住的第四點是:
多個線程訪問共享資源的代碼有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。 為了加深理解,下面舉幾個例子。
有兩個采購員,他們的工作內容是相同的,都是遵循如下的步驟:
(1)到市場上去,尋找并購買有潛力的樣品。
(2)回到公司,寫報告。
這兩個人的工作內容雖然一樣,他們都需要購買樣品,他們可能買到同樣種類的樣品,但是他們絕對不會購買到同一件樣品,他們之間沒有任何共享資源。所以,他們可以各自進行自己的工作,互不干擾。
這兩個采購員就相當于兩個線程;兩個采購員遵循相同的工作步驟,相當于這兩個線程執行同一段代碼。
下面給這兩個采購員增加一個工作步驟。采購員需要根據公司的“布告欄”上面公布的信息,安排自己的工作計劃。
這兩個采購員有可能同時走到布告欄的前面,同時觀看布告欄上的信息。這一點問題都沒有。因為布告欄是只讀的,這兩個采購員誰都不會去修改布告欄上寫的信息。
下面增加一個角色。一個辦公室行政人員這個時候,也走到了布告欄前面,準備修改布告欄上的信息。
如果行政人員先到達布告欄,并且正在修改布告欄的內容。兩個采購員這個時候,恰好也到了。這兩個采購員就必須等待行政人員完成修改之后,才能觀看修改后的信息。
如果行政人員到達的時候,兩個采購員已經在觀看布告欄了。那么行政人員需要等待兩個采購員把當前信息記錄下來之后,才能夠寫上新的信息。
上述這兩種情況,行政人員和采購員對布告欄的訪問就需要進行同步。因為其中一個線程(行政人員)修改了共享資源(布告欄)。而且我們可以看到,行政人員的工作流程和采購員的工作流程(執行代碼)完全不同,但是由于他們訪問了同一份可變共享資源(布告欄),所以他們之間需要同步。
同步鎖 前面講了為什么要線程同步,下面我們就來看如何才能線程同步。
線程同步的基本實現思路還是比較容易理解的。我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個線程獲取了這把鑰匙,才有權利訪問該共享資源。
生活中,我們也可能會遇到這樣的例子。一些超市的外面提供了一些自動儲物箱。每個儲物箱都有一把鎖,一把鑰匙。人們可以使用那些帶有鑰匙的儲物箱,把東西放到儲物箱里面,把儲物箱鎖上,然后把鑰匙拿走。這樣,該儲物箱就被鎖住了,其他人不能再訪問這個儲物箱。(當然,真實的儲物箱鑰匙是可以被人拿走復制的,所以不要把貴重物品放在超市的儲物箱里面。于是很多超市都采用了電子密碼鎖。)
線程同步鎖這個模型看起來很直觀。但是,還有一個嚴峻的問題沒有解決,這個同步鎖應該加在哪里?
當然是加在共享資源上了。反應快的讀者一定會搶先回答。
沒錯,
如果可能,我們當然盡量把同步鎖加在共享資源上。一些比較完善的共享資源,比如,文件系統,數據庫系統等,自身都提供了比較完善的同步鎖機制。我們不用另外給這些資源加鎖,這些資源自己就有鎖。
但是,大部分情況下,我們在代碼中訪問的共享資源都是比較簡單的共享對象。這些對象里面沒有地方讓我們加鎖。
讀者可能會提出建議:為什么不在每一個對象內部都增加一個新的區域,專門用來加鎖呢?這種設計理論上當然也是可行的。問題在于,線程同步的情況并不是很普遍。如果因為這小概率事件,在所有對象內部都開辟一塊鎖空間,將會帶來極大的空間浪費。得不償失。
于是,現代的編程語言的設計思路都是把同步鎖加在代碼段上。確切的說,是把同步鎖加在“訪問共享資源的代碼段”上。這一點一定要記住,
同步鎖是加在代碼段上的。
同步鎖加在代碼段上,就很好地解決了上述的空間浪費問題。但是卻增加了模型的復雜度,也增加了我們的理解難度。
現在我們就來仔細分析“同步鎖加在代碼段上”的線程同步模型。
首先,我們已經解決了同步鎖加在哪里的問題。我們已經確定,
同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。
其次,我們要解決的問題是,我們應該在代碼段上加什么樣的鎖。這個問題是重點中的重點。這是我們尤其要注意的問題:
訪問同一份共享資源的不同代碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那么根本就起不到同步的作用,沒有任何意義。
這就是說,
同步鎖本身也一定是多個線程之間的共享對象。
Java語言的synchronized關鍵字
為了加深理解,舉幾個代碼段同步的例子。
不同語言的同步鎖模型都是一樣的。只是表達方式有些不同。這里我們以當前最流行的Java語言為例。Java語言里面用synchronized關鍵字給代碼段加鎖。整個語法形式表現為
synchronized(同步鎖) {
// 訪問共享資源,需要同步的代碼段
}
這里尤其要注意的就是,同步鎖本身一定要是共享的對象。
… f1() {
Object lock1 = new Object(); // 產生一個同步鎖
synchronized(lock1){
// 代碼段 A
// 訪問共享資源 resource1
// 需要同步
}
}
上面這段代碼沒有任何意義。因為那個同步鎖是在函數體內部產生的。每個線程調用這段代碼的時候,都會產生一個新的同步鎖。那么多個線程之間,使用的是不同的同步鎖。根本達不到同步的目的。
同步代碼一定要寫成如下的形式,才有意義。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 需要同步
}
你不一定要把同步鎖聲明為static或者public,但是你一定要保證相關的同步代碼之間,一定要使用同一個同步鎖。
講到這里,你一定會好奇,這個同步鎖到底是個什么東西。為什么隨便聲明一個Object對象,就可以作為同步鎖?
在Java里面,同步鎖的概念就是這樣的。
任何一個Object Reference都可以作為同步鎖。我們可以把Object Reference理解為對象在內存分配系統中的內存地址。因此,要保證同步代碼段之間使用的是同一個同步鎖,我們就要保證這些同步代碼段的synchronized關鍵字使用的是同一個Object Reference,同一個內存地址。這也是為什么我在前面的代碼中聲明lock1的時候,使用了final關鍵字,這就是為了保證lock1的Object Reference在整個系統運行過程中都保持不變。
一些求知欲強的讀者可能想要繼續深入了解synchronzied(同步鎖)的實際運行機制。Java虛擬機規范中(你可以在google用“JVM Spec”等關鍵字進行搜索),有對synchronized關鍵字的詳細解釋。synchronized會編譯成 monitor enter, … monitor exit之類的指令對。Monitor就是實際上的同步鎖。每一個Object Reference在概念上都對應一個monitor。
這些實現細節問題,并不是理解同步鎖模型的關鍵。我們繼續看幾個例子,加深對同步鎖模型的理解。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 需要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 B
// 訪問共享資源 resource1
// 需要同步
}
}
上述的代碼中,代碼段A和代碼段B就是同步的。因為它們使用的是同一個同步鎖lock1。
如果有10個線程同時執行代碼段A,同時還有20個線程同時執行代碼段B,那么這30個線程之間都是要進行同步的。
這30個線程都要競爭一個同步鎖lock1。同一時刻,只有一個線程能夠獲得lock1的所有權,只有一個線程可以執行代碼段A或者代碼段B。其他競爭失敗的線程只能暫停運行,
進入到該同步鎖的就緒(Ready)隊列。
每一個同步鎖下面都掛了幾個線程隊列,包括
就緒(Ready)隊列,
待召(Waiting)隊列等。比如,lock1對應的就緒隊列就可以叫做lock1 - ready queue。每個隊列里面都可能有多個暫停運行的線程。
注意,
競爭同步鎖失敗的線程進入的是該同步鎖的就緒(Ready)隊列,而不是后面要講述的待召隊列(Waiting Queue,也可以翻譯為等待隊列)。就緒隊列里面的線程總是時刻準備著競爭同步鎖,時刻準備著運行。而待召隊列里面的線程則只能一直等待,直到等到某個信號的通知之后,才能夠轉移到就緒隊列中,準備運行。
成功獲取同步鎖的線程,執行完同步代碼段之后,會釋放同步鎖。該同步鎖的就緒隊列中的其他線程就繼續下一輪同步鎖的競爭。成功者就可以繼續運行,失敗者還是要乖乖地待在就緒隊列中。
因此,線程同步是非常耗費資源的一種操作。我們要盡量控制線程同步的代碼段范圍。同步的代碼段范圍越小越好。我們用一個名詞“同步粒度”來表示同步代碼段的范圍。
同步粒度 在Java語言里面,我們可以直接把synchronized關鍵字直接加在函數的定義上。
比如。
… synchronized … f1() {
// f1 代碼段
}
這段代碼就等價于
… f1() {
synchronized(this){ // 同步鎖就是對象本身
// f1 代碼段
}
}
同樣的原則適用于靜態(static)函數
比如。
… static synchronized … f1() {
// f1 代碼段
}
這段代碼就等價于
…static … f1() {
synchronized(Class.forName(…)){ // 同步鎖是類定義本身
// f1 代碼段
}
}
但是,
我們要盡量避免這種直接把synchronized加在函數定義上的偷懶做法。因為我們要控制同步粒度。同步的代碼段越小越好。synchronized控制的范圍越小越好。 我們不僅要在縮小同步代碼段的長度上下功夫,我們同時還要注意細分同步鎖。 比如,下面的代碼
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 A
// 訪問共享資源 resource1
// 需要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 B
// 訪問共享資源 resource1
// 需要同步
}
}
… f3() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 C
// 訪問共享資源 resource2
// 需要同步
}
}
… f4() {
synchronized(lock1){ // lock1 是公用同步鎖
// 代碼段 D
// 訪問共享資源 resource2
// 需要同步
}
}
上述的4段同步代碼,使用同一個同步鎖lock1。所有調用4段代碼中任何一段代碼的線程,都需要競爭同一個同步鎖lock1。
我們仔細分析一下,發現這是沒有必要的。
因為f1()的代碼段A和f2()的代碼段B訪問的共享資源是resource1,f3()的代碼段C和f4()的代碼段D訪問的共享資源是resource2,它們沒有必要都競爭同一個同步鎖lock1。我們可以增加一個同步鎖lock2。f3()和f4()的代碼可以修改為:
public static final Object lock2 = new Object();
… f3() {
synchronized(lock2){ // lock2 是公用同步鎖
// 代碼段 C
// 訪問共享資源 resource2
// 需要同步
}
}
… f4() {
synchronized(lock2){ // lock2 是公用同步鎖
// 代碼段 D
// 訪問共享資源 resource2
// 需要同步
}
}
這樣,f1()和f2()就會競爭lock1,而f3()和f4()就會競爭lock2。這樣,分開來分別競爭兩個鎖,就可以大大較少同步鎖競爭的概率,從而減少系統的開銷。
信號量 同步鎖模型只是最簡單的同步模型。同一時刻,只有一個線程能夠運行同步代碼。
有的時候,我們希望處理更加復雜的同步模型,比如生產者/消費者模型、讀寫同步模型等。這種情況下,同步鎖模型就不夠用了。我們需要一個新的模型。這就是我們要講述的信號量模型。
信號量模型的工作方式如下:線程在運行的過程中,可以主動停下來,等待某個信號量的通知;這時候,該線程就進入到該信號量的待召(Waiting)隊列當中;等到通知之后,再繼續運行。
很多語言里面,同步鎖都由專門的對象表示,對象名通常叫Monitor。
同樣,在很多語言中,信號量通常也有專門的對象名來表示,比如,Mutex,Semphore。
信號量模型要比同步鎖模型復雜許多。一些系統中,信號量甚至可以跨進程進行同步。另外一些信號量甚至還有計數功能,能夠控制同時運行的線程數。
我們沒有必要考慮那么復雜的模型。所有那些復雜的模型,都是最基本的模型衍生出來的。只要掌握了最基本的信號量模型——“等待/通知”模型,復雜模型也就迎刃而解了。
我們還是以Java語言為例。Java語言里面的同步鎖和信號量概念都非常模糊,沒有專門的對象名詞來表示同步鎖和信號量,只有兩個同步鎖相關的關鍵字——volatile和synchronized。
這種模糊雖然導致概念不清,但同時也避免了Monitor、Mutex、Semphore等名詞帶來的種種誤解。我們不必執著于名詞之爭,可以專注于理解實際的運行原理。
在Java語言里面,任何一個Object Reference都可以作為同步鎖。同樣的道理,任何一個Object Reference也可以作為信號量。
Object對象的
wait()方法就是等待通知,Object對象的notify()方法就是發出通知。 具體調用方法為
(1)等待某個信號量的通知
public static final Object signal = new Object();
… f1() {
synchronized(singal) { // 首先我們要獲取這個信號量。這個信號量同時也是一個同步鎖
// 只有成功獲取了signal這個信號量兼同步鎖之后,我們才可能進入這段代碼
signal.wait(); // 這里要放棄信號量。本線程要進入signal信號量的待召(Waiting)隊列
// 可憐。辛辛苦苦爭取到手的信號量,就這么被放棄了
// 等到通知之后,從待召(Waiting)隊列轉到就緒(Ready)隊列里面
// 轉到了就緒隊列中,離CPU核心近了一步,就有機會繼續執行下面的代碼了。
// 仍然需要把signal同步鎖競爭到手,才能夠真正繼續執行下面的代碼。命苦啊。
…
}
}
需要注意的是,上述代碼中的signal.wait()的意思。signal.wait()很容易導致誤解。signal.wait()的意思并不是說,signal開始wait,而是說,運行這段代碼的當前線程開始wait這個signal對象,即進入signal對象的待召(Waiting)隊列。
(2)發出某個信號量的通知
… f2() {
synchronized(singal) { // 首先,我們同樣要獲取這個信號量。同時也是一個同步鎖。
// 只有成功獲取了signal這個信號量兼同步鎖之后,我們才可能進入這段代碼
signal.notify(); // 這里,我們通知signal的待召隊列中的某個線程。
// 如果某個線程等到了這個通知,那個線程就會轉到就緒隊列中
// 但是本線程仍然繼續擁有signal這個同步鎖,本線程仍然繼續執行
// 嘿嘿,雖然本線程好心通知其他線程,
// 但是,本線程可沒有那么高風亮節,放棄到手的同步鎖
// 本線程繼續執行下面的代碼
…
}
}
需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal這個對象本身。而是通知正在等待signal信號量的其他線程。
以上就是Object的wait()和notify()的基本用法。
實際上,wait()還可以定義等待時間,當線程在某信號量的待召隊列中,等到足夠長的時間,就會等無可等,無需再等,自己就從待召隊列轉移到就緒隊列中了。
另外,還有一個notifyAll()方法,表示通知待召隊列里面的所有線程。
這些細節問題,并不對大局產生影響。
綠色線程
綠色線程(Green Thread)是一個相對于操作系統線程(Native Thread)的概念。
操作系統線程(Native Thread)的意思就是,程序里面的線程會真正映射到操作系統的線程,線程的運行和調度都是由操作系統控制的
綠色線程(Green Thread)的意思是,程序里面的線程不會真正映射到操作系統的線程,而是由語言運行平臺自身來調度。
當前版本的Python語言的線程就可以映射到操作系統線程。當前版本的Ruby語言的線程就屬于綠色線程,無法映射到操作系統的線程,因此Ruby語言的線程的運行速度比較慢。
難道說,綠色線程要比操作系統線程要慢嗎?當然不是這樣。事實上,情況可能正好相反。Ruby是一個特殊的例子。線程調度器并不是很成熟。
目前,線程的流行實現模型就是綠色線程。比如,stackless Python,就引入了更加輕量的綠色線程概念。在線程并發編程方面,無論是運行速度還是并發負載上,都優于Python。
另一個更著名的例子就是ErLang(愛立信公司開發的一種開源語言)。
ErLang的綠色線程概念非常徹底。ErLang的線程不叫Thread,而是叫做Process。這很容易和進程混淆起來。這里要注意區分一下。
ErLang Process之間根本就不需要同步。因為ErLang語言的所有變量都是final的,不允許變量的值發生任何變化。因此根本就不需要同步。
final變量的另一個好處就是,對象之間不可能出現交叉引用,不可能構成一種環狀的關聯,對象之間的關聯都是單向的,樹狀的。因此,內存垃圾回收的算法效率也非常高。這就讓ErLang能夠達到Soft Real Time(軟實時)的效果。這對于一門支持內存垃圾回收的語言來說,可不是一件容易的事情。
Java中的多線程使用 synchronized關鍵字實現同步.為了避免線程中使用共享資源的沖突,當線程進入 synchronized的共享對象時,將為共享對象加上鎖,阻止其他的線程進入該共享對象.但是,正因為這樣,當多線程訪問多個共享對象時,如果線程鎖定對象的順序處理不當話就有可能線程間相互等待的情況,即常說的: 死鎖現象.
引發死鎖的條件:
必須滿足以下四種條件
1,互斥條件,每個資源要么已經分配給一個進程,要么就是可用的。
2,占有等待條件,已經得到了某個資源的進程可以再請求新的資源
3,不可搶占條件,已經分配給一個進程的資源不能強制的被搶占,只能被占有他的進程顯示的釋放
4,環路等待條件,死鎖發生時,系統中一定有兩個或者兩個以上的進程組成一環路,該環路中的每一個
進程都在等待下一個進程占有的資源。
處理死鎖的策略:
1,忽略該問題,你忽略它,它也會忽略你
2,測試死鎖并恢復,讓死鎖發生,檢測,一旦檢測到,恢復
3,仔細對資源進行分配,動態避免死鎖
4,通過破壞四個死鎖條件之一
方法一對應的時鴕鳥算法,就是出現這種死鎖的可能性很低,比如操作系統的fork,可能5年出現一次,
而在這段過程中,因為硬件等其它原因肯定要重新啟動機器,放棄fork損失太大,就可以忽略這種死鎖
,象鴕鳥一樣,把頭埋進沙子,當什么都沒發生。
方法二:檢測并恢復
恢復方法有:
搶占恢復
回退恢復
殺死進程恢復
銀行家算法:
如果有4個人(A,B,C,D)去銀行貸款,銀行有金額10個單位,
A貸款最大為6 ,A已經貸款1
B貸款最大為5 ,B已經貸款1
C貸款最大為4 ,C已經貸款2
D貸款最大為7 ,D已經貸款4
這個時候只有C的請求能通過,因為現在還有可用貸款2,只有C才能完成,然后釋放更多,來讓其它完成
這個時候如果給其它任何一個單位的貸款,那么所有的人都不能達到需求,完成。
銀行家問題時個經典的問題,但是很少能得到實際的利用,因為每個客戶自己都不知道自己需要多少資
源,同時,也不知道有多少個客戶。因為不停的有用戶login ,logout
方法四:破壞條件
1,破壞互斥條件,不讓獨占出現,
例如不讓一個用戶獨占打印機,如spooling技術,讓多個用戶同時進入spooling
問題:可能在spooling中產生死鎖
2,破壞占有等待條件
檢測這個進程需要的所有資源是不是可用,如果可用分配,不可用的話就等待
問題:進程要在開始知道自己需要多少資源,這樣可以使用銀行家算法完成。
但是資源利用不是最優。
3,破壞不可搶占,這個實現起來最困難
4,破壞閉環
把所有資源編號,按照順序請求
饑餓:
與死鎖很接近的時饑餓
如果一個打印機的使用,是通過某種算法避免死鎖,但是每次都是最小文件先打印,這樣就可能產生一
種情況,大的文件永遠不能打印,饑餓而死。
一般來說,每一種使用線程的語言中都存在線程死鎖問題,Java開發中遇到線程死鎖問題也是非常普遍。筆者在程序開發中就常常碰到死鎖的問題,并經常束手無策。本文分享筆者在JAVA開發中對線程死鎖的一些看法。
一. 什么是線程
在談到線程死鎖的時候,我們首先必須了解什么是Java線程。一個程序的進程會包含多個線程,一個線程就是運行在一個進程中的一個邏輯流。多線程允許在程序中并發執行多個指令流,每個指令流都稱為一個線程,彼此間互相獨立。
線程又稱為輕量級進程,它和進程一樣擁有獨立的執行控制,由操作系統負責調度,區別在于線程沒有獨立的存儲空間,而是和所屬進程中的其它線程共享一個存儲空間,這使得線程間的通信較進程簡單。筆者的經驗是編寫多線程序,必須注意每個線程是否干擾了其他線程的工作。每個進程開始生命周期時都是單一線程,稱為“主線程”,在某一時刻主線程會創建一個對等線程。如果主線程停滯則系統就會切換到其對等線程。和一個進程相關的線程此時會組成一個對等線程池,一個線程可以殺死其任意對等線程。
因為每個線程都能讀寫相同的共享數據。這樣就帶來了新的麻煩:由于數據共享會帶來同步問題,進而會導致死鎖的產生。
二. 死鎖的機制
由多線程帶來的性能改善是以可靠性為代價的,主要是因為有可能產生線程死鎖。死鎖是這樣一種情形:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不能正常運行。簡單的說就是:線程死鎖時,第一個線程等待第二個線程釋放資源,而同時第二個線程又在等待第一個線程釋放資源。這里舉一個通俗的例子:如在人行道上兩個人迎面相遇,為了給對方讓道,兩人同時向一側邁出一步,雙方無法通過,又同時向另一側邁出一步,這樣還是無法通過。假設這種情況一直持續下去,這樣就會發生死鎖現象。
導致死鎖的根源在于不適當地運用“synchronized”關鍵詞來管理線程對特定對象的訪問。“synchronized”關鍵詞的作用是,確保在某個時刻只有一個線程被允許執行特定的代碼塊,因此,被允許執行的線程首先必須擁有對變量或對象的排他性訪問權。當線程訪問對象時,線程會給對象加鎖,而這個鎖導致其它也想訪問同一對象的線程被阻塞,直至第一個線程釋放它加在對象上的鎖。
Java中每個對象都有一把鎖與之對應。但Java不提供單獨的lock和unlock操作。下面筆者分析死鎖的兩個過程“上鎖”和“鎖死” 。
(1) 上鎖
許多線程在執行中必須考慮與其他線程之間共享數據或協調執行狀態,就需要同步機制。因此大多數應用程序要求線程互相通信來同步它們的動作,在 Java 程序中最簡單實現同步的方法就是上鎖。在 Java 編程中,所有的對象都有鎖。線程可以使用 synchronized 關鍵字來獲得鎖。在任一時刻對于給定的類的實例,方法或同步的代碼塊只能被一個線程執行。這是因為代碼在執行之前要求獲得對象的鎖。
為了防止同時訪問共享資源,線程在使用資源的前后可以給該資源上鎖和開鎖。給共享變量上鎖就使得 Java 線程能夠快速方便地通信和同步。某個線程若給一個對象上了鎖,就可以知道沒有其他線程能夠訪問該對象。即使在搶占式模型中,其他線程也不能夠訪問此對象,直到上鎖的線程被喚醒、完成工作并開鎖。那些試圖訪問一個上鎖對象的線程通常會進入睡眠狀態,直到上鎖的線程開鎖。一旦鎖被打開,這些睡眠進程就會被喚醒并移到準備就緒隊列中。
(2)鎖死
如果程序中有幾個競爭資源的并發線程,那么保證均衡是很重要的。系統均衡是指每個線程在執行過程中都能充分訪問有限的資源,系統中沒有餓死和死鎖的線程。當多個并發的線程分別試圖同時占有兩個鎖時,會出現加鎖沖突的情形。如果一個線程占有了另一個線程必需的鎖,互相等待時被阻塞就有可能出現死鎖。
在編寫多線程代碼時,筆者認為死鎖是最難處理的問題之一。因為死鎖可能在最意想不到的地方發生,所以查找和修正它既費時又費力。例如,常見的例子如下面這段程序。
public int sumArrays(int[] a1, int[] a2)
{
int value = 0;
int size = a1.length;
if (size == a2.length)
{
synchronized(a1)
{ //1 synchronized(a2)
{ //2 for (int i=0; i<size; i++)
value += a1[i] + a2[i];
} } } return value;
}
這段代碼在求和操作中訪問兩個數組對象之前鎖定了這兩個數組對象。它形式簡短,編寫也適合所要執行的任務;但不幸的是,它有一個潛在的問題。這個問題就是它埋下了死鎖的種子。
三. 如何檢測死鎖的根源
Java并不提供對死鎖的檢測機制。筆者認為常用分析Java代碼問題的最有效的工具仍然是java thread dump。當死鎖發生時,JVM通常處于掛起狀態,thread dump可以給出靜態穩定的信息,查找死鎖只需要查找有問題的線程。Java虛擬機死鎖發生時,從操作系統上觀察,虛擬機的CPU占用率為零,很快會從top或prstat的輸出中消失。這時可以收集thread dump,查找"waiting for monitor entry"的thread,如果大量thread都在等待給同一個地址上鎖(因為對于Java,一個對象只有一把鎖),這說明很可能死鎖發生了。
為了確定問題,筆者建議在隔幾分鐘后再次收集一次thread dump,如果得到的輸出相同,仍然是大量thread都在等待給同一個地址上鎖,那么肯定是死鎖了。如何找到當前持有鎖的線程是解決問題的關鍵。一般方法是搜索thread dump,查找"locked,找到持有鎖的線程。如果持有鎖的線程還在等待給另一個對象上鎖,那么還是按上面的辦法順藤摸瓜,直到找到死鎖的根源為止。
另外,在thread dump里還會經常看到這樣的線程,它們是等待一個條件而主動放棄鎖的線程。有時也需要分析這類線程,尤其是線程等待的條件。
四. 幾種常見死鎖及對策
解決死鎖沒有簡單的方法,這是因為線程產生死鎖都各有各的原因,而且往往具有很高的負載。大多數軟件測試產生不了足夠多的負載,所以不可能暴露所有的線程錯誤。在這里中,筆者將討論開發過程常見的4類典型的死鎖和解決對策。
(1)數據庫死鎖
在數據庫中,如果一個連接占用了另一個連接所需的數據庫鎖,則它可以阻塞另一個連接。如果兩個或兩個以上的連接相互阻塞,則它們都不能繼續執行,這種情況稱為數據庫死鎖。
數據庫死鎖問題不易處理,通常數據行進行更新時,需要鎖定該數據行,執行更新,然后在提交或回滾封閉事務時釋放鎖。由于數據庫平臺、配置的隔離級以及查詢提示的不同,獲取的鎖可能是細粒度或粗粒度的,它會阻塞(或不阻塞)其他對同一數據行、表或數據庫的查詢?;跀祿炷J剑x寫操作會要求遍歷或更新多個索引、驗證約束、執行觸發器等。每個要求都會引入更多鎖。此外,其他應用程序還可能正在訪問同一數據庫模式中的某些對象,并獲取不同應用程序所具有的鎖。
所有這些因素綜合在一起,數據庫死鎖幾乎不可能被消除了。值得慶幸的是,數據庫死鎖通常是可恢復的:當數據庫發現死鎖時,它會強制銷毀一個連接(通常是使用最少的連接),并回滾其事務。這將釋放所有與已經結束的事務相關聯的鎖,至少允許其他連接中有一個可以獲取它們正在被阻塞的鎖。
由于數據庫具有這種典型的死鎖處理行為,所以當出現數據庫死鎖問題時,數據庫常常只能重試整個事務。當數據庫連接被銷毀時,會拋出可被應用程序捕獲的異常,并標識為數據庫死鎖。如果允許死鎖異常傳播到初始化該事務的代碼層之外,則該代碼層可以啟動一個新事務并重做先前所有工作。
當出現問題就重試,由于數據庫可以自由地獲取鎖,所以幾乎不可能保證兩個或兩個以上的線程不發生數據庫死鎖。此方法至少能保證在出現某些數據庫死鎖情況時,應用程序能正常運行。
(2)資源池耗盡死鎖
客戶端的增加導致資源池耗盡死鎖是由于負載而造成的,即資源池太小,而每個線程需要的資源超過了池中的可用資源。假設連接池最多有10個連接,同時有10個對外部并發調用。這些線程中每一個都需要一個數據庫連接用來清空池。現在,每個線程都執行嵌套的調用。則所有線程都不能繼續,但又都不放棄自己的第一個數據庫連接。這樣,10個線程都將被死鎖。
研究此類死鎖,會發現線程存儲中有大量等待獲取資源的線程,以及同等數量的空閑且未阻塞的活動數據庫連接。當應用程序死鎖時,如果可以在運行時檢測連接池,就能確認連接池實際上已空。
修復此類死鎖的方法包括:增加連接池的大小或者重構代碼,以便單個線程不需要同時使用很多數據庫連接?;蛘呖梢栽O置內部調用使用不同的連接池,即使外部調用的連接池為空,內部調用也能使用自己的連接池繼續。
(3)單線程、多沖突數據庫連接死鎖
對同一線程執行嵌套的調用有時出現死鎖,此情形即使在非高負載系統中通常也會發生。當第一個(外部)連接已獲取第二個(內部)連接所需要的數據庫鎖,則第二個連接將永久阻塞第一個連接,并等待第一個連接被提交或回滾,這就出現了死鎖情形。因為數據庫沒有注意到兩個連接之間的關系,所以數據庫不會將此情形檢測為死鎖。這樣即使不存在并發,此代碼也將導致死鎖。此情形有多種具體的變種,可以涉及多個線程和兩個以上的數據庫連接。
(4)Java虛擬機鎖與數據庫鎖沖突
這種情形發生在數據庫鎖與Java虛擬機鎖并存的時候。在這種情況下,一個線程占有一個數據庫鎖并嘗試獲取Java虛擬機鎖。同時,另一個線程占有Java虛擬機鎖并嘗試獲取數據庫鎖。此時,數據庫發現一個連接阻塞了另一個連接,但由于無法阻止連接繼續,所以不會檢測到死鎖。Java虛擬機發現同步的鎖中有一個線程,并有另一個嘗試進入的線程,所以即使Java虛擬機能檢測到死鎖并對它們進行處理,它還是不會檢測到這種情況。
總而言之,JAVA應用程序中的死鎖是一個大問題——它能導致整個應用程序慢慢終止,還很難被分離和修復,尤其是當開發人員不熟悉如何分析死鎖環境的時候。
五. 死鎖的經驗法則
筆者在開發中總結以下死鎖問題的經驗。
(1) 對大多數的Java程序員來說最簡單的防止死鎖的方法是對競爭的資源引入序號,如果一個線程需要幾個資源,那么它必須先得到小序號的資源,再申請大序號的資源。可以在Java代碼中增加同步關鍵字的使用,這樣可以減少死鎖,但這樣做也會影響性能。如果負載過重,數據庫內部也有可能發生死鎖。
(2)了解數據庫鎖的發生行為。假定任何數據庫訪問都有可能陷入數據庫死鎖狀況,但是都能正確進行重試。例如了解如何從應用服務器獲取完整的線程轉儲以及從數據庫獲取數據庫連接列表(包括互相阻塞的連接),知道每個數據庫連接與哪個Java線程相關聯。了解Java線程和數據庫連接之間映射的最簡單方法是向連接池訪問模式添加日志記錄功能。
(3)當進行嵌套的調用時,了解哪些調用使用了與其它調用同樣的數據庫連接。即使嵌套調用運行在同一個全局事務中,它仍將使用不同的數據庫連接,而不會導致嵌套死鎖。
(4)確保在峰值并發時有足夠大的資源池。
(5)避免執行數據庫調用或在占有Java虛擬機鎖時,執行其他與Java虛擬機無關的操作。
最重要的是,多線程設計雖然是困難的,但在開始編程之前詳細設計系統能夠幫助你避免難以發現死鎖的問題。死鎖在語言層面上不能解決,就需要一個良好設計來避免死鎖。
摘要: 在java中有一類線程,專門在后臺提供服務,此類線程無需顯式關閉,當程序結束了,它也就結束了,這就是守護線程 daemon thread。如果還有非守護線程的線程在執行,它就不會結束。 守護線程有何用處呢?讓我們來看個實踐中的例子。 在我們的系統中經常應用各種配置文件...
閱讀全文
同時開始5個線程,用各自的文本框顯示count,和按鈕控制count的自加
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class Ticker extends Thread{
private Button t=new Button("toggle");
private TextField tf=new TextField(10);
//開關控制count的變化
private runFlag=true;
private int count=0;
class Stop implements ActionListener{
@Override
public void actionPerformed(ActionEvent e){
runFlag=!runFlag;
}
}
public Ticker(Container c){
t.addActionListener(new Stop());
//Panel容器
Panel p=new Panel();
p.add(t);
p.add(tf);
c.add(p);
}
@Override
public void run(){
while(true){
try(
Thread.currentThread().sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
if(runFlag)
tf.setText(Integer.toString(++count));
}
}
}
public class Counter extends Applet{
private Button start=new Button("Start");
private boolean started=false;
private int size=0;
private Ticker[] ts;
@Override
public void init(){
start.addActionListener(new Start());
add(start);
ts=new Ticker[size];
for(int i=0;i<size;i++){
ts[i]=new Ticker(Counter.this);
}
}
class Start implements ActionListener{
@Override
public void actionPerformed(ActionEvent e){
if(!started){
started=true;
for(int i=0;i<size;i++){
ts[i].start();
}
}
}
}
public static void main(String[] args){
Counter c=new Counter();
Frame frame=new Frame("程序片");
frame.addWindowListener(
new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e){
System.exit(0);
}
}
);
frame.setSize(300,c.size*50);
frame.add(c,BorderLayout.CENTER);
c.init();
c.start();
frame.setVisible(true);
}
}
/**--注意--**/
以上代碼都是在文本編輯器中寫的,可能會有些許紕漏
摘要: (1)方法Join是干啥用的? 簡單回答,同步,如何同步? 怎么實現的? 下面將逐個回答。 自從接觸Java多線程,一直對Join理解不了。JDK是這樣說的: join public final void join(long millis)throws Interrupte...
閱讀全文
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class TestCut {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
TestCut test = new TestCut();
List<Student> list = test.readFile("D:\\Students.txt", "GBK");
test.printAver(list);
System.out.println("-------------------------------------------");
test.printExcel(list);
System.out.println("---------------------------------------------");
test.printEnglishAvg(list);
}
/**
* 按照平均分降序格式化輸出
*
* @param list
*/
public void printAver(List<Student> list) {
if (list != null) {
String top = "序號\t學號\t平均分\t數學\t語文\t英語";
System.out.println(top);
Collections.sort(list, new Comparator<Student>() {
// 根據平均數來進行比較,降序排序
public int compare(Student arg0, Student arg1) {
if (arg1 == null)
return -1;
if (arg0.getAvg() < arg1.getAvg())
return 1;
else if (arg0.getAvg() == arg1.getAvg())
return 0;
else
return -1;
}
});
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
System.out.print((i + 1) + "\t" + student.getS_no() + "\t"
+ student.getAvg() + "\t");
System.out.print(student.getMaths() + "\t"
+ student.getChinese() + "\t");
if (student.getEnglish() != null)
System.out.println(student.getEnglish());
else {
System.out.println();
}
}
} else {
System.out.println("文件內容為空!");
}
}
/**
* 按照優秀率格式化輸出
*
* @param list
*/
public void printExcel(List<Student> list) {
if (list != null) {
String top = "序號\t學號\t優秀率\t數學\t語文\t英語";
System.out.println(top);
Collections.sort(list, new Comparator<Student>() {
// 根據優秀率來進行比較,降序排序
public int compare(Student arg0, Student arg1) {
if (arg1 == null)
return -1;
if (arg0.getExcel() < arg1.getExcel())
return 1;
else if (arg0.getExcel() == arg1.getExcel())
return 0;
else
return -1;
}
});
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
DecimalFormat df = new DecimalFormat("#%");
System.out.print((i + 1) + "\t" + student.getS_no() + "\t"
+ df.format(student.getExcel()) + "\t");
System.out.print(student.getMaths() + "\t"
+ student.getChinese() + "\t");
if (student.getEnglish() != null)
System.out.println(student.getEnglish());
else {
System.out.println();
}
}
} else {
System.out.println("文件內容為空!");
}
}
/**
* 求英語平均成績
* @param list
*/
public void printEnglishAvg(List<Student> list) {
printAvgByCourse(list, 3);
}
/**
* 求課程平均成績,并輸出
*
* @param list
* @param course
* 課程(1:數學,2:語文,3:英語)
*/
private void printAvgByCourse(List<Student> list, int course) {
Integer avg = 0;
switch (course) {
case 1: {
Integer maths = 0;
for (Student student : list) {
maths += student.getMaths();
}
avg = maths / list.size();
System.out.println("數學平均成績:\t" + avg);
break;
}
case 2: {
Integer chinese = 0;
for (Student student : list) {
chinese += student.getChinese();
}
avg = chinese / list.size();
System.out.println("語文平均成績:\t" + avg);
break;
}
case 3: {
Integer english = 0;
Integer size = 0;
for (Student student : list) {
if (student.getEnglish() != null) {
english += student.getEnglish();
size++;
}
}
if (size != 0)
avg = english / size;
System.out.println("英語平均成績:\t" + avg);
break;
}
default: {
System.out.println("不存在此課程");
break;
}
}
}
/**
* 讀取文件信息
*
* @param fileName
* 文件路徑
* @param charset
* 編碼
* @return
* @throws IOException
*/
public List<Student> readFile(String fileName, String charset) {
List<Student> list = new ArrayList<Student>();
FileInputStream fi = null;
BufferedReader in = null;
try {
fi = new FileInputStream(new File(fileName));
in = new BufferedReader(new InputStreamReader(fi, charset));
String result = in.readLine();
while ((result = in.readLine()) != null) {
String[] str = result.split("\t");
Student student = new Student();
for (int i = 0; i < str.length; i++) {
student.setS_no(str[0]);
student.setMaths(Integer.parseInt(str[1]));
student.setChinese(Integer.parseInt(str[2]));
if (str.length > 3) {
student.setEnglish(Integer.parseInt(str[3]));
}
student.setAvg(student.culAvg());
student.setExcel(student.culExcel());
}
list.add(student);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
}
class Student implements Serializable {
private static final long serialVersionUID = -6517638655032546653L;
/**
* 學號
*/
private String s_no;
/**
* 數學
*/
private Integer maths;
/**
* 語文
*/
private Integer chinese;
/**
* 英語
*/
private Integer english;
/**
* 平均分
*/
private Integer avg;
/**
* 優秀率
*/
private float excel;
public Student() {
}
public Student(Integer maths, Integer chinese, Integer english) {
this.maths = maths;
this.chinese = chinese;
this.english = english;
this.avg = culAvg(maths, chinese, english);
this.excel = culExcel(maths, chinese, english);
}
public String getS_no() {
return s_no;
}
public void setS_no(String sNo) {
s_no = sNo;
}
public Integer getMaths() {
return maths;
}
public void setMaths(Integer maths) {
this.maths = maths;
}
public Integer getChinese() {
return chinese;
}
public void setChinese(Integer chinese) {
this.chinese = chinese;
}
public Integer getEnglish() {
return english;
}
public void setEnglish(Integer english) {
this.english = english;
}
public Integer getAvg() {
return avg;
}
public void setAvg(Integer avg) {
this.avg = avg;
}
public float getExcel() {
return excel;
}
public void setExcel(float excel) {
this.excel = excel;
}
public String toString() {
StringBuffer sb = new StringBuffer("[");
sb.append("s_no=" + s_no).append(",maths=").append(maths).append(
",chinese=").append(chinese).append(",english=")
.append(english).append(",avg=").append(avg).append(",excel=")
.append(excel).append("]");
return sb.toString();
}
/**
* 計算平均數
*
* @param maths
* 數學
* @param chinese
* 語文
* @param english
* 英語
* @return
*/
private Integer culAvg(Integer maths, Integer chinese, Integer english) {
// 計算平均分
Integer sum = chinese + maths;
float aver = english == null ? (float) sum / 2
: (float) (sum + english) / 3;
return Math.round(aver);
}
/**
* 計算優秀率
*
* @param maths
* 數學
* @param chinese
* 語文
* @param english
* 英語
* @return
*/
private float culExcel(Integer maths, Integer chinese, Integer english) {
final Integer EXCEL_NUMBER = 85;
Integer total_number = english == null ? 2 : 3;
Integer ex = 0;
if (maths >= EXCEL_NUMBER)
ex++;
if (chinese >= EXCEL_NUMBER)
ex++;
if (english != null && english >= EXCEL_NUMBER)
ex++;
return (float) ex / total_number;
}
/**
* 計算平均數
*
* @return
*/
public Integer culAvg() {
return culAvg(this.maths, this.chinese, this.english);
}
/**
* 計算優秀率
*
* @return
*/
public float culExcel() {
return culExcel(this.maths, this.chinese, this.english);
}
}
package number;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
public class WriteFile {
public static void main(String[] args) {
File file=new File("temp.txt");//txt文檔是E:\workspace\study(你自己的工作區目錄)下的temp.txt文檔
try{
//先寫入內容到指定的文檔
System.out.println("請輸入文件的內容:");
InputStreamReader isr=new InputStreamReader(System.in );
BufferedReader br=new BufferedReader(isr);
String str=br.readLine();
FileWriter fw=new FileWriter(file);
PrintWriter pw=new PrintWriter(fw);
while(!str.equals("")){
pw.println(str);
str=br.readLine();
}
br.close();
pw.close();
System.out.println("寫入內容成功!");
//讀取文檔里面的內容
FileReader fr=new FileReader(file);
BufferedReader br2=new BufferedReader(fr);
String s=br2.readLine();
System.out.println("文檔內容為:");
while(s!=null){
System.out.println(s);
s=br2.readLine();
}
br2.close();
}catch(IOException e){
e.printStackTrace();
}
}
}