#
本文內容
同步不是改善程序安全性的靈丹妙藥。
發生死鎖的兩種情況和解決方法。
同步不是改善程序安全性的靈丹妙藥
從《線程的同步》一節中我們可以知道,synchronized能保證只有一個線程進入同步方法或同步塊,但為了安全性盲目給多線程程序加上synchronized關鍵字并不是問題解決之道,這不但會降低程序的效率;還有可能帶來嚴重的問題-死鎖。
死鎖發生在兩個或以上的線程在等待對象鎖被釋放,但程序的環境卻讓lock無法釋放時。下面我們將看到兩種類型的死鎖例子。
某線程不退出同步函數造成的死鎖
public class PaintBoard extends Thread{
private boolean flag=true;
public void paint(){
System.out.println("模擬繪畫");
}
public synchronized void run(){
while(flag){
try{
paint();
Thread.sleep(1000);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
public synchronized void stopDraw(){
flag=false;
System.out.println("禁止繪畫");
}
public static void main(String[] args){
PaintBoard paintBoard=new PaintBoard();
paintBoard.start();
new StopThread(paintBoard);
}
}
public class StopThread implements Runnable{
private PaintBoard paintBoard;
public StopThread(PaintBoard paintBoard){
this.paintBoard=paintBoard;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
System.out.println("試圖停止繪畫過程");
paintBoard.stopDraw();
System.out.println("停止繪畫過程完成");
}
}
}
問題的發生和解決
剛才的死鎖原因是run()函數中有一個無限循環,一個線程進入后會在其中往復操作,這使它永遠不會放棄對this的鎖定,結果導致其它線程無法獲得this的鎖定而進入stopDraw函數。
我們把修飾run函數的synchronized取消就能解決問題。 run函數中不會改變任何量,這種函數是不該加上synchronized的。
兩個線程爭搶資源造成的死鎖.
public class Desk{
private Object fork=new Object();
private Object knife=new Object();
public void eatForLeft(){
synchronized(fork){
System.out.println("左撇子拿起叉");
sleep(1);
synchronized(knife){
System.out.println("左撇子拿起刀");
System.out.println("左撇子開始吃飯");
}
}
}
public void eatForRight(){
synchronized(knife){
System.out.println("右撇子拿起刀");
sleep(1);
synchronized(fork){
System.out.println("右撇子拿起叉");
System.out.println("右撇子開始吃飯");
}
}
}
private void sleep(int second){
try{
Thread.sleep(second*1000);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
Desk desk=new Desk();
new LeftHand(desk);
new RightHand(desk);
}
}
public class LeftHand implements Runnable{
private Desk desk;
public LeftHand(Desk desk){
this.desk=desk;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
desk.eatForLeft();
}
}
}
public class RightHand implements Runnable{
private Desk desk;
public RightHand(Desk desk){
this.desk=desk;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
desk.eatForRight();
}
}
}
問題的發生和解決
這部分程序中于兩個線程都要獲得兩個對象的鎖定才能執行實質性操作,但運行起來卻發現其它線程持有了自己需要的另一個鎖定,于是停在Wait Set中等待對方釋放這個鎖定,結果造成了死鎖。
解決這個問題的方法是保證鎖對象的持有順序,如果兩個加上了同步的函數都是先刀后叉的形式則不會發生問題。
小結
同步不是改善程序安全性的靈丹妙藥,盲目同步也會導致嚴重的問題-死鎖.
某線程持續不退出同步函數會造成死鎖.解決方法是去掉或更換不正確的同步。
兩個線程都等待對方釋放自己需要的資源也會造成死鎖.這種情況的解決方法是確保同步鎖對象的持有順序。
多線程操作同一實例的問題
在多線程環境中,經常有兩個以上線程操作同一實例的問題,無論是并行Parallel環境還是并發Concurrent環境,都有發生有多個線程修改同一變量的問題,如果這個變量是成員變量,多線程將會給程序帶來破壞性的影響。請見以下代碼。
資源庫類
public class ResourceLib {
private long count1;
private long count2;
public ResourceLib(int count) {
this.count1 = count;
this.count2 = count;
}
/**
* 取回資源
* 加上synchronized才是線程安全
*
* @param count
*/
public void fetch(int count) {
count1 += count;
mockLongTimeProcess();
count2 += count;
checkTwoCount(count);
}
/**
* 送出資源
* 加上synchronized才是線程安全
*
* @param count
* @return
*/
public void send(int count) {
count1 -= count;
mockLongTimeProcess();
count2 -= count;
checkTwoCount(count);
}
/**
* 模擬一個耗時過程
*
*/
private void mockLongTimeProcess(){
try{
Thread.sleep(1000);
}
catch(Exception ex){
ex.printStackTrace();
}
}
private void checkTwoCount(int borrowCount) {
if (count1 != count2) {
System.out.println(count1 + "!= " + count2);
System.exit(0);
} else {
System.out.println(count1 + "==" + count2);
}
if (Math.abs(count1) > 10000000 || Math.abs(count2) > 10000000) {
count1 = 0;
count2 = 0;
}
}
public static void main(String[] args) {
ResourceLib lib = new ResourceLib(10000);
for (int i = 1; i < 20; i++) {
new Supplier(String.valueOf(i), i, lib);
}
for (int i = 1; i < 10; i++) {
new Comsumer(String.valueOf(i), i, lib);
}
}
}
取資源和給資源的兩個線程
public class Comsumer implements Runnable{
private ResourceLib resourceLib;
private int count;
public Comsumer(String name,int count,ResourceLib resourceLib){
this.count=count;
this.resourceLib=resourceLib;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
resourceLib.send(count);
}
}
}
public class Supplier implements Runnable{
private ResourceLib resourceLib;
private int count;
public Supplier(String name,int count,ResourceLib resourceLib){
this.count=count;
this.resourceLib=resourceLib;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
resourceLib.fetch(count);
}
}
}
運行結果
在main函數中,程序啟動了多個消費者線程和生產者線程,消費者線程在不斷減少count1和count2;生產者線程在不斷增加count1和count2,在單線程環境中,程序絕不會出現count1和count2不相等的情況,而多線程環境中,可能有一個線程在檢查count1和count2時,其中一個已經被另一個線程所修改。
因此導致了兩個值不相等的情況發生。
運行結果之一
10145!= 10001
10145!= 10003
10145!= 10006
10145!= 10010
10145!= 10015
10145!= 10021
10145!= 10028
10145!= 10036
10145!= 10045
10145!= 10055
10145!= 10066
另一個經典多線程實例:銀行取款
package com.sitinspring.unsafebank;
public class Bank{
private int count;
public Bank(int count){
this.count=count;
}
public void withdraw(int money){
if(count>money){
mockLongTimeProcess();// 模擬耗時過程
count-=money;
System.out.println("提走"+money+" 現有"+count);
}
else{
System.out.println(" 現有數量"+count+"小于"+money+" 不能提取");
}
checkCount();
}
public void checkCount(){
if(count<0){
System.out.println(count + "< 0 ");
System.exit(0);
}
}
/**
* 模擬一個耗時過程
*
*/
private void mockLongTimeProcess(){
try{
Thread.sleep(1000);
}
catch(Exception ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
Bank bank=new Bank(1000);
for(int i=1;i<10;i++){
new Customer(i*i*i,bank);
}
}
}
客戶類及講述
public class Customer implements Runnable{
private Bank bank;
private int count;
public Customer(int count,Bank bank){
this.count=count;
this.bank=bank;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
bank.withdraw(count);
}
}
}
在單線程環境中,提款時銀行的總數絕不會是負數,但在多線程環境中,有可能在一個線程A符合條件在進行耗時運算和網絡數據傳遞時,另一個線程B已經把錢提走,總數已經發生變化,結果A線程再提款時總錢數已經減小了,因此致使銀行總錢數小于零。
解決方法:在對成員變量進行修改的函數前加上synchronized關鍵字
synchronized方法又被成為”同步“方法。當一個方法加上關鍵字synchronized聲明之后,就可以讓一個線程操作這個方法。“讓一個線程操作”并不是說只能讓某一個特定的線程操作而已,而是指一次只能讓一個線程執行,也就是說,在一個線程沒有退出同步方法前,其它線程絕無可能進入這個同步方法和其它并列的同步方法,只能在外面排隊等候。
一個實例的synchronized方法只能允許1次一個線程執行。但是非synchronized方法就沒有這個限制,它可以供2個以上的線程執行。
修改后的線程安全的Bank類
public class Bank{
private int count;
public Bank(int count){
this.count=count;
}
public synchronized void withdraw(int money){
if(count>money){
mockLongTimeProcess();// 模擬耗時過程
count-=money;
System.out.println("提走"+money+" 現有"+count);
}
else{
System.out.println(" 現有數量"+count+"小于"+money+" 不能提取");
}
checkCount();
}
public void checkCount(){
if(count<0){
System.out.println(count + "< 0 ");
System.exit(0);
}
}
。。。、// 部分代碼省略
}

