#
本文内容
同步不是改善程序安全性的灵丹妙药。
发生死锁的两种情况和解决方法。
同步不是改善程序安全性的灵丹妙药
从《线程的同步》一节中我们可以知道,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.blogjava.net/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.blogjava.net/Files/sitinspring/TaxCaculator20071025203159.rar
世界天天在变,程序也一样,唯一不变的只有变化二字.现代程序应该随着日新月异的环境而不断变化,此之谓"物竞天择,适者生存",下面的例子就演示了这一变化过程.
需求如下:(注:非真实税率,仅仅是个例子)
&nb...
阅读全文
摘要: 需求:给出三角形的三边长,判断是否三角形,如是,判断是等边三角形,等腰三角形,不等边三角形,锐角三角形,直角三角形和钝角三角形,并计算出三角形的面积.
考查点:建模的准确性,思维的全面性,浮点数的比较.
补充知识:cosA=b方+c方-a方/2*b*c
package com.sitinspring;
/** *//*...
阅读全文