获取当前线程:用Java线程获取优异性能(II)——使用同步连载线程访问关键代码部份来源: 发布时间:星期三, 2008年12月17日 浏览:98次 评论:0
摘要
开发者有时创建多线程会生成值或产生其它奇怪行为古怪行为般出现在个多线程没使用同步连载线程访问关键代码部份时候同步连载线程访问关键代码部份是什么意思呢?在这篇文章中解释了同步Java同步机制以及当开发者没有正确使用这个机制时出现两个问题旦你看完这篇文章你就可以避免在你多线程Java中因缺乏同步而产生奇怪行为
创建多线程Java难吗?仅从用Java线程获取优异性能(I)中获得信息你就可以回答不毕竟我已经向你显示了如何轻松地创建线程对象通过Threadstart思路方法起动和这些对象相关线程以及通过其它Thread思路方法比如 3个重载join思路方法执行简单线程操作至今仍有许多开发者在开发些多线程时面临困难境遇他们经常功能不稳定或产生值例如个多线程可能将不正确雇员资料存贮在数据库中比如姓名和地址姓名可能属于个雇员而地址却属于另个是什么引起这种奇怪行为呢? 是缺乏同步:连载行为或在同时间排序线程访问那些让多重线程操作类和字段变量例子代码序列以及其他共享资源我称这些代码序列为关键代码部份
注意:不象类和例子字段变量线程不能共享本地变量和参数原因是:本地变量和参数在个线程思路方法中分配——叫堆栈结果每个线程都收到它自己对那些变量拷贝相反线程能够共享类字段和例子字段那些变量在个线程思路方法(叫堆栈)中没有被分配取而代的它们作为类(类字段)或对象(例子字段)部份在共享内存堆中被分配
这篇文章将教你如何使用同步连载线程访问关键代码部份我用个介绍说明为什么些多线程必须使用同步例子作为开始我接下来就监视器和锁探讨Java同步机制和synchronized 关键字我通过研究由这样错用产生两个问题判定常常不正确使用同步机制而否认了它好处
阅读有关线程整个系列:
· 第I部份:介绍线程、线程类及Runnable
· 第II部份:使用同步连载线程访问关键代码部份
对于同步需要
为什么我们需要同步呢?种回答考虑这个例子:你写个使用对线程模拟取款/存款金融事务Java在那个中个线程处理存款同时其它线程正处理取款每个线程操作对共享变量、类及例子字段变量这些用来标识金融事务姓名和账号对于个正确金融事务每个线程必须在其它线程开始给name和amount赋值前(并且同时打印那些值)给name和amount变量赋值(并打印那些值模拟存贮事务)其源代码如下:
列表1. NeedForSynchronizationDemo.java
// NeedForSynchronizationDemo.java
NeedForSynchronizationDemo
{
public void (String args)
{
FinTrans ft = FinTrans ;
TransThread tt1 = TransThread (ft, "Deposit Thread");
TransThread tt2 = TransThread (ft, "Withdrawal Thread");
tt1.start ;
tt2.start ;
}
}
FinTrans
{
public String transName;
public double amount;
}
TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象引用
}
public void run
{
for ( i = 0; i < 100; i)
{
(getName .equals ("Deposit Thread"))
{
//存款线程关键代码部份开始
ft.transName = "Deposit";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
.out.prln (ft.transName + " " + ft.amount);
//存款线程关键代码部份结束
}
{
//取款线程关键代码部份开始
ft.transName = "Withdrawal";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
.out.prln (ft.transName + " " + ft.amount);
//取款线程关键代码部份结束
}
}
}
}
NeedForSynchronizationDemo源代码有两个关键代码部份:个可理解为存款线程另个可理解为取款线程在存款线程关键代码部份中线程分配Deposit String对象引用给共享变量transName及分配2000.0 给共享变量amount同样在取款关键代码部份线程分配Withdrawal String对象引用给transName及分配250.0给amount在每个线程分配的后打印那些变量内容当你运行NeedForSynchronizationDemo时你可能期望输出类似于Withdrawal 250.0 和Deposit 2000.0两行组成列表相反你收到输出如下所示:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 2000.0
Deposit 250.0
明显有问题取款线程不应该模拟$2,000取款存款线程不应该模拟$250存款每个线程产生不致输出是什么引起了这些矛盾呢?我们是如下认为:
· 在个单处理器机器上线程共享处理器结果个线程仅能执行定时间段在其它时间里 JVM/操作系统暂停那个线程执行并允许其它线程执行——种线程时序安排在个多处理器机器上依靠线程和处理器数目每个线程都能拥有它自己处理器
· 在单处理器机器上个线程执行时间段没有足够长到在其它线程开始执行关键代码部份前完成它自己关键代码部分在个多处理器机器上线程能够同时执行它们自己关键代码部份然而它们可能在区别时间进入它们关键代码部份
· 无论是单处理器或是多处理器机器下面情形都可能发生:线程A在它关键代码部份分配个值给共享变量X并决定执行个要求100毫秒输入/输出操作接下来线程B进入它关键代码部份分配个区别值给X执行个50毫秒输入/输出操作并分配值给共享变量Y 和Z线程A输入/输出操作完成并分配它自己值给Y和ZX包含个B分配值然而Y和Z包含A分配值这是个矛盾结果
这个矛盾是怎样在NeedForSynchronizationDemo中产生呢?假设存款线程执行ft.transName = "Deposit"并且接下来Thread.sleep在那点存款线程交出处理器控制段时间进行休眠让取款线程执行假定存款线程休眠500毫秒(感谢Math.random从0到999毫秒范围随机选取个值)在存款线程休眠期间取款线程执行ft.transName = "Withdrawal"休眠50毫秒 (取款线程随机选取休眠值)醒后执行ft.amount = 250.0并执行.out.prln (ft.transName + " " + ft.amount)—所有都在存款线程醒来的前结果取款线程打印Withdrawal 250.0那是正确当存款线程醒来执行ft.amount = 2000.0接下来执行.out.prln (ft.transName + " " + ft.amount)这个时间Withdrawal 2000.0 打印那是不正确虽然存款线程先前分配"Deposit"引用给transName但这个引用随后会在取款线程分配”Withdrawal”引用给那个共享变量时消失当存款线程醒来时它就不能存贮正确引用到transName但通过分配2000.0给amount继续它执行虽然两个变量都不会有无效值但它们结合值却是矛盾假如这样话它们值显示企图取款$2,000
很久以前计算机科学家发明了描述导致矛盾多线程组合行为个术语术语是竞态条件(race condition)—每个线程竞相在其它线程进入同关键代码部份前完成它自己关键代码部份行为作为NeedForSynchronizationDemo示范线程执行顺序是不可知这里不能保证个线程能够在其它线程进入关键代码部份前完成它自己关键代码部份因此我们会有竞态条件引起不致要阻止竞态条件每个线程必须在其它线程进入同关键代码部份或其它操作同共享变量或资源相关关键代码部份前完成它自己关键代码部份对于个关键代码部份没有连载访问思路方法(即是在个时间只允许访问个线程)你就不能阻止竞态条件或不致出现幸运是Java提供了连载线程访问思路方法:通过它同步机制
注意:对于Java类型只有长整型和双精度浮点型变量倾向于不致为什么?个32位JVM般用两个临近32位步长访问个64位长整型变量或个64位双精度浮点型变量个线程可能在完成第步后等待其它线程执行所有两步接下来第个线程可能醒来并完成第 2步产生个值既区别于第个线程也区别于第 2线程值变量结果如果至少个线程能够修改个长整型变量或个双精度浮点型变量那些读取和(或)修改那个变量所有线程就必须使用同步连载访问
Java同步机制
Java提供个同步机制以阻止多于个线程在时间任意点在个或多个关键代码部份执行代码这种机制将自己建立在监视器和锁概念基础上个监视器被作为包在关键代码部份周围保护个锁被作为监视器用来防止多重线程进入监视器个软件Software实体其想法是:当个线程想进入个监视器监视着关键代码部份时那个线程必须获得个和监视器相关对象锁(每个对象都有它自己锁)如果些其它线程保存着这个锁 JVM会强迫请求线程在个和监视器/锁有关等待区域等待当监视器中线程释放锁时 JVM从监视器等待区域中移出等待线程并允许那个线程获得锁且处理监视器关键代码部份
要和监视器/锁起工作 JVM提供了monitorenter和monitorexit 指令幸运地是你不需要在如此低级别地工作取而代的你能够在synchronized声明和同步思路方法中使用Javasynchronized关键字
同步声明
些关键代码部份占了它们封装思路方法小部份为了防止多重线程访问这们关键代码部份你可使用synchronized声明这个声明有如下语法:
'synchronized' '(' objectidentier ')'
'{'
//关键代码部份
'}'
synchronized声明用关键字synchronized开始及用个objectidentier这出现在对圆括弧的间objectidentier 引用个和synchronized 声明描述监视器相关锁对象最后Java声明关键代码部份出现在对花括弧的间你怎样解释synchronized声明呢?看看如下代码片断:
synchronized ("sync object")
{
//访问共享变量及其它共享资源
}
从个源代码观点看个线程企图进入synchronized声明保护关键代码部份在内部 JVM 检查是否些其它线程控制着和"sync object"对象相关锁如果没有其它线程控制着锁 JVM将锁给请求线程并允许那个线程进入花括弧的间关键代码部份然而如果有其它线程控制着锁 JVM会强迫请求线程在个私有等待区域等待直到在关键代码部份内当前线程完成执行最后声明及经过最后花括弧
你能够使用synchronized声明去消除NeedForSynchronizationDemo竞态条件如何消除请看练习列表2:
列表2. SynchronizationDemo1.java
// SynchronizationDemo1.java
SynchronizationDemo1
{
public void (String args)
{
FinTrans ft = FinTrans ;
TransThread tt1 = TransThread (ft, "Deposit Thread");
TransThread tt2 = TransThread (ft, "Withdrawal Thread");
tt1.start ;
tt2.start ;
}
}
FinTrans
{
public String transName;
public double amount;
}
TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称 Save thread's name
this.ft = ft; //保存对金融事务对象引用
}
public void run
{
for ( i = 0; i < 100; i)
{
(getName .equals ("Deposit Thread"))
{
synchronized (ft)
{
ft.transName = "Deposit";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
}
}
}
仔细看看SynchronizationDemo1run思路方法包含两个夹在synchronized (ft) { and }间关键代码部份每个存款和取款线程必须在任线程进入它关键代码部份前获得和ft引用FinTrans对象相关锁假如如果存款线程在它关键代码部份且取款线程想进入它自己关键代码部份取款线程就应努力获得锁当存款线程在它关键代码部份执行时控制着锁 JVM 便强迫取款线程等待直到存款线程执行完关键代码部份并释放锁(当执行离开关键代码部份时锁自动释放)
窍门技巧:当你需要决定是否个线程控制和个给定对象相关锁时Thread静态布尔holdsLock(Object o)思路方法如果线程控制着和对象相关锁思路方法这个思路方法便返回个布尔真值否则返回个假值例如如果你打算将.out.prln (Thread.holdsLock (ft))放置在SynchronizationDemo1思路方法末尾 holdsLock将返回假值返回 假值是执行思路方法主线程没有使用同步机制获得任何锁可是如果你打算将.out.prln (Thread.holdsLock (ft))放在runsynchronized (ft)声明中 holdsLock将返回真值无论是存款线程或是取款线程都不得不在那些线程能够进入它关键代码部份前获得和ft引用FinTrans对象相关锁
Synchronized思路方法
你能够通过你源代码使用synchronized声明然而你也可能陷入过多使用这样声明而导致代码效率低例如假设你包含个带两个连续synchronized声明思路方法每个声明都企图获得同公共对象锁获得和翻译对象锁要消耗时间重复(在个循环中)那个思路方法会降低性能每次对那个思路方法个都必须获得和释放两个锁花费大量时间获得和释放锁要消除这个问题你应考虑使用同步思路方法
个同步思路方法不是个例子就是个其头包含synchronized关键字类思路方法例如: synchronized void pr (String s)当你同步个完整例子思路方法时个线程必须获得和那个思路方法出现对象相关锁例如给个ft.update("Deposit", 2000.0)例子思路方法并且假定update是同步个思路方法必须获得和ft引用对象相关锁要看个SynchronizationDemo1版本同步思路方法源代码请查看列表3:
列表3. SynchronizationDemo2.java
// SynchronizationDemo2.java
SynchronizationDemo2
{
public void (String args)
{
FinTrans ft = FinTrans ;
TransThread tt1 = TransThread (ft, "Deposit Thread");
TransThread tt2 = TransThread (ft, "Withdrawal Thread");
tt1.start ;
tt2.start ;
}
}
FinTrans
{
private String transName;
private double amount;
synchronized void update (String transName, double amount)
{
this.transName = transName;
this.amount = amount;
.out.prln (this.transName + " " + this.amount);
}
}
TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象引用
}
public void run
{
for ( i = 0; i < 100; i)
(getName .equals ("Deposit Thread"))
ft.update ("Deposit", 2000.0);
ft.update ("Withdrawal", 250.0);
}
}
虽然比列表2稍微更简洁表3达到是同目如果存款线程update思路方法 JVM检查看是否取款线程已经获得和ft引用对象相关锁如果是这样存款线程就等待否则那个线程就进入关键代码部份
SynchronizationDemo2示范了个同步例子思路方法然而你也能够同步 思路方法例如 java.util.Calendar类声明了个public synchronized Locale getAvailableLocales 思路方法类思路方法没有个this引用概念那么类思路方法从哪里获得它锁呢?类思路方法从类对象获得它们锁——每个和Class对象相关载入类从那些载入类类思路方法得到它们锁我称这样锁为 locks
些混淆同步例子思路方法和同步类思路方法为帮助你理解在同步类思路方法同步例子思路方法中到底发生了什么应在头脑里保持如下两个观点:
1. 对象锁和类锁互相没有关系它们是区别实体你独立地获得和释放每个锁个同步类思路方法同步例子思路方法获得两个锁首先同步例子思路方法获得它对象对象锁其次那个思路方法获得同步类思路方法类锁
2. 同步类思路方法能够个对象同步思路方法或使用对象去锁住个同步块在那种情形下个线程最初获得同步类思路方法类锁并且接下来获得对象对象锁因此同步例子思路方法个同步类思路方法也获得两个锁
下面代码片断描述了这两个观点:
LockTypes
{
//刚好在执行进入instanceMethod前获得对象锁
synchronized void instanceMethod
{
//当线程离开instanceMethod时释放对象锁
}
//刚好在执行进入Method前获得类锁
synchronized void Method (LockTypes lt)
{
lt.instanceMethod ;
//刚好在关键代码部份执行前获得对象锁
synchronized (lt)
{
//关键代码部份
//当线程离开关键代码部份时释放对象锁
}
//当线程离开Method时释放类锁
}
}
代码段示范了同步例子思路方法instanceMethod同步类思路方法Method通过阅读注解你看到Method首先获得它类锁接下来获得和lt引用LockTypes对象相关对象锁
警告:不要同步个线程对象run思路方法多线程需要执行run那些线程企图对同个对象同步所以在个时间里只有个线程能够执行run结果在每个线程能访问run前必须等待前线程结束
同步机制两个问题
尽管其简单开发者经常滥用Java同步机制会导致由区别步变得死锁这章将检查这些问题并提供对避免它们建议
注意:个和同步机制有关线程问题是和锁获得和释放有关时间成本换句话说个线程将花费时间去获得或释放个锁当在个循环中获得/释放个锁单独时间成本合计起来就会降低性能对于旧JVMs,锁获得时间成本经常导致重大性能损失幸运地是 Sun微系统HotSpot JVM (其装载在J2SE SDK上)提供快速锁获得和释放大大减少了对这些影响
区别步
在个线程自动或不自动(通过个例外)退出个关键代码部份时它释放个锁以便另个线程能够得以进入假设两个线程想进入同个关键代码部份为了阻止两个线程同时进入那个关键代码部份每个线程必须努力获得同个锁如果每个线程企图获得个区别锁并成功了两个线程都进入了关键代码部份则两个线程都不得不等待其它线程释放它锁其它线程获得了个区别锁最终结果是:没有同步示范如列表4:
列表4. NoSynchronizationDemo.java
// NoSynchronizationDemo.java
NoSynchronizationDemo
{
public void (String args)
{
FinTrans ft = FinTrans ;
TransThread tt1 = TransThread (ft, "Deposit Thread");
TransThread tt2 = TransThread (ft, "Withdrawal Thread");
tt1.start ;
tt2.start ;
}
}
FinTrans
{
public String transName;
public double amount;
}
TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象引用
}
public void run
{
for ( i = 0; i < 100; i)
{
(getName .equals ("Deposit Thread"))
{
synchronized (this)
{
ft.transName = "Deposit";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
{
synchronized (this)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
}
}
}
当你运行NoSynchronizationDemo时你将看到类似如下输出:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 250.0
Withdrawal 2000.0
Deposit 2000.0
尽管使用了synchronized声明但没有同步发生为什么?检查synchronized (this)关键字this指向当前对象存款线程企图获得和化分配给tt1TransThread对象引用有关锁 (在思路方法中)类似取款线程企图获得和化分配给tt2TransThread对象引用有关锁我们有两个区别TransThread对象并且每个线程企图在进入它自己关键代码部份前获得和其各自TransThread对象相关锁线程获得区别锁两个线程都能在同时间进入它们自己关键代码部份结果是没有同步
窍门技巧:为了避免个没有同步情形选择个对于所有相关线程都公有对象那样话这些线程竞相获得同个对象锁并且同时间仅有个线程在能够进入相关关键代码部份
死锁
在有些中下面情形可能出现:在线程B能够进入B关键代码部份前线程A获得个线程B需要锁类似在线程A能够进入A关键代码部份前线程B获得个线程A需要锁两个线程都没有拥有它自己需要锁每个线程都必须等待获得它锁此外没有线程能够执行没有线程能够释放其它线程锁并且执行被冻结这种行为叫作死锁(deadlock)其示范列如表5:
列表5. DeadlockDemo.java
// DeadlockDemo.java
DeadlockDemo
{
public void (String args)
{
FinTrans ft = FinTrans ;
TransThread tt1 = TransThread (ft, "Deposit Thread");
TransThread tt2 = TransThread (ft, "Withdrawal Thread");
tt1.start ;
tt2.start ;
}
}
FinTrans
{
public String transName;
public double amount;
}
TransThread extends Thread
{
private FinTrans ft;
private String anotherSharedLock = "";
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象引用
}
public void run
{
for ( i = 0; i < 100; i)
{
(getName .equals ("Deposit Thread"))
{
synchronized (ft)
{
synchronized (anotherSharedLock)
{
ft.transName = "Deposit";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
}
{
synchronized (anotherSharedLock)
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep (() (Math.random * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
.out.prln (ft.transName + " " + ft.amount);
}
}
}
}
}
}
如果你运行DeadlockDemo你将可能看到在应用冻结前仅个单独输出行要解冻DeadlockDemo按Ctrl-C (假如你正在个Windows命令提示符中使用SunSDK1.4)
什么将引起死锁呢?仔细查看源代码存款线程必须在它能够进入其内部关键代码部份前获得两个锁和ft引用FinTrans对象有关外部锁和和anotherSharedLock引用String对象有关内部锁类似取款线程必须在其能够进入它自己内部关键代码部份前获得两个锁和anotherSharedLock引用String对象有关外部锁和和ft引用FinTrans对象有关内部锁假定两个线程执行命令是每个线程获得它外部锁因此存款线程获得它FinTrans锁以及取款线程获得它String锁现在两个线程都执行它们外部锁它们处在它们相应外部关键代码部份两个线程接下来企图获得内部锁因此它们能够进入相应内部关键代码部份
存款线程企图获得和anotherSharedLock引用对象相关锁然而取款线程控制着锁所以存款线程必须等待类似取款线程企图获得和ft引用对象相关锁但是取款线程不能获得那个锁存款线程(它正在等待)控制着它因此取款线程也必须等待两个线程都不能操作两个线程都不能释放它控制着锁两个线程不能释放它控制着锁是每个线程都正在等待每个线程都死锁并且冻结
窍门技巧:为了避免死锁仔细分析你源代码看看当个同步思路方法其它同步思路方法时什么地方可能出现线程互相企图获得彼此锁你必须这样做JVM不能探测并防止死锁
回顾
为了使用线程达到优异性能你将遇到你多线程需要连载访问关键代码部份情形同步可以有效地阻止在奇怪行为中产生不致你能够使用synchronized声明以保护个思路方法部份或同步整个思路方法但应仔细检查你代码以防止可能造成同步失败或死锁故障
0
相关文章读者评论发表评论 |
|