修改后的線程安全的ResourceLib類
public class ResourceLib {
private long count1;
private long count2;
public synchronized void fetch(int count) {
count1 += count;
mockLongTimeProcess();
count2 += count;
checkTwoCount(count);
}
public synchronized void send(int count) {
count1 -= count;
mockLongTimeProcess();
count2 -= count;
checkTwoCount(count);
}
public void checkTwoCount(int borrowCount) {
if (count1 != count2) {
System.out.println(count1 + "!= " + count2);
System.exit(0);
} else {
System.out.println(count1 + "==" + count2);
}
if (Math.abs(count1) > 10000000 || Math.abs(count2) > 10000000) {
count1 = 0;
count2 = 0;
}
}
}
注:部分代碼省略

執行之后
在一個執行synchronized方法的線程執行結束后,鎖定即被釋放, 其它不得其門而入的線程開始爭搶鎖定,一定會有一個線程獲取鎖定,沒有搶到的線程只好再繼續等候.
注意: 非靜態的synchronized方法鎖定的對象是實例,靜態的synchronized方法鎖定的對象是類對象。
同步塊
以下同步方法可用右邊的同步塊代替:
public synchronized void fun(){
………
}
與左邊同步方法對等的同步塊:
public void fun(){
synchronized(this){
………
}
}
同步塊和同步方法的比較
1)同步方法鎖定的類的實例或類對象,同步塊則可以換成任意實例,靈活性更高。
2)有時需要多個鎖定而不是一個,如函數A和函數B需要鎖定1,函數B和函數C需要鎖定2,這時如果使用同步方法無疑會鎖定A和C,造成程序效率的降低。這時最應該使用同步塊。
什么時候該加同步synchronized
如果一個函數或代碼塊有可能被多個線程進入,而這個函數或代碼塊又修改了類的成員變量,則這個這個函數或代碼塊就應該加上同步synchronized。
如果一個函數或代碼有可能被多個線程進入,而這個函數或代碼塊只是讀取類的成員變量,則這個這個函數或代碼塊就不該加上同步synchronized。
單線程程序
一般來說,在沒有線程的幫助下,程序在一個時間段只能執行一段代碼,其它代碼段只有在等待它完成后才能執行。該程序的處理流程從頭到尾只有一條線,這樣的程序我們稱之為單線程程序(Single Thread Program)
典型的單線程程序:
public class SingleThreadProgram{
public static void main(String[] args){
for(int i=0;i<1000;i++){
System.out.print("SingleThreadProgram");
}
}
}
多線程程序
當程序由一個以上的線程所構成時,稱此程序為多線程程序(Multithread Program),java從設計伊始就把程序的多線程能力列入了考慮范圍。
典型的多線程程序有:
1)GUI應用程序,我們目前做的Swing桌面程序就屬于此類。
2)較花費時間的I/O處理,一般來說,文件和網絡的輸入/輸出處理比較花費時間,如果在這段無法進行其它處理,則程序性能會大打折扣,遇到這種情況首先要想到用多線程解決問題.
3)多連接網絡處理。
并發(Concurrent)與并行(Parallel)
當有多個線程在操作時,如果系統只有一個CPU,則它根本不可能真正同時進行一個以上的線程,它只能把CPU運行時間劃分成若干個時間段,再將時間段分配給各個線程執行,在一個時間段的線程代碼運行時,其它線程處于掛起狀態.這種方式我們稱之為并發(Concurrent).
當系統有一個以上CPU時,則線程的操作有可能非并發.當一個CPU執行一個線程時,另一個CPU可以執行另一個線程,兩個線程互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel)

