国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
馴服Java線程
(一)
JAVA 線程架構(gòu)
Java 多線程編程其實(shí)并不象大多數(shù)的書描述的那樣簡單,所有關(guān)于UI(用戶界面)的Java編程都要涉及多線程。這一章將會通過討論幾種操作系統(tǒng)的線程架構(gòu)和這些架構(gòu)將會怎樣影響Java多線程編程。按照這樣思路,我將介紹一些在Java的入門級書籍中描述的不慎清楚的關(guān)鍵術(shù)語和概念。理解這些概念是使你能夠看懂本書所提供的例子的必備條件。
多線程編程的問題
象鴕鳥一樣的把自己的頭埋在沙子里,假裝不去考慮多線程的問題其實(shí)是目前很多人進(jìn)行Java編程共同弊病。但是在真正的產(chǎn)品中,你卻無法回避這個(gè)嚴(yán)重的問題。目前,市面上大多數(shù)的書對Java線程的描述都是很膚淺的,甚至它們提供的例子本身就無法在多線程的環(huán)境下正確運(yùn)行。
事實(shí)上,多線程是影響所有代碼的重要因素??梢詷O端一點(diǎn)的說,單線程的代碼在現(xiàn)實(shí)應(yīng)用中,一錢不值,甚至根本無法運(yùn)行,更不用說正確性和高效率了。所以你應(yīng)該從一開始就把多線程作為一個(gè)重要的方面,融入你的代碼架構(gòu)。
所有不平凡的Java程序都是多線程的
不管你喜歡與否,所有的Java程序除了小部分非常簡單的控制臺程序都是基于多線程的。原因在于Java的Abstract Windowing Toolkit ( AWT )和它的擴(kuò)展Swing,AWT用一個(gè)特殊的線程處理所有的操作系統(tǒng)級的事件,這個(gè)特殊的線程是在第一個(gè)窗口出現(xiàn)的時(shí)候產(chǎn)生的。因此,幾乎所有的AWT程序都有至少2個(gè)線程在運(yùn)行:一個(gè)是main函數(shù)所在的線程和處理來自O(shè)S的事件和調(diào)用注冊的監(jiān)聽者的響應(yīng)方法(也就是回調(diào)函數(shù))的AWT線程。必須注意的是所有注冊的監(jiān)聽者方法,運(yùn)行在AWT線程上,而不是人們一般認(rèn)為的main函數(shù)(這也是監(jiān)聽器注冊的線程)。
這種架構(gòu)有兩個(gè)問題。第一,雖然監(jiān)聽器的方法是運(yùn)行在AWT線程上的,但是他們其實(shí)都是在main線程上聲明的內(nèi)部類(inner-class)。第二,雖然監(jiān)聽器的方法是運(yùn)行在AWT線程上的,但是它一般會非常頻繁的訪問它的外部類,也就是運(yùn)行在main線程上的類的成員變量。當(dāng)這兩個(gè)線程競爭(compete)訪問同一個(gè)對象實(shí)例(Object)時(shí),會引起非常嚴(yán)重的線程同步問題。適當(dāng)?shù)氖褂藐P(guān)鍵字synchronized是保證兩個(gè)線程安全訪問共享對象的必要手段。
更糟的是,AWT線程不但止處理監(jiān)聽器方法,還有響應(yīng)來自操作系統(tǒng)的事件。這就意味著,如果你的監(jiān)聽器方法占用大量的CPU時(shí)間來進(jìn)行處理,則你的程序?qū)o法響應(yīng)操作系統(tǒng)級的事件(例如鼠標(biāo)點(diǎn)擊事件和鍵盤事件)。這些事件將會被阻塞在事件隊(duì)列中,直到監(jiān)聽器方法返回。具體的表現(xiàn)就是UI的死鎖。這樣會讓用戶無法接受的。Listing 1.1就是這樣一個(gè)無響應(yīng)UI的例子。程序產(chǎn)生一個(gè)包含兩個(gè)按鈕的Frame。Sleep按鈕使它所在的線程(也就是前面所說的AWT事件處理線程)休眠5秒鐘。Hello按鈕只是簡單的在控制臺上打印“Hello World”。在你按下Sleep按鈕5秒鐘之內(nèi),無論你按多少次Hello按鈕,程序都不會有任何響應(yīng)。如果你在這期間按下了Hello按鈕5次。那么“Hello World”將會立即被連續(xù)打印五次,當(dāng)你Sleep按鈕的監(jiān)聽器方法結(jié)束以后。這就證明了5個(gè)鼠標(biāo)點(diǎn)擊事件被阻塞在事件隊(duì)列里,直到Sleep按鈕的事件響應(yīng)完。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class Hang extends JFrame
{
public Hang()
{   JButton b1 = new JButton( "Sleep" );
JButton b2 = new JButton( "Hello" );
b1.addActionListener
(   new ActionListener()
{   public void actionPerformed( ActionEvent event )
{   try
{   Thread.currentThread().sleep(5000);
}
catch(Exception e){}
}
}
);
b2.addActionListener
(   new ActionListener()
{   public void actionPerformed( ActionEvent event )
{   System.out.println("Hello world");
}
}
);
getContentPane().setLayout( new FlowLayout() );
getContentPane().add( b1 );
getContentPane().add( b2 );
pack();
show();
}
public static void main( String[] args )
{
new Hang();
}
}
大多數(shù)的書籍中討論的Java GUI都回避了線程的問題。在現(xiàn)實(shí)中,對于UI事件采取單線程的方法都是不可取的。所有成功的UI程序都有下面幾個(gè)共同點(diǎn):
l         UI必須就程序的運(yùn)行狀態(tài)進(jìn)程,給用戶一些回饋信息。簡單的彈出一個(gè)顯示程序正在做的事情的對話框是不足夠的。你必須告訴用戶操作的運(yùn)行進(jìn)度(例如一個(gè)帶有百分比的進(jìn)度條)。
l         必須做到當(dāng)?shù)讓酉到y(tǒng)狀態(tài)改變時(shí),不會為了更新窗口而把整個(gè)UI重繪。
l         你必須使你的程序做到,當(dāng)用戶點(diǎn)擊Cancel按鈕時(shí),你的程序能夠立即響應(yīng),并及時(shí)終止。
l         必須做到當(dāng)一個(gè)需要長時(shí)間運(yùn)行的操作正在運(yùn)行時(shí),用戶可以在你的UI界面上做其它的操作。
這是條規(guī)則可以用一句話來總結(jié):不允許死鎖的UI界面出現(xiàn),不允許當(dāng)程序運(yùn)行一個(gè)
耗時(shí)很長的操作時(shí),忽略掉用戶其他的操作,如鼠標(biāo)點(diǎn)擊和鍵盤事件,因此不允許在監(jiān)聽器窗口中,運(yùn)行長時(shí)間的操作。耗時(shí)操作必須在后臺的其他線程運(yùn)行。因此,真正的程序在任何時(shí)候都會2個(gè)以上的線程在跑。
(二)
Java 線程的支持不是平臺獨(dú)立的
非常不幸,作為Java語言所保證的平臺獨(dú)立性最重要的組成部分-------Java線程,并非是平臺獨(dú)立的。這增加了實(shí)現(xiàn)不依賴于平臺的線程系統(tǒng)的難度。在實(shí)現(xiàn)的時(shí)候,不得不考慮每個(gè)平臺的細(xì)微區(qū)別,以確保你的程序在每個(gè)平臺都保持一致。其實(shí),寫一個(gè)獨(dú)立于平臺的程序,還是有可能的,但必須非常小心。不過你可以放心,這個(gè)令人失望的事實(shí),并不是Java的問題。(“Ace”Framework 就是一個(gè)非常好的,也非常復(fù)雜的平臺獨(dú)立線程系統(tǒng)http://www.cs.wustl.edu/~schmidt/ACE.html)。所以,在我繼續(xù)講下去之前,我不得不線討論一下,由于平臺的多樣性,而導(dǎo)致的JVM的不一致性。
線程和進(jìn)程(Threads and Processes)
第一個(gè)關(guān)鍵的系統(tǒng)級概念,究竟什么是線程或者說究竟什么是進(jìn)程?她們其實(shí)就是操作系統(tǒng)內(nèi)部的一種數(shù)據(jù)結(jié)構(gòu)。
進(jìn)程數(shù)據(jù)結(jié)構(gòu)掌握著所有與內(nèi)存相關(guān)的東西:全局地址空間、文件句柄等等諸如此類的東西。當(dāng)一個(gè)進(jìn)程放棄執(zhí)行(準(zhǔn)確的說是放棄占有CPU),而被操作系統(tǒng)交換到硬盤上,使別的進(jìn)程有機(jī)會運(yùn)行的時(shí)候,在那個(gè)進(jìn)程里的所有數(shù)據(jù)也將被寫到硬盤上,甚至包括整個(gè)系統(tǒng)的核心(core memory)。可以這么說,當(dāng)你想到進(jìn)程(process),就應(yīng)該想到內(nèi)存(memory) (進(jìn)程 == 內(nèi)存)。如上所述,切換進(jìn)程的代價(jià)非常大,總有那么一大堆的內(nèi)存要移來移去。你必須用秒這個(gè)單位來計(jì)量進(jìn)程切換(上下文切換),對于用戶來說秒意味著明顯的等待和硬盤燈的狂閃(對于作者的我,就意味著IBM龍騰3代的爛掉,5555555)。言歸正傳,對于Java而言,JVM就幾乎相當(dāng)于一個(gè)進(jìn)程(process),因?yàn)橹挥羞M(jìn)程才能擁有堆內(nèi)存(heap,也就是我們平時(shí)用new操作符,分出來的內(nèi)存空間)。
那么線程是什么呢?你可以把它看成“一段代碼的執(zhí)行”---- 也就是一系列由JVM執(zhí)行的二進(jìn)制指令。這里面沒有對象(Object)甚至沒有方法(Method)的概念。指令執(zhí)行的序列可以重疊,并且并行的執(zhí)行。后面,我會更加詳細(xì)的論述這個(gè)問題。但是請記住,線程是有序的指令,而不是方法(method)。
線程的數(shù)據(jù)結(jié)構(gòu),與進(jìn)程相反,僅僅只包括執(zhí)行這些指令的信息。它包含當(dāng)前的運(yùn)行上下文(context):如寄存器(register)的內(nèi)容、當(dāng)前指令的在運(yùn)行引擎的指令流中的位置、保存方法(methods)本地參數(shù)和變量的運(yùn)行時(shí)堆棧。如果發(fā)生線程切換,OS只需把寄存器的值壓進(jìn)棧,然后把線程包含的數(shù)據(jù)結(jié)構(gòu)放到某個(gè)類是列表(LIST)的地方;把另一個(gè)線程的數(shù)據(jù)從列表中取出,并且用棧里的值重新設(shè)置寄存器。切換線程更加有效率,時(shí)間單位是毫秒。對于Java而言,一個(gè)線程可以看作是JVM的一個(gè)狀態(tài)。
運(yùn)行時(shí)堆棧(也就是前面說的存儲本地變量和參數(shù)的地方)是線程數(shù)據(jù)結(jié)構(gòu)一部分。這是因?yàn)槎鄠€(gè)線程,每一個(gè)都有自己的運(yùn)行時(shí)堆棧,也就是說存儲在這里面的數(shù)據(jù)是絕對線程安全(后面將會詳細(xì)解釋這個(gè)概念)的。因?yàn)榭梢钥隙ㄒ粋€(gè)線程是無法修改另一個(gè)線程的系統(tǒng)級的數(shù)據(jù)結(jié)構(gòu)的。也可以這么說一個(gè)不訪問堆內(nèi)存的(只讀寫堆棧內(nèi)存)方法,是線程安全的(Thread Safe)。
線程安全和同步
線程安全,是指一個(gè)方法(method)可以在多線程的環(huán)境下安全的有效的訪問進(jìn)程級的數(shù)據(jù)(這些數(shù)據(jù)是與其他線程共享的)。事實(shí)上,線程安全是個(gè)很難達(dá)到的目標(biāo)。
線程安全的核心概念就是同步,它保證多個(gè)線程:
l         同時(shí)開始執(zhí)行,并行運(yùn)行
l         不同時(shí)訪問相同的對象實(shí)例
l         不同時(shí)執(zhí)行同一段代碼
我將會在后面的章節(jié),一一細(xì)訴這些問題。但現(xiàn)在還是讓我們來看看同步的一種經(jīng)典的
實(shí)現(xiàn)方法——信號量。信號量是任何可以讓兩個(gè)線程為了同步它們的操作而相互通信的對象。Java也是通過信號量來實(shí)現(xiàn)線程間通信的。
不要被微軟的文檔所暗示的信號量僅僅是Dijksta提出的計(jì)數(shù)型信號量所迷惑。信號量其實(shí)包含任何可以用來同步的對象。
如果沒有synchronized關(guān)鍵字,就無法用JAVA實(shí)現(xiàn)信號量,但是僅僅只依靠它也不足夠。我將會在后面為大家演示一種用Java實(shí)現(xiàn)的信號量。
同步的代價(jià)很高喲!
同步(或者說信號量,隨你喜歡啦)的一個(gè)很讓人頭痛的問題就是代價(jià)??紤]一下,下面的代碼:
Listing 1.2:
import java.util.*;
import java.text.NumberFormat;
class Synch
{
private static long[ ]              locking_time   = new long[100];
private static long[ ]              not_locking_time = new long[100];
private static final long       ITERATIONS = 10000000;
synchronized long locking     (long a, long b){return a + b;}
long              not_locking (long a, long b){return a + b;}
private void test( int id )
{
long start = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{     locking(i,i);
}
locking_time[id] = System.currentTimeMillis() - start;
start                      = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{     not_locking(i,i);
}
not_locking_time[id] = System.currentTimeMillis() - start;
}
static void print_results( int id )
{
NumberFormat compositor = NumberFormat.getInstance();
compositor.setMaximumFractionDigits( 2 );
double time_in_synchronization = locking_time[id] - not_locking_time[id];
System.out.println( "Pass " + id + ": Time lost: "
+ compositor.format( time_in_synchronization                         )
+ " ms. "
+ compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
+ "% increase"
);
}
static public void main(String[ ] args) throws InterruptedException
{
final Synch tester = new Synch();
tester.test(0); print_results(0);
tester.test(1); print_results(1);
tester.test(2); print_results(2);
tester.test(3); print_results(3);
tester.test(4); print_results(4);
tester.test(5); print_results(5);
tester.test(6); print_results(6);
final Object start_gate = new Object();
Thread t1 = new Thread()
{     public void run()
{     try{ synchronized(start_gate) {     start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(7);
}
};
Thread t2 = new Thread()
{     public void run()
{     try{ synchronized(start_gate) {     start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(8);
}
};
Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
t1.start();
t2.start();
synchronized(start_gate){ start_gate.notifyAll(); }
t1.join();
t2.join();
print_results( 7 );
print_results( 8 );
}
}
這是一個(gè)簡單的基準(zhǔn)測試程序,她清楚的向大家揭示了同步的代價(jià)是多么的大。test(…)方法調(diào)用2個(gè)方法1,000,000,0次。其中一個(gè)是同步的,另一個(gè)則否。下面是在我的機(jī)器上輸出的結(jié)果(CPU: P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and HotSpot 1.4.01-b01):
C:\>java -verbose:gc Synch
Pass 0: Time lost: 251 ms. 727.5% increase
Pass 1: Time lost: 250 ms. 725% increase
Pass 2: Time lost: 251 ms. 602% increase
Pass 3: Time lost: 250 ms. 725% increase
Pass 4: Time lost: 261 ms. 752.5% increase
Pass 5: Time lost: 260 ms. 750% increase
Pass 6: Time lost: 261 ms. 752.5% increase
Pass 7: Time lost: 1,953 ms. 1,248.82% increase
Pass 8: Time lost: 3,475 ms. 8,787.5% increase
這里為了使HotSpot JVM充分的發(fā)揮其威力,test( )方法被多次反復(fù)調(diào)用。一旦這段程序被徹底優(yōu)化以后,也就是大約在Pass 6時(shí),同步的代價(jià)達(dá)到最大。Pass 7 和Pass 8與前面的區(qū)別在于,我new了兩個(gè)新的線程來并行執(zhí)行test方法,兩個(gè)線程競爭執(zhí)行(后面是適當(dāng)?shù)牡胤?,我會解釋什么是“競爭”,如果你已?jīng)等不及了,買本大學(xué)的操作系統(tǒng)課本看看吧! J),這使結(jié)果更加接近真實(shí)。同步的代價(jià)是如此之高的,應(yīng)該盡量避免無謂的同步代價(jià)。
現(xiàn)在是時(shí)候我們更深入的討論一下同步的代價(jià)了。HotSpot JVM一般會使用一到兩個(gè)方法來實(shí)現(xiàn)同步,這主要取決于是否存在線程的競爭。當(dāng)沒有競爭的時(shí)候,計(jì)算機(jī)的匯編指令順序的執(zhí)行,這些指令的執(zhí)行是不被打斷。指令試圖測試一個(gè)比特(bit),然后設(shè)置各種二進(jìn)制位來表示測試的結(jié)果,如果這個(gè)bit沒有被設(shè)置,指令就設(shè)置它。這可以說是非常原始的信號量,因?yàn)楫?dāng)兩個(gè)線程同步的企圖設(shè)置一個(gè)bit的值時(shí),只有一個(gè)線程可以成功,兩個(gè)線程都會檢查結(jié)果,看看是不是自己設(shè)成功了。
如果bit已經(jīng)被設(shè)置(這里說的是有線程競爭的情況下),失敗的JVM(線程)不得不離開操作系統(tǒng)的核心進(jìn)程等待這個(gè)bit位被清零。這樣來回的在系統(tǒng)核心中切換是非常耗時(shí)的。在NT系統(tǒng)下,需要600次機(jī)械指令循環(huán)來進(jìn)入一次系統(tǒng)內(nèi)核,這還僅僅是進(jìn)入所耗費(fèi)的時(shí)間還不包括做操作的時(shí)間。
是不是覺得很無聊了,呵呵!今天似乎都是些不頂用的東西。但這是必須的,為了使你能夠讀懂后面的內(nèi)容。下一篇,我將會談到一些更有趣的話題,例如如何避免同步,如果大家不反對,我還想講一些設(shè)計(jì)模式的東西。下回見!
(三)
避免同步
大部分顯示的同步都可以避免。一般不操作對象狀態(tài)信息(例如數(shù)據(jù)成員)的方法都不需要同步,例如:一些方法只訪問本地變量(也就是說在方法內(nèi)部聲明的變量),而不操作類級別的數(shù)據(jù)成員,并且這些方法不會通過傳入的引用參數(shù)來修改外部的對象。符合這些條件的方法都不需要使用synchronization這種重量級的操作。除此之外,還可以使用一些設(shè)計(jì)模式(Design Pattern)來避免同步(我將會在后面提到)。
你甚至可以通過適當(dāng)?shù)慕M織你的代碼來避免同步。相對于同步的一個(gè)重要的概念就是原子性。一個(gè)原子性的操作事不能被其他線程中斷的,通常的原子性操作是不需要同步的。
Java定義一些原子性的操作。一般的給變量付值的操作是原子的,除了long和double??聪旅娴拇a:
class Unreliable
{
private long x;
public long get_x( ) {return x;}
public void set_x(long value) { x = value; }
}
線程1調(diào)用:
obj.set_x( 0 );
線程2調(diào)用:
obj.set_x( 0x123456789abcdef )
問題在于下面這行代碼:
x = value;
JVM為了效率的問題,并沒有把x當(dāng)作一個(gè)64位的長整型數(shù)來使用,而是把它分為兩個(gè)32-bit,分別付值:
x.high_word = value.high_word;
x.low_word = value.low_word;
因此,存在一個(gè)線程設(shè)置了高位之后被另一個(gè)線程切換出去,而改變了其高位或低位的值。所以,x的值最終可能為0x0123456789abcdef、0x01234567000000、0x00000000abcdef和0x00000000000000。你根本無法確定它的值,唯一的解決方法是,為set_x( )和get_x()方法加上synchronized這個(gè)關(guān)鍵字或者把這個(gè)付值操作封裝在一個(gè)確保原子性的代碼段里。
所以,在操作的long型數(shù)據(jù)的時(shí)候,千萬不要想當(dāng)然。強(qiáng)迫自己記住吧:只有直接付值操作是原子的(除了上面的例子)。其它,任何表達(dá)式,象x = ++y、x += y都是不安全的,不管x或y的數(shù)據(jù)類型是否是小于64位的。很可能在付值之前,自增之后,被其它線程搶先了(preempted)。
競爭條件
在術(shù)語中,對于前面我提到的多線程問題——兩個(gè)線程同步操作同一個(gè)對象,使這個(gè)對象的最終狀態(tài)不明——叫做競爭條件。競爭條件可以在任何應(yīng)該由程序員保證原子操作的,而又忘記使用synchronized的地方。在這個(gè)意義上,可以把synchronized看作一種保證復(fù)雜的、順序一定的操作具有原子性的工具,例如給一個(gè)boolean值變量付值,就是一個(gè)隱式的同步操作。
不變性
一種有效的語言級的避免同步的方法就是不變性(immutability)。一個(gè)自從產(chǎn)生那一刻起就無法再改變的對象就是不變性對象,例如一個(gè)String對象。但是要注意類似這樣的表達(dá)式:string1 += string2;本質(zhì)上等同于string1 = string1 + string2;其實(shí)第三個(gè)包含string1和string2的string對象被隱式的產(chǎn)生,最后,把string1的引用指向第三個(gè)string。這樣的操作,并不是原子的。
由于不變對象的值無法發(fā)生改變,所以可以為多個(gè)線程安全的同步操作,不需要synchronized。
把一個(gè)類的所有數(shù)據(jù)成員都聲明為final就可以創(chuàng)建一個(gè)不變類型了。那些被聲明為final的數(shù)據(jù)成員并不是必須在聲明的時(shí)候就寫死,但必須在類的構(gòu)造函數(shù)中,全部明確的初始化。例如:
Class I_am_immutable
{
private final int MAX_VALUE = 10;
private final int blank_final;
public I_am_immutable( int_initial_value )
{
blank_final = initial_value;
}
}
一個(gè)由構(gòu)造函數(shù)進(jìn)行初始化的final型變量叫做blank final。一般的,如果你頻繁的只讀訪問一個(gè)對象,把它聲明成一個(gè)不變對象是個(gè)保證同步的好辦法,而且可以提高JVM的效率,因?yàn)镠otSpot會把它放到堆棧里以供使用。
同步封裝器(Synchronization Wrappers)
同步還是不同步,是問題的所在。讓我們跳出這樣的思維模式吧,世事無絕對。有什么辦法可以使你的類靈活的在同步與不同步之間切換呢? 有一個(gè)非常好的現(xiàn)成例子,就是新近引入JAVA的Collection框架,它是用來取代原本散亂的、繁重的Vector等類型。Vector的任何方法都是同步的,這就是為什么說它繁重了。而對于collections對象,在需要保證同步的時(shí)候,一般會由訪問它方法來保證同步,因此沒有必要兩次鎖定(一次是鎖定包含使用collection對象的方法的對象,一次是鎖定collection對象自身)。Java的解決方案是使用同步封裝器。其基本原理來自四人幫(Gang-of-Four)的Decorator模式,一個(gè)Decorator自身就實(shí)現(xiàn)某個(gè)接口,而且又包含了實(shí)現(xiàn)同樣接口的數(shù)據(jù)成員,但是在通過外部類方法調(diào)用內(nèi)部成員的相同方法的時(shí)候,控制或者修改傳入的變量。java.io這個(gè)包里的所有類都是Decorator:一個(gè)BufferedInputStream既實(shí)現(xiàn)了虛類InputStream的所有方法,又包含了一個(gè)InputStream引用所指向的成員變量。程序員調(diào)用外部容器類的方法,實(shí)際上是變相的調(diào)用內(nèi)部對象的方法。
我們可以利用這種設(shè)計(jì)模式。來實(shí)現(xiàn)靈活的同步方法。如下例:
Interface Some_interface
{
Object message( );
}
class Not_thread_safe implements Some_interface
{
public Object message( )
{
//實(shí)現(xiàn)該方法的代碼,省~~~~~~~~~~~
return null;
}
}
class Thread_safe_wrapper implements Some_interface
{
Some_interface  not_thread_safe;
public Thread_safe_wrapper(Some_interface  not_thread_safe)
{
this.not_thread_safe  =  not_thread_safe;
}
public Some_interface extract( )
{
return not_thread_safe;
}
public synchronized Object message( )
{
return not_thread_safe.message( );
}
}
當(dāng)不存在線程安全的時(shí)候,你可以直接使用Not_thread_safe類對象。當(dāng)需要考慮線程安全的時(shí)候,只需要把它包裝一下:
Some_interface object = new Not_thread_safe( );
……………
object = new Thread_safe_wrapper(object); //object現(xiàn)在變成線程安全了
當(dāng)你不需要考慮線程安全的時(shí)候,你可以還原object對象:
object = ((Thread_safe_Wrapper)object).extract( );
下一回,我們又要深入底層機(jī)制了。呵呵!千萬不要悶著大家呀!下回見!
(四)
線程的并發(fā)性
下一個(gè)與OS平臺相關(guān)的問題(這也是編寫與平臺無關(guān)的Java程序要面對的問題)是必須確定并發(fā)性和并行在該平臺的定義。并發(fā)的多線程系統(tǒng)總會給人多個(gè)任務(wù)同時(shí)運(yùn)行的感覺,其實(shí)這些任務(wù)是被分割為許多的塊交錯(cuò)在一起執(zhí)行的。在一個(gè)并行的系統(tǒng)中,兩個(gè)任務(wù)實(shí)際上是同時(shí)(這里的同時(shí)是真正的同時(shí),而不是快速交錯(cuò)執(zhí)行所產(chǎn)生的并行假象)運(yùn)行的,這就要求有多個(gè)CPU。如圖1.1:
圖1.1 Concurrency vs Parallelism
多線程其實(shí)并不能加快的程序速度。如果你的程序并不需要頻繁的等待IO操作完成,那么多線程程序還會比單線程程序更慢些。但在多CPU系統(tǒng)下則反之。
Java線程系統(tǒng)非平臺獨(dú)立的主要原因就是要實(shí)現(xiàn)徹底的平行運(yùn)行的線程,如果不使用OS提供的系統(tǒng)線程模型,是不可能的。對于JAVA而言,在理論上,允許由JVM來模仿整個(gè)線程系統(tǒng),從而避免我在前一篇文章(馴服JAVA線程2)中,所提到的進(jìn)入OS核心的時(shí)間消耗。但是,這樣也排除了程序中的并行性,因?yàn)槿绻皇褂萌魏尾僮飨到y(tǒng)級的線程(這樣是為了保持平臺獨(dú)立性),OS會把JVM的實(shí)例當(dāng)成一個(gè)單線程的程序來看待,也就只會分配單個(gè)CPU來執(zhí)行它,從而導(dǎo)致就算運(yùn)行在多CPU的機(jī)器上,而且只有一個(gè)JVM實(shí)例在單獨(dú)運(yùn)行,也不可能出現(xiàn)兩個(gè)Java線程真正的并行運(yùn)行(充分的利用兩個(gè)CPU)。
所以,要真正的實(shí)現(xiàn)并行運(yùn)行,只有存在兩個(gè)JVM實(shí)例,分別運(yùn)行不同的程序。做的再好一點(diǎn)就是讓JVM把Java的線程映射到OS級的線程上去(一個(gè)Java線程就是一個(gè)系統(tǒng)的線程,讓系統(tǒng)進(jìn)行調(diào)配,充分發(fā)揮系統(tǒng)對資源的操控能力,這樣就不存在只能在一個(gè)CPU上運(yùn)行的問題了)。不幸的是,不同的操作系統(tǒng)實(shí)現(xiàn)的線程機(jī)制也不同,而且這些區(qū)別已經(jīng)到了在編程時(shí)不能忽視的地步了。
由于平臺不同而導(dǎo)致的問題
下面,我將會通過比較Solaris和WindowsNT對線程機(jī)制實(shí)現(xiàn)不同之處,來說明前面提到的問題。
Java,在理論上,至少有10個(gè)線程優(yōu)先等級劃分(如果有兩個(gè)或兩個(gè)以上的線程都處在on ready狀態(tài)下,那么擁有高優(yōu)先級的線程將會先執(zhí)行)。在Solaris里,支持231個(gè)優(yōu)先等級,當(dāng)然對于支持Java那10個(gè)的等級是沒問題的。
在NT下,最多只有7優(yōu)先級劃分,卻必須映射到Java的10個(gè)等級。這就會出現(xiàn)很多的可能性(可能Java里面的優(yōu)先級1、2就等同于NT里的優(yōu)先級1,優(yōu)先級8、9、10則等于NT里的7級,還有很多的可能性)。因此,在NT下依靠優(yōu)先級來調(diào)度線程時(shí)存在很多問題。
更不幸的還在后面呢!NT下的線程優(yōu)先級竟然還不是固定的!這就更加復(fù)雜了!NT提供了一個(gè)名叫優(yōu)先級助推(Priority Boosting)的機(jī)制。這個(gè)機(jī)制使程序員可以通過調(diào)用一個(gè)C語言的系統(tǒng)Call(Windows NT/2000/XP: You can disable the priority-boosting feature by calling the SetProcessPriorityBoost or SetThreadPriorityBoost function. To determine whether this feature has been disabled, call the GetProcessPriorityBoost or GetThreadPriorityBoost function.)來改變線程優(yōu)先級,但Java不能這樣做。當(dāng)打開了Priority Boosting功能的時(shí)候,NT依據(jù)線程每次執(zhí)行I/O相關(guān)的系統(tǒng)調(diào)用的大概時(shí)間來提高該線程的優(yōu)先級。在實(shí)踐中,這意味著一個(gè)線程的優(yōu)先級可能高過你的想象,因?yàn)檫@個(gè)線程碰巧在一個(gè)繁忙的時(shí)刻進(jìn)行了一次I/O操作。線程優(yōu)先級助推機(jī)制的目的是為了防止一個(gè)后臺進(jìn)程(或線程)影響了前臺的UI顯示進(jìn)程。其它的操作系統(tǒng)同樣有著復(fù)雜的算法來降低后臺進(jìn)程的優(yōu)先級。這個(gè)機(jī)制的一個(gè)嚴(yán)重的副作用就是使我們無法通過優(yōu)先級來判斷即將運(yùn)行的就緒太線程。
在這種情況下,事態(tài)往往會變得更糟。
在Solaris中,也就意味著在所有的Unix系統(tǒng)中,或者說在所有當(dāng)代的操作系統(tǒng)中,除了微軟的以外,每個(gè)進(jìn)程或者線程都有優(yōu)先級。高優(yōu)先級的進(jìn)程時(shí)不會被低優(yōu)先級的進(jìn)程打斷的,此外,一個(gè)進(jìn)程的優(yōu)先級可以由管理員限制和設(shè)定,以防止一個(gè)用戶進(jìn)程是打斷OS核心進(jìn)程或者服務(wù)。NT對此都無法支持。一個(gè)NT的進(jìn)程就是一個(gè)內(nèi)存的地址空間。它沒有固定優(yōu)先級,也不能被預(yù)先編排。而全部交由系統(tǒng)來調(diào)度,如果一個(gè)線程運(yùn)行在一個(gè)不再內(nèi)存中的進(jìn)程下,這個(gè)進(jìn)程將會被切換進(jìn)內(nèi)存。NT中進(jìn)程的優(yōu)先級被簡化為幾個(gè)分布在實(shí)際優(yōu)先級范圍內(nèi)的優(yōu)先級類,也就是說他們是不固定的,由系統(tǒng)核心干預(yù)調(diào)配。如1.2圖:
圖1.2 Windows NT優(yōu)先級架構(gòu)
上圖中的列,代表線程的優(yōu)先級,只有22級是有所有的程序所使用(其它的只能為NT自己使用)。行代表前面提過的優(yōu)先級類。
一個(gè)運(yùn)行在“Idle”級進(jìn)程上的線程,只能使用1-6 和 15,這七個(gè)優(yōu)先級別,當(dāng)然具體的是那一級,還要取決于線程的設(shè)定。運(yùn)行在“Normal”級進(jìn)程里并且沒有得到焦點(diǎn)的一個(gè)線程將可能會使用1,6 — 10或15的優(yōu)先級。如果有焦點(diǎn)而且所在進(jìn)程使還是“Normal”級,這樣里面的線程將會運(yùn)行在1,7 — 11或者15 級。這也就意味著一個(gè)擁有搞優(yōu)先級但在“Idle”進(jìn)程內(nèi)的線程,有可能被一個(gè)低優(yōu)先級但是運(yùn)行在“Normal”級的線程搶先(Preempt),但這只限于后臺進(jìn)程。還應(yīng)該注意到一個(gè)運(yùn)行在“High”優(yōu)先級類的進(jìn)程只有6個(gè)優(yōu)先級等級而其它優(yōu)先級類都有7。
NT不對進(jìn)程的優(yōu)先級類進(jìn)行任何限制。運(yùn)行在任意進(jìn)程上的任意線程,可以通過優(yōu)先級助推機(jī)制完全控制整個(gè)系統(tǒng), OS核心沒有任何的防御。另一方面,Solaris完全支持進(jìn)程優(yōu)先級的機(jī)制,因?yàn)槟憧赡苄枰O(shè)定你的屏幕保護(hù)程序的優(yōu)先級,以防止它阻礙系統(tǒng)重要進(jìn)程的運(yùn)行J。因?yàn)樵谝慌_關(guān)鍵的服務(wù)器中,優(yōu)先級低的進(jìn)程就不應(yīng)該也不能占用高優(yōu)先級的線程執(zhí)行。由此可見,微軟的操作系統(tǒng)根本不適合做高可靠性服務(wù)器。
那么我們在編程的時(shí)候,怎樣避免呢?對于NT的這種無限制的優(yōu)先級設(shè)定和無法控制的優(yōu)先級助推機(jī)制(對于Java程序),事實(shí)上就沒有絕對安全的方法來使Java程序依賴優(yōu)先級調(diào)度線程的執(zhí)行。一個(gè)折衷的方法,就是在用setPriority( )函數(shù)設(shè)定線程優(yōu)先級的時(shí)候,只使用Thread.MAX_PRIORITY, Thread.MIN_PRIORITY和Thread.NORM_PRIORITY這幾個(gè)沒有具體指明優(yōu)先級的參數(shù)。這個(gè)限制至少可以避免把10級映射為7級的問題。另外,還建議可以通過os.name的系統(tǒng)屬性來判定是否是NT,如果是就通過調(diào)用一個(gè)本地函數(shù)來關(guān)閉優(yōu)先級助推機(jī)制,但是那對于運(yùn)行在沒有使用Sun的JVM plug-in的IE的Java程序,也是毫無作用的(微軟的JVM使用了一個(gè)非標(biāo)準(zhǔn)的,本地實(shí)現(xiàn))。最后,還是建議大家在編程的時(shí)候,把大多數(shù)的線程的優(yōu)先級設(shè)置為NORM_PRIORITY,并且依靠線程調(diào)度機(jī)制(scheduling)。(我將會再后面的談到這個(gè)問題)
協(xié)作?。–ooperate)
一般來說,有兩種線程模式:協(xié)作式和搶先式。
協(xié)作式多線程模型
在一個(gè)協(xié)作式的系統(tǒng)中,一個(gè)線程包留對處理器的控制直到它自己決定放棄(也許它永遠(yuǎn)不會放棄控制權(quán))。多個(gè)線程之間不得不相互合作否則可能只有一個(gè)線程能夠執(zhí)行,其它都處于饑餓狀態(tài)。在大多數(shù)的協(xié)作式系統(tǒng)中,線程的調(diào)度一般由優(yōu)先級決定。當(dāng)當(dāng)前的線程放棄控制權(quán),等待的線程中優(yōu)先級高的將會得到控制權(quán)(一個(gè)特例,就是Window 3.x系統(tǒng),它也是協(xié)作式系統(tǒng),但卻沒有很多的線程執(zhí)行進(jìn)度調(diào)節(jié),得到焦點(diǎn)的窗體獲得控制權(quán))。
協(xié)作式系統(tǒng)相對于搶先式系統(tǒng)的一個(gè)主要優(yōu)點(diǎn)就是它運(yùn)行速度快、代價(jià)低。比如,一個(gè)上下文切換——控制權(quán)由一個(gè)線程轉(zhuǎn)換到另一個(gè)線程——可以完全由用戶模式子系統(tǒng)庫完成,而不需進(jìn)入系統(tǒng)核心態(tài)(在NT下,就相當(dāng)于600個(gè)機(jī)械指令時(shí)間)。在協(xié)作式系統(tǒng)下,用戶態(tài)上下文切換就相當(dāng)于C語言調(diào)用,setjump / longjump。大量的協(xié)作式線程同時(shí)運(yùn)行,也不會影響性能,因?yàn)橐磺杏沙绦騿T編程掌握,更加不需要考慮同步的問題。程序員只要保證它的線程在沒有完成目標(biāo)以前不放棄對CPU的控制。但世界終歸不是完美的,人生總充滿了遺憾。協(xié)同式線程模型也有自己硬傷:
1.         在協(xié)作式線程模型下,用戶編程非常麻煩(其實(shí)就是系統(tǒng)把復(fù)雜度轉(zhuǎn)移到用戶身上)。把很長的操作分割成許多小塊,是一件需要很小心的事。
2.         協(xié)作式線程無法并行執(zhí)行。
搶先式多線程模型
另一種選擇就是搶先式模型,這種模式就好象是有一個(gè)系統(tǒng)內(nèi)部的時(shí)鐘,由它來觸發(fā)線程的切換。也就是說,系統(tǒng)可以任意的從線程中奪回對CPU的控制權(quán),在把控制權(quán)分給其它的線程。兩次切換之間的時(shí)間間隔就叫做時(shí)間切片(Time Slice)。
搶先式系統(tǒng)的效率不如協(xié)作式高,因?yàn)镺S核心必須負(fù)責(zé)管理線程,但是這樣使用戶在編程的時(shí)候不用考慮那么多的其它問題,簡化了用戶的工作,而且使程序更加可靠,因?yàn)榫€程饑餓不再是一個(gè)問題。最關(guān)鍵的優(yōu)勢在于搶先式模型是并行的。通過前面的介紹,你可以知道協(xié)作式線程調(diào)度是由用戶子程序完成,而不是OS,因此,你最多可以做到使你的程序具有并發(fā)性(如圖1.1)。為了達(dá)到真正并行的目的,必須有操作系統(tǒng)介入。四個(gè)線程并行的運(yùn)行在四個(gè)CPU上的效率要比四個(gè)線程并發(fā)的運(yùn)行高的多。
一些操作系統(tǒng),象Windows 3.1,只支持協(xié)作式模型;還有一些,象NT只支持搶先式模型(當(dāng)然了你也可以通過使用用戶模式的庫調(diào)用在NT上模擬協(xié)作式模型。NT就有這么一個(gè)庫叫“fiber”,但是遺憾的是fiber充滿的了Bugs,并且沒有徹底的整合到底層系統(tǒng)中去。)Solaris提供了世界上可能是最好的(當(dāng)然也可能是最差的)線程模型,它既支持協(xié)作式,又支持搶先式。
從核心線程到用戶進(jìn)程的映射
最后一個(gè)要解決的問題就是核心線程到用戶態(tài)進(jìn)程的映射。NT使用的是一對一的映射模式,見圖1.3。
圖1.3 NT的線程模型
NT的用戶線程就相當(dāng)于系統(tǒng)核心線程。他們被OS直接映射到每一個(gè)處理器上,并且總是搶先式的。所有的線程操作和同步都是通過核心調(diào)用完成的。這是一個(gè)非常直接的模型,但是它既不靈活又低效。
圖1.4表現(xiàn)的Solaris模型更加有趣。Solaris增加了一個(gè)叫輕量級進(jìn)程(LWP — lightweight process)的概念。LWP是可以運(yùn)行一個(gè)或多個(gè)線程的可調(diào)度單元。只在LWP這個(gè)程次上進(jìn)行并行處理。一般的,LWP都存放在緩沖池中,并且按需分配給相應(yīng)的處理器。如果一個(gè)LWP要執(zhí)行某些時(shí)序性要求很高的任務(wù)時(shí),它一定要綁定特定處理器,以阻止其它的LWPs使用它。
從用戶的角度來看,這是一個(gè)既協(xié)作又搶先的線程模型。簡單來說,一個(gè)進(jìn)程至少有一個(gè)LWP供它所包含的所有線程共享使用。每個(gè)線程必須通過出讓使用權(quán)(yield)來讓其它線程執(zhí)行(協(xié)作),但是單個(gè)LWP又可以為其它的進(jìn)程的LWP搶先。這樣在進(jìn)程一級上就達(dá)到了并行的效果,同時(shí)在里面線程又處于協(xié)作的工作模式。
一個(gè)進(jìn)程并不限死只有一個(gè)LWP,這個(gè)進(jìn)程下的線程們可以共享整個(gè)LWP池。一個(gè)線程可以通過以下兩種方法綁定到一個(gè)LWP上:
1.         通過編程顯示的綁定一個(gè)或多個(gè)線程到一個(gè)指定的LWP。在這個(gè)情況下,同一個(gè)LWP下的線程必須協(xié)作式工作,但是這些線程又可以搶先其它LWP下的線程。這也就是說如果限制一個(gè)LWP只能綁定一個(gè)線程,那就變成了NT的搶先式線程系統(tǒng)了。
2.         通過用戶態(tài)的調(diào)度器自動(dòng)綁定。從編程的角度看,這是一個(gè)比較混亂的情況,因?yàn)槟悴⒉荒芗僭O(shè)環(huán)境究竟是協(xié)作式的,還是搶先式的。
線程系統(tǒng)給于用戶最大的靈活性。你可以在速度極快的并發(fā)協(xié)作式系統(tǒng)和較慢但
確是并行的搶先式系統(tǒng),或者這兩者的折衷中選擇。但是,Solaris的世界真的是那么完美嗎?(我的一貫論調(diào)又出現(xiàn)了,呵呵?。┻@一切一切的靈活性對于一個(gè)Java程序員來說等于沒有,因?yàn)槟悴⒉荒軟Q定JVM采用的線程模型。例如,早期的Solaris JVM采取嚴(yán)格的協(xié)作式機(jī)制。JVM相當(dāng)于一個(gè)LWP,所有Java線程共享這唯一一個(gè)LWP。現(xiàn)在Solaris JVM又采用徹底的搶先式模型了,所有的線程獨(dú)占各自的LWP。
那么我們這些可憐的程序員怎么辦?我們在這個(gè)世上是如此的渺小,就連JVM采用那種模式的線程機(jī)制都無法確定。為了寫出平臺獨(dú)立的代碼,必須做出兩個(gè)表面上矛盾的假設(shè):
1.         一個(gè)線程可以被另一個(gè)線程在任何時(shí)候搶先。因此,你必須小心的使用synchronized關(guān)鍵字來保證非原子性的操作運(yùn)行正確。
2.         一個(gè)線程永遠(yuǎn)不會被搶先除非它自己放棄控制權(quán)。因此,你必須偶然的執(zhí)行一些放棄控制權(quán)的操作來給機(jī)會別的線程運(yùn)行,適當(dāng)?shù)氖褂脃ield( )和sleep( )或者利用阻塞性的I/O調(diào)用。例如當(dāng)你的線程每進(jìn)行100次遍例或者相當(dāng)長度的密集操作后,你應(yīng)該主動(dòng)的使你的線程休眠幾百個(gè)毫秒,來給低優(yōu)先級的線程以機(jī)會運(yùn)行。注意!yield( )方法只會把控制權(quán)交給與你線程的優(yōu)先級相當(dāng)或者更高的線程。
圖1.4  Solaris 線程模型
總結(jié)
由于諸多的OS級因素導(dǎo)致了Java程序員在編寫徹底平臺獨(dú)立的多線程程序時(shí),麻煩頻頻(唉~~~~~~我又想發(fā)牢騷了,忍?。。N覀冎荒馨醋钤愀獾那闆r打算,例如只能假設(shè)你的線程隨時(shí)都會被搶先,所以必須適當(dāng)?shù)氖褂胹ynchronized;又不得不假設(shè)你的線程永遠(yuǎn)不會被搶先,如果你不自己放棄,所以你又必須偶爾使用yield( )和sleep( )或阻塞的I/O操作來讓出控制權(quán)。還有就是我一開始就介紹的:永遠(yuǎn)不要相信線程優(yōu)先級,如果你想真正做到平臺獨(dú)立!
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報(bào)
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
編程中國-解析Java的多線程機(jī)制
java thread setDaemon的細(xì)節(jié)
Java多線程編程總結(jié)
java多線程-概念&創(chuàng)建啟動(dòng)&中斷&守護(hù)線程&優(yōu)先級&線程狀態(tài)(多線程編程之一)
學(xué)習(xí)Java多線程之線程定義、狀態(tài)和屬性
深入淺出:JAVA多線程編程實(shí)戰(zhàn)-基礎(chǔ)篇
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服