多線程在并發和并行環境中的不同作用
在并發環境時,多線程不可能真正充分利用CPU,節約運行時間,它只是以”掛起->執行->掛起”的方式以很小的時間片分別運行各個線程,給用戶以每個線程都在運行的錯覺.在這種環境中,多線程程序真正改善的是系統的響應性能和程序的友好性.
在并行環境中, 一個時刻允許多個線程運行,這時多線程程序才真正充分利用了多CPU的處理能力, 節省了整體的運行時間.在這種環境中,多線程程序能體現出它的四大優勢:充分利用CPU,節省時間,改善響應和增加程序的友好性.
PS:在多核時代來臨后,開發多線程程序的能力更是每個程序員都該具備的.
創建多線程程序
創建多線程程序我們通常有兩種方法:
1)讓類繼承java.lang.Thread,這種方法優勢在于調用稍微方便,一般用于后臺批處理程序的場合,但劣勢是類無法再繼承別的類。
2)讓類實現接口java.lang.Runnable,這種方法調用時需要借助Thread的幫助,稍顯麻煩,但優勢在于對類繼承體系沒有影響,這是使用線程時最常用的方法。
兩種方法的線程執行部分都在run()函數中,它們的效率沒有差別。
多線程程序創建和啟動示例
創建線程
// 繼承Thread類
public class Thread1 extends Thread{
public void run(){
while(true){
System.out.println("<Thread1 extends Thread>");
}
}
}
// 實現Runnable接口
public class Thread2 implements Runnable{
public void run(){
while(true){
System.out.println("<Thread2 implements Runnable>");
}
}
}
啟動線程
public class Main{
public static void main(String[] args){
// 啟動線程1,Thread1直接繼承自java.lang.Thread類
Thread1 th1=new Thread1();
th1.start();
// 啟動線程2,thread2實現自java.lang.Runnable接口
Thread2 thread2=new Thread2();
Thread th2=new Thread(thread2);
th2.start();
while(true){
System.out.println("<Main Thread>");
}
}
}
概念解析Start和Run
public void run()
這個函數容納線程啟動后執行的代碼塊,線程啟動起來,run函數中的代碼會得到執行.
Thead.start()
這是啟動一個線程的方法,調用了這個方法后,線程才會得到執行.
取得線程執行的結果
通過觀察run函數的簽名public void run()我們可以發現,它既沒有輸入參數,也沒有返回值,那如何取得線程的返回值呢?一般來說我們有三種辦法:
1)讓線程修改公有變量,如某類的靜態公有字段.這種方式古老而危險,最好不要采用.
2)輪詢線程執行結果,線程執行的結果放在線程類的一個字段中,外界不斷通過輪詢去查看執行結果.這種方式會浪費很多時間,結果也不可靠,不建議采用.
3)回調方式,把調用方的指針通過線程類的構造函數傳入線程類的一個字段中,當線程執行完取得結果后再通過這個字段反向調用調用方的函數.這是取得線程執行結果的最佳解決方案.
下面請看回調方式的實現.
Boss類
這個類用于啟動Secretary線程去查找文件, findFile()是啟動線程并查找的函數, giveBossResult(String file,String reult)是供Secretary類回調的函數.
public class Boss{
private String name;
public Boss(String name){
this.name=name;
}
public void giveBossResult(String file,String reult){
if(reult!=null){
System.out.println("文件"+file+"序列號等于:"+reult);
}
else{
System.out.println("無法找到文件"+file);
}
}
public void findFile(){
Map<String,String> files=new Hashtable<String,String>();
files.put("001", "員工花名冊");
files.put("002", "企業收支");
files.put("003", "客戶花名錄");
files.put("004", "對手狀況分析");
files.put("005", "當月收支");
files.put("006", "市場份額分析");
files.put("007", "大連酒店一覽");
files.put("008", "娛樂場所名錄");
files.put("009", "關系單位聯系名錄");
Secretary andy=new Secretary("Andy",this,"員工花名冊",files);
Thread th1=new Thread(andy);
th1.start();
Secretary cindy=new Secretary("cindy",this,"上市情況分析",files);
Thread th2=new Thread(cindy);
th2.start();
}
public static void main(String[] args){
Boss boss=new Boss("Bill");
boss.findFile();
}
}
Secretary類
這個類是進行多線程查找文件的類,查找的結果通過回調方法告知Boss實例.
Boss實例,查找的文件名,查找的集合都通過Secretary類的構造函數傳進來.
public class Secretary implements Runnable{
private String name;
private Boss boss;
private String file;
private Map<String,String> files;
public Secretary(String name,Boss boss,String file,Map<String,String> files){
this.name=name;
this.boss=boss;
this.file=file;
this.files=files;
}
public void run(){
for(Map.Entry<String,String> entry:files.entrySet()){
if(entry.getValue().equals(file)){
boss.giveBossResult(file,entry.getKey());
return;
}
}
boss.giveBossResult(file,null);
}
}
摘要: 一.Prop類(用來讀取屬性文件,單例)
package com.sitinspring.standardWeblogicJms;
import java.io.FileInputStream;
import java.util.Hashtable;
import java.util.Properties;
import ja...
閱讀全文
一.WeblogicMDB類(Message Driven Bean)
import javax.ejb.EJBException;
import javax.ejb.MessageDrivenBean;
import javax.ejb.MessageDrivenContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;


public class WeblogicMDB implements MessageDrivenBean, MessageListener
{
private static final long serialVersionUID = 5582665474886073061L;

private MessageDrivenContext context;

private Context jndiContext;

public void setMessageDrivenContext(MessageDrivenContext context)

throws EJBException
{
this.context = context;

try
{
jndiContext = new InitialContext();

} catch (Exception ex)
{
ex.printStackTrace();
}
}


public void ejbCreate()
{

}


public void ejbRemove()
{

}


public void onMessage(Message message)
{

if (message instanceof TextMessage)
{
//System.out.println("Yeah! I have received the TextMassage:");
TextMessage txtmsg = (TextMessage) message;


try
{
System.out.println("I have received the TextMassage:");
System.out.println(txtmsg.getText());

} catch (JMSException e)
{
e.printStackTrace();
}
}
}
}
二.ejb-jar.xml
<?xml version="1.0"?>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_2_0.dtd">

<ejb-jar>

<enterprise-beans>

<message-driven>
<ejb-name>WeblogicMDBName</ejb-name>
<ejb-class>WeblogicMDB</ejb-class>
<transaction-type>Container</transaction-type>
<message-driven-destination>
<destination-type>javax.jms.Queue</destination-type>
</message-driven-destination>
<env-entry>
<description>This is a bean listening on a queue.</description>
<env-entry-name>listen_type</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>queue</env-entry-value>
</env-entry>
</message-driven>
</enterprise-beans>
</ejb-jar>





三.weblogic-ejb-jar.xml
<?xml version="1.0"?>
<!DOCTYPE weblogic-ejb-jar PUBLIC '-//BEA Systems, Inc.//DTD WebLogic 8.1.0 EJB//EN' 'http://www.bea.com/servers/wls810/dtd/weblogic-ejb-jar.dtd'>

<weblogic-ejb-jar>

<weblogic-enterprise-bean>
<ejb-name>WeblogicMDBName</ejb-name>
<message-driven-descriptor>
<pool>
<max-beans-in-free-pool>10</max-beans-in-free-pool>
<initial-beans-in-free-pool>
2
</initial-beans-in-free-pool>
</pool>
<destination-jndi-name>MyJMSQueue</destination-jndi-name>
<initial-context-factory>
weblogic.jndi.WLInitialContextFactory
</initial-context-factory>
<provider-url>t3://127.0.0.1:7001/</provider-url>
<connection-factory-jndi-name>
MyJMSConnectionFactory
</connection-factory-jndi-name>
</message-driven-descriptor>
</weblogic-enterprise-bean>
</weblogic-ejb-jar>


使用上一篇文章中的QueueSupplier發送消息,輸出示例:
過程很簡單,值得注意的是系統的JDK最好和Weblogic使用的保持一致,至少不能存在代差.例如
輸出消息我曾經是這樣寫的:
System.out.println("I have received the TextMassage:"+txtmsg.getText());
系統中的JDK1.5使用StringBuilder來把兩個字符串加在一起,而Weblogic自帶的1.4不認識StringBuilder,就產生了異常.
而制定Weblogic8.1的JDK為系統中的JDK1.5又會導致錯誤. 所以,編程時建議JDK和Weblogic8.1的JDK保持一致,以避免發生莫明其妙的錯誤.
程序下載:
http://www.aygfsteel.com/Files/sitinspring/WeblogicMDB20070910131749.rar
摘要: 1.JmsQueueSender類,用于發送消息
package com.sitinspring.springjms;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jm...
閱讀全文
摘要: 分類匯總是統計中常用,舉例來說如統計學生成績,及格不及格的歸類,分優良中差等級歸類等,每個單項代碼很好寫,但是如果分類匯總的項目多了,能一種匯總寫一個函數嗎? 比如說有些科目60分才算及格,有些科目50分就算;有些老師喜歡分優良中差四等,有些老師卻喜歡分ABCD;不一而足,如果每個都寫一個函數無疑是個編寫和維護惡夢. 如果我們用匿名類把分類匯總的規則和分類匯總的過程分別抽象出來,代碼就清晰靈活多了...
閱讀全文
摘要: 本例完整程序下載: http://www.aygfsteel.com/Files/sitinspring/TaxCaculator20071025203159.rar
世界天天在變,程序也一樣,唯一不變的只有變化二字.現代程序應該隨著日新月異的環境而不斷變化,此之謂"物競天擇,適者生存",下面的例子就演示了這一變化過程.
需求如下:(注:非真實稅率,僅僅是個例子)
&nb...
閱讀全文
摘要: 需求:給出三角形的三邊長,判斷是否三角形,如是,判斷是等邊三角形,等腰三角形,不等邊三角形,銳角三角形,直角三角形和鈍角三角形,并計算出三角形的面積.
考查點:建模的準確性,思維的全面性,浮點數的比較.
補充知識:cosA=b方+c方-a方/2*b*c
package com.sitinspring;
/** *//*...
閱讀全文