Java线程入门教程 详细

Java线程入门教程 收藏
线程是Java的内嵌特性,线程并不容易掌握,有专门介绍Java线程的书籍,读者可以参考。由此可见Java线程的重要性,本文将详细介绍线程的基本知识。
 

       有的时候你可能想写一个程序,每隔一段时间执行相关的任务,这个时候你可以使用Timer和TimerTask,非常方便。你可以参考http://blog.csdn.net/mingjava/archive/2004/07/04/33749.aspx

 

        在Java中实现一个线程有两种方法,第一是实现Runnable接口实现它的run()方法,第二种是继承Thread类,覆盖它的run()方法。下面是代码示例:
public class DoSomething implements Runnable {
    public void run(){
     // here is where you do something
    }
}
public class DoAnotherThing extends Thread {
    public void run(){
 // here is where you do something
    }
}
这两种方法的区别是,如果你的类已经继承了其它的类,那么你只能选择实现Runnable接口了,因为Java只允许单继承的。

 

        Java中的线程有四种状态分别是:运行、就绪、挂起、结束。如果一个线程结束了也就说明他是一个死线程了。当你调用一个线程实例的start()的方法的时候,这个时候线程进入就绪状态,注意并不是运行状态,当虚拟机开始分配给他CPU的运行时间片的时候线程开始进入运行状态,当线程进入等待状态,例如等待某个事件发生的时候,这时候线程处于挂起状态。

 

       启动一个线程你只需要调用start()方法,针对两种实现线程的方法也有两种启动线程的方法,分别如下:
DoSomething doIt = new DoSomething();
Thread myThread = new Thread( doIt );
myThread.start();

DoAnotherThing doIt = new DoAnotherThing();
doIt.start();
由于安全等因素Thread中的stop()方法已经不推荐使用了,因此如果你想要停止一个线程的时候可以通过设置一个信号量,例如:
public class MyThread implements Runnable {
    private boolean quit = false;

 

    public void run(){
    while( !quit ){
     // do something
    }
    }

 

    public void quit(){
     quit = true;
    }
}
        如果每个线程只做它自己的事情,那么就很简单了,但是有的时候几个线程可能要同时访问一个对象并可能对它进行修改,这个时候你必须使用线程的同步在方法或者代码块使用关键字synchronized,例如:
public class Counter {
    private int counter;

 

    public synchronized int increment(){
 return ++counter;
    }

 

    public synchronized int decrement(){
 if( --counter < 0 ){
     counter = 0;
 }

 

 return counter;
    }
}
每个java对象都可以最为一个监视器,当线程访问它的synchronized方法的时候,他只允许在一个时间只有一个线程对他访问,让其他得线程排队等候。这样就可以避免多线程对共享数据造成破坏。记住synchronized是会耗费系统资源降低程序执行效率的,因此一定要在需要同步的时候才使用,尤其在J2ME的开发中要小心。如果你要是想让线程等待某个事件的发生然后继续执行的话,那么这就涉及到线程的调度了。在java中通过wait(),notify(),notifyAll()来实现,这三个方法是在Object类中定义的,当你想让线程挂起的时候调用obj.wait()方法,在同样的obj上调用notify()则让线程重新开始运行。

 

      最后以SUN提供的Producer/Consumer的例子来结束这篇文章,内容是Producer产生一个数字而Consumer消费这个数字,这个小程序里面基本覆盖了本文所有的知识点。请详细研究一下代码
public class Producer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

 

    public Producer(CubbyHole c, int number) {
        cubbyhole = c;
        this.number = number;
    }

 

    public void run() {
        for (int i = 0; i < 10; i++) {
            cubbyhole.put(i);
            System.out.println("Producer #" + this.number
                               + " put: " + i);
            try {
                sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) { }
        }
    }
}

 

public class CubbyHole {
    private int contents;
    private boolean available = false;

 

    public synchronized int get() {
        while (available == false) {
            try {
                wait();
            } catch (InterruptedException e) { }
        }
        available = false;
        notifyAll();
        return contents;
    }

 

    public synchronized void put(int value) {
        while (available == true) {
            try {
                wait();
            } catch (InterruptedException e) { }
        }
        contents = value;
        available = true;
        notifyAll();
    }
}

 

public class Consumer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

 

    public Consumer(CubbyHole c, int number) {
        cubbyhole = c;
        this.number = number;
    }

 

    public void run() {
        int value = 0;
        for (int i = 0; i < 10; i++) {
            value = cubbyhole.get();
            System.out.println("Consumer #" + this.number
                               + " got: " + value);
        }
    }
}

public class ProducerConsumerTest {
    public static void main(String[] args) {
        CubbyHole c = new CubbyHole();
        Producer p1 = new Producer(c, 1);
        Consumer c1 = new Consumer(c, 1);

        p1.start();
        c1.start();
    }
}
 

SUN说输出的结果应该是如下形式,但是在我的机器上却不是这样的,做了一些改动才正确,有兴趣的朋友可以运行一下看看结果,欢迎和我讨论一下!
Producer #1 put: 0
Consumer #1 got: 0
Producer #1 put: 1
Consumer #1 got: 1
Producer #1 put: 2
Consumer #1 got: 2
Producer #1 put: 3
Consumer #1 got: 3
Producer #1 put: 4
Consumer #1 got: 4
Producer #1 put: 5
Consumer #1 got: 5
Producer #1 put: 6
Consumer #1 got: 6
Producer #1 put: 7
Consumer #1 got: 7
Producer #1 put: 8
Consumer #1 got: 8
Producer #1 put: 9
Consumer #1 got: 9

介绍Java中线程、线程类及Runnable 
 
发布时间:2006.03.10 04:25     来源:CSDN    作者:

 

用Java线程获取优异性能(I)
——介绍线程、线程类及Runnable
Jeff Friesen 著 刘建华 编译 


 


--------------------------------------------------------------------------------

 
 

 

 


摘要

 

 

用户期望程序能展现优异的性能。为了满足这个期望,你的程序常常使用到线程。在这篇文章中我们开始练习使用线程。你将学习到线程、线程类及Runnable。

 

 

 

 

 

用户不喜欢反应迟钝的软件。当用户单击一个鼠标时,他们希望程序立即回应他们的请求,即使程序正处于费时的运行之中,比如为一篇很长的文档重编页码或等待一个网络操作的完成。对用户响应很慢的程序其性能拙劣。为提高程序性能,开发者一般使用线程。

 

 

这篇文章是探索线程的第一部份。虽然你可能认为线程是一种难于掌握的事物,但我打算向你显示线程是易于理解的。在这篇文章中,我将向你介绍线程和线程类,以及讨论Runnable。此外,在后面的文章中,我将探索同步(通过锁),同步的问题(比如死锁),等待/通知机制,时序安排(有优先权和没有优先权),线程中断,计时器,挥发性,线程组和线程本地变量。 

 

 

阅读关于线程设计的整个系列:

 

 

·第1部份:介绍线程和线程类,以及Runnable

 

 

·第2部份:使用同步使线程串行化访问关键代码部份

 

 

注意 

 

 

这篇文章及其应用程序的三个相关线程练习与applets不同。然而,我在应用程序中介绍的多数应用到applets。主要不同的是:为了安全的原因,不是所有的线程操作都可以放到一个applet中(我将在以后的文章中讨论applets)。

 

 

什么是线程?

 

 

线程的概念并不难于掌握:它是程序代码的一个独立的执行通道。当多个线程执行时,经由相同代码的一个线程的通道通常与其它的不同。例如,假设一个线程执行一段相当于一个if-else语句的if部分的字节代码时,而另一个线程正执行相当于else部分的字节代码。JVM怎样保持对于每一个线程执行的跟踪呢?JVM给每一个线程它自己的方法调用堆栈。另外跟踪当前指令字节代码,方法堆栈跟踪本地变量,JVM传递给一个方法的参数,以及方法的返回值。 

 

 

当多个线程在同一个程序中执行字节代码序列时,这种行为叫作多线程。多线程在多方面有利于程序: 

 

 

·当执行其它任务时多线程GUI(图形用户界面)程序仍能保持对用户的响应,比如重编页码或打印一个文档。 

 

 

·带线程的程序一般比它们没有带线程的副本程序完成得快。这尤其表现在线程运行在一个多处理器机器上,在这里每一个线程都有它自己的处理器。

 

 

Java通过java.lang.Thread类完成多线程。每一个线程对象描述一个单独的执行线程。那些运行发生在线程的run()方法中。因为缺省的run()方法什么都不做,你必须创建Thread子类并重载run()以完成有用的工作。练习列表1中领略一个在Thread中的线程及多线程: 

 

 

列表1. ThreadDemo.java 

 

 

// ThreadDemo.java

 

 

class ThreadDemo

 

 

{

 

 

public static void main (String [] args)

 

 

{

 

 

MyThread mt = new MyThread ();

 

 

mt.start ();

 

 

for (int i = 0; i < 50; i++)

 

 

System.out.println ("i = " + i + ", i * i = " + i * i);

 

 

}

 

 

}

 

 

class MyThread extends Thread

 

 

{

 

 

public void run ()

 

 

{

 

 

for (int count = 1, row = 1; row < 20; row++, count++)

 

 

{

 

 

for (int i = 0; i < count; i++)

 

 

System.out.print ('*');

 

 

System.out.print ('/n');

 

 

}

 

 

}

 

 

}

 

 

列表1显示了一个由类ThreadDemo和MyThread组成的应用程序的源代码。类ThreadDemo通过创建一个MyThread对象驱动应用程序,开始一个与其对象相关的线程并执行一段打印一个正方形表的代码。相反, MyThread重载Thread的run()方法打印(通过标准输入流)一个由星形符号组成的直角三角形。 

 

 

当你键入java ThreadDemo运行应用程序时, JVM创建一个运行main()方法的开始线程。通过执行mt.start (),开始线程告诉JVM创建一个执行包含MyThread对象的run()方法的字节代码指令的第二个线程。当start()方法返回时,开始线程循环执行打印一个正方形表,此时另一个新线程执行run()方法打印直角三角形。 

 

 

输出会象什么样呢?运行ThreadDemo就可以看到。你将注意到每一个线程的输出与其它线程的输出相互交替。这样的结果是因为两个线程将它们的输出都发送到了同样的标准输出流。 

 

 

注意

 

 

多数(不是所有)JVM设备使用下层平台的线程性能。因为那些性能是平台特有的,你的多线程程序的输出顺序可能与一些人的其他输出的顺序不一样。这种不同是由于时序的安排,我将在这一系列的稍后探讨这一话题。

 

 

线程类

 

 

要精通写多线程代码,你必须首先理解创建Thread类的多种方法。这部份将探讨这些方法。明确地说,你将学到开始线程的方法,命名线程,使线程休眠,决定一个线程是否激活,将一个线程与另一个线程相联,和在当前线程的线程组及子组中列举所有激活的线程。我也会讨论线程调试辅助程序及用户线程与监督线程的对比。 

 

 

我将在以后的文章中介绍线程方法的余下部份,Sun不赞成的方法除外。 

 

 

警告

 

 

Sun有一些不赞成的线程方法种类,比如suspend()和resume(),因为它们能锁住你的程序或破坏对象。所以,你不必在你的代码中调用它们。考虑到针对这些方法工作区的SDK文件,在这篇文章中我没有包含这些方法。 

 

 

构造线程

 

 

Thread有八个构造器。最简单的是:

 

 

·Thread(),用缺省名称创建一个Thread对象 

 

 

·Thread(String name),用指定的name参数的名称创建一个Thread对象

 

 

下一个最简单的构造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable参数之外,这些构造器与前述的构造器一样。不同的是:Runnable参数识别提供run()方法的线程之外的对象。(你将在这篇文章稍后学到Runnable。)最后几个构造器是Thread(String name),Thread(Runnable target),和Thread(Runnable target, String name)。然而,最后的构造器包含了一个为了组织意图的ThreadGroup参数。 

 

 

最后四个构造器之一,Thread(ThreadGroup group, Runnable target, String name, long stackSize),令人感兴趣的是它能够让你指定想要的线程方法调用堆栈的大小。能够指定大小将证明在使用递归方法(一种为何一个方法不断重复调用自身的技术)优美地解决一些问题的程序中是十分有帮助的。通过明确地设置堆栈大小,你有时能够预防StackOverflowErrors。然而,太大将导致OutOfMemoryErrors。同样,Sun将方法调用堆栈的大小看作平台依赖。依赖平台,方法调用堆栈的大小可能改变。因此,在写调用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代码前仔细考虑你的程序分枝。 

 

 

开始你的运载工具

 

 

线程类似于运载工具:它们将程序从开始移动到结束。Thread 和Thread子类对象不是线程。它们描述一个线程的属性,比如名称和包含线程执行的代码(经由一个run()方法)。当一个新线程执行run()时,另一个线程正调用Thread或其子类对象的start()方法。例如,要开始第二个线程,应用程序的开始线程—它执行main()—调用start()。作为响应,JVM和平台一起工作的线程操作代码确保线程正确地初始化并调用Thread或其子类对象的run()方法。 

 

 

一旦start()完成,多重线程便运行。因为我们趋向于在一种线性的方式中思维,我们常发现当两个或更多线程正运行时理解并发(同时)行为是困难的。因此,你应该看看显示与时间对比一个线程正在哪里执行(它的位置)的图表。下图就是这样一个图表。 


 

 
 

 

 

 

与时间对比一个开始线程和一个新建线程执行位置的行为
 

 

 

图表显示了几个重要的时间段: 

 

 

·开始线程的初始化 

 

 

·线程开始执行main()瞬间 

 

 

·线程开始执行start()的瞬间 

 

 

·start()创建一个新线程并返回main()的瞬间 

 

 

·新线程的初始化 

 

 

·新线程开始执行run()的瞬间 

 

 

·每个线程结束的不同瞬间

 

 

注意新线程的初始化,它对run()的执行,和它的结束都与开始线程的执行同时发生。 

 

 

警告

 

 

一个线程调用start()后,在run()方法退出前并发调用那方法将导致start()掷出一个java.lang.IllegalThreadStateException对象。

 


 

 


 

 

用Java线程获取优异性能(II)
——使用同步连载线程访问关键代码部份
Jeff Friesen 著 刘建华 编译 
2002-7-12 14:57:56

--------------------------------------------------------------------------------
 

摘要
开发者有时创建的多线程程序会生成错误值或产生其它奇怪的行为。古怪行为一般出现在一个多线程程序没使用同步连载线程访问关键代码部份的时候。同步连载线程访问关键代码部份是什么意思呢?在这篇文章中解释了同步,Java的同步机制,以及当开发者没有正确使用这个机制时出现的两个问题。一旦你看完这篇文章,你就可以避免在你的多线程Java程序中因缺乏同步而产生的奇怪行为。 
创建多线程Java程序难吗?仅从《用Java线程获取优异性能(I)》中获得的信息你就可以回答,不。毕竟,我已经向你显示了如何轻松地创建线程对象,通过调用Thread的start()方法起动与这些对象相关的线程,以及通过调用其它Thread方法,比如三个重载的join()方法执行简单的线程操作。至今仍有许多开发者在开发一些多线程程序时面临困难境遇。他们的程序经常功能不稳定或产生错误值。例如,一个多线程程序可能将不正确的雇员资料存贮在数据库中,比如姓名和地址。姓名可能属于一个雇员的,而地址却属于另一个的。是什么引起这种奇怪行为的呢? 是缺乏同步:连载行为,或在同一时间排序,线程访问那些让多重线程操作的类和字段变量实例的代码序列,以及其他共享资源。我称这些代码序列为关键代码部份。 
注意:不象类和实例字段变量,线程不能共享本地变量和参数。原因是:本地变量和参数在一个线程方法中分配——叫堆栈。结果,每一个线程都收到它自己对那些变量的拷贝。相反,线程能够共享类字段和实例字段因为那些变量在一个线程方法(叫堆栈)中没有被分配。取而代之,它们作为类(类字段)或对象(实例字段)的一部份在共享内存堆中被分配。
这篇文章将教你如何使用同步连载线程访问关键代码部份。我用一个说明为什么一些多线程程序必须使用同步的例子作为开始。我接下来就监视器和锁探讨Java的同步机制和synchronized 关键字。我通过研究由这样的错用产生的两个问题判定常常因为不正确的使用同步机制而否认了它的好处。 
阅读关于线程程序的整个系列: 
· 第I部份:介绍线程、线程类及Runnable 
· 第II部份:使用同步连载线程访问关键代码部份
对于同步的需要
为什么我们需要同步呢?一种回答,考虑这个例子:你写一个使用一对线程模拟取款/存款金融事务的Java程序。在那个程序中,一个线程处理存款,同时其它线程正处理取款。每一个线程操作一对共享变量、类及实例字段变量,这些用来标识金融事务的姓名和账号。对于一个正确的金融事务,每一个线程必须在其它线程开始给name和amount赋值前(并且同时打印那些值)给name和amount变量赋值(并打印那些值,模拟存贮事务)。其源代码如下: 
列表1. NeedForSynchronizationDemo.java 
// NeedForSynchronizationDemo.java
class NeedForSynchronizationDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
//存款线程关键代码部份的开始
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
//存款线程关键代码部份的结束
}
else
{
//取款线程关键代码部份的开始
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (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和Z。因为X包含一个B分配的值,然而Y和Z包含A分配的值,这是一个矛盾的结果。 
这个矛盾是怎样在NeedForSynchronizationDemo中产生的呢?假设存款线程执行ft.transName = "Deposit"并且接下来调用Thread.sleep()。在那一点,存款线程交出处理器控制一段时间进行休眠,让取款线程执行。假定存款线程休眠500毫秒(感谢Math.random()从0到999毫秒范围随机选取一个值)。在存款线程休眠期间,取款线程执行ft.transName = "Withdrawal",休眠50毫秒 (取款线程随机选取休眠值),醒后执行ft.amount = 250.0并执行System.out.println (ft.transName + " " + ft.amount)—所有都在存款线程醒来之前。结果,取款线程打印Withdrawal 250.0,那是正确的。当存款线程醒来执行ft.amount = 2000.0,接下来执行System.out.println (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位双精度浮点型变量。一个线程可能在完成第一步后等待其它线程执行所有的两步。接下来,第一个线程可能醒来并完成第二步,产生一个值既不同于第一个线程也不同于第二线程的值的变量。结果,如果至少一个线程能够修改一个长整型变量或一个双精度浮点型变量,那些读取和(或)修改那个变量的所有线程就必须使用同步连载访问。
 


  Java的同步机制
Java提供一个同步机制以阻止多于一个的线程在时间的任意一点在一个或多个关键代码部份执行代码。这种机制将自己建立在监视器和锁的概念基础上。一个监视器被作为包在关键代码部份周围的保护,一个锁被作为监视器用来防止多重线程进入监视器的一个软件实体。其想法是:当一个线程想进入一个监视器监视着的关键代码部份时,那个线程必须获得一个与监视器相关的对象的锁。(每个对象都有它自己的锁)如果一些其它线程保存着这个锁, JVM会强迫请求线程在一个与监视器/锁有关的等待区域等待。当监视器中的线程释放锁时, JVM从监视器的等待区域中移出等待线程并允许那个线程获得锁且处理监视器的关键代码部份。 
要和监视器/锁一起工作, JVM提供了monitorenter和monitorexit 指令。幸运地是,你不需要在如此低级别地工作。取而代之,你能够在synchronized声明和同步方法中使用Java的synchronized关键字。 
同步声明
一些关键代码部份占了它们封装方法的一小部份。为了防止多重线程访问这们的关键代码部份,你可使用synchronized声明。这个声明有如下的语法: 
'synchronized' '(' objectidentifier ')'
'{'
//关键代码部份
'}'
synchronized声明用关键字synchronized开始及用一个objectidentifier,这出现在一对圆括弧之间。objectidentifier 引用一个与synchronized 声明描述的监视器相关的锁对象。最后,Java声明的关键代码部份出现在一对花括弧之间。你怎样解释synchronized声明呢?看看如下代码片断: 
synchronized ("sync object")
{
//访问共享变量及其它共享资源
}
从一个源代码观点看,一个线程企图进入synchronized声明保护的关键代码部份。在内部, JVM 检查是否一些其它线程控制着与"sync object"对象相关的锁。如果没有其它线程控制着锁, JVM将锁给请求线程并允许那个线程进入花括弧之间的关键代码部份。然而,如果有其它线程控制着锁, JVM会强迫请求线程在一个私有等待区域等待直到在关键代码部份内的当前线程完成执行最后声明及经过最后的花括弧。 
你能够使用synchronized声明去消除NeedForSynchronizationDemo的竞态条件。如何消除,请看练习列表2: 
列表2. SynchronizationDemo1.java 
// SynchronizationDemo1.java
class SynchronizationDemo1
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程的名称 Save thread's name
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
仔细看看SynchronizationDemo1,run()方法包含两个夹在synchronized (ft) { and }间的关键代码部份。每个存款和取款线程必须在任一线程进入它的关键代码部份前获得与ft引用的FinTrans对象相关的锁。假如如果存款线程在它的关键代码部份且取款线程想进入它自己的关键代码部份,取款线程就应努力获得锁。因为当存款线程在它的关键代码部份执行时控制着锁, JVM 便强迫取款线程等待直到存款线程执行完关键代码部份并释放锁。(当执行离开关键代码部份时,锁自动释放) 
技巧:当你需要决定是否一个线程控制与一个给定对象相关的锁时,调用Thread的静态布尔holdsLock(Object o)方法。如果线程调用控制着与对象相关的锁的方法,这个方法便返回一个布尔真值。否则,返回一个假值。例如,如果你打算将System.out.println (Thread.holdsLock (ft))放置在SynchronizationDemo1的main()方法末尾, holdsLock()将返回假值。返回 假值是因为执行main()方法的主线程没有使用同步机制获得任何锁。可是,如果你打算将System.out.println (Thread.holdsLock (ft))放在run()的synchronized (ft)声明中, holdsLock()将返回真值因为无论是存款线程或是取款线程都不得不在那些线程能够进入它的关键代码部份前获得与ft引用的FinTrans对象相关的锁。
Synchronized方法
你能够通过你的程序的源代码使用synchronized声明。然而,你也可能陷入过多使用这样的声明而导致代码效率低。例如,假设你的程序包含一个带两个连续synchronized声明的方法,每一个声明都企图获得同一公共对象的锁。因为获得和翻译对象的锁要消耗时间,重复调用(在一个循环中)那个方法会降低程序的性能。每次对那个方法的一个调用都必须获得和释放两个锁。程序花费大量的时间获得和释放锁。要消除这个问题,你应考虑使用同步方法。 
一个同步方法不是一个实例就是一个其头包含synchronized关键字的类方法。例如: synchronized void print (String s)。当你同步一个完整实例方法时,一个线程必须获得与那个方法调用出现的对象相关的锁。例如,给一个ft.update("Deposit", 2000.0)实例方法调用,并且假定update()是同步的,一个方法必须获得与ft引用的对象相关的锁。要看一个SynchronizationDemo1版本的同步方法的源代码,请查看列表3: 
列表3. SynchronizationDemo2.java 
// SynchronizationDemo2.java
class SynchronizationDemo2
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
private String transName;
private double amount;
synchronized void update (String transName, double amount)
{
this.transName = transName;
this.amount = amount;
System.out.println (this.transName + " " + this.amount);
}
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
if (getName ().equals ("Deposit Thread"))
ft.update ("Deposit", 2000.0);
else
ft.update ("Withdrawal", 250.0);
}
}
虽然比列表2稍微更简洁,表3达到的是同一目的。如果存款线程调用update()方法, JVM检查看是否取款线程已经获得与ft引用的对象相关的锁。如果是这样,存款线程就等待。否则,那个线程就进入关键代码部份。 
SynchronizationDemo2示范了一个同步实例方法。然而,你也能够同步class 方法。例如, java.util.Calendar类声明了一个public static synchronized Locale [] getAvailableLocales() 方法。因为类方法没有一个this引用的概念,那么类方法从哪里获得它的锁呢?类方法从类对象获得它们的锁——每一个与Class对象相关的载入的类,从那些载入的类的类方法得到它们的锁。我称这样的锁为class locks。 
一些程序混淆同步实例方法和同步类方法。为帮助你理解在同步类方法调用同步实例方法的程序中到底发生了什么,应在头脑里保持如下两个观点: 
1. 对象锁和类锁互相没有关系。它们是不同的实体。你独立地获得和释放每一个锁。一个调用同步类方法的同步实例方法获得两个锁。首先,同步实例方法获得它的对象的对象锁。其次,那个方法获得同步类方法的类锁。 
2. 同步类方法能够调用一个对象的同步方法或使用对象去锁住一个同步块。在那种情形下,一个线程最初获得同步类方法的类锁并且接下来获得对象的对象锁。因此,调用同步实例方法的一个同步类方法也获得两个锁。
下面的代码片断描述了这两个观点: 
class LockTypes
{
//刚好在执行进入instanceMethod()前获得对象锁
synchronized void instanceMethod ()
{
//当线程离开instanceMethod()时释放对象锁
}
//刚好在执行进入classMethod()前获得类锁
synchronized static void classMethod (LockTypes lt)
{
lt.instanceMethod ();
//刚好在关键代码部份执行前获得对象锁
synchronized (lt)
{
//关键代码部份
//当线程离开关键代码部份时释放对象锁
}
//当线程离开classMethod()时释放类锁 
}

代码段示范了调用同步实例方法instanceMethod()的同步类方法classMethod()。通过阅读注解,你看到classMethod()首先获得它的类锁接下来获得与lt引用的LockTypes对象相关的对象锁。
警告:不要同步一个线程对象的run()方法因为多线程需要执行run()。因为那些线程企图对同一个对象同步,所以在一个时间里只有一个线程能够执行run()。结果,在每一个线程能访问run()前必须等待前一线程结束。
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (this)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
当你运行NoSynchronizationDemo时,你将看到类似如下的输出: 
Withdrawal 250.0
Withdrawal 2000.0
Deposit 250.0
Withdrawal 2000.0
Deposit 2000.0
尽管使用了synchronized声明,但没有同步发生。为什么?检查synchronized (this)。因为关键字this指向当前对象,存款线程企图获得与初始化分配给tt1的TransThread对象引用有关的锁。 (在main()方法中)。类似的,取款线程企图获得与初始化分配给tt2的TransThread对象引用有关的锁。我们有两个不同的TransThread对象,并且每一个线程企图在进入它自己关键代码部份前获得与其各自TransThread对象相关的锁。因为线程获得不同的锁,两个线程都能在同一时间进入它们自己的关键代码部份。结果是没有同步。
技巧:为了避免一个没有同步的情形,选择一个对于所有相关线程都公有的对象。那样的话,这些线程竞相获得同一个对象的锁,并且同一时间仅有一个线程在能够进入相关的关键代码部份。
死锁
在有些程序中,下面的情形可能出现:在线程B能够进入B的关键代码部份前线程A获得一个线程B需要的锁。类似的,在线程A能够进入A的关键代码部份前线程B获得一个线程A需要的锁。因为两个线程都没有拥有它自己需要的锁,每个线程都必须等待获得它的锁。此外,因为没有线程能够执行,没有线程能够释放其它线程的锁,并且程序执行被冻结。这种行为叫作死锁(deadlock)。其示范列如表5: 
列表5. DeadlockDemo.java 
// DeadlockDemo.java
class DeadlockDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
private static String anotherSharedLock = "";
TransThread (FinTrans ft, String name)
{
super (name); //保存线程的名称
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
synchronized (anotherSharedLock)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
else
{
synchronized (anotherSharedLock)
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
}
如果你运行DeadlockDemo,你将可能看到在应用程序冻结前仅一个单独输出行。要解冻DeadlockDemo,按Ctrl-C (假如你正在一个Windows命令提示符中使用Sun的SDK1.4)。 
什么将引起死锁呢?仔细查看源代码。存款线程必须在它能够进入其内部关键代码部份前获得两个锁。与ft引用的FinTrans对象有关的外部锁和与anotherSharedLock引用的String对象有关的内部锁。类似的,取款线程必须在其能够进入它自己的内部关键代码部份前获得两个锁。与anotherSharedLock引用的String对象有关的外部锁和与ft引用的FinTrans对象有关的内部锁。假定两个线程的执行命令是每个线程获得它的外部锁。因此,存款线程获得它的FinTrans锁,以及取款线程获得它的String锁。现在两个线程都执行它们的外部锁,它们处在它们相应的外部关键代码部份。两个线程接下来企图获得内部锁,因此它们能够进入相应的内部关键代码部份。 
存款线程企图获得与anotherSharedLock引用对象相关的锁。然而,因为取款线程控制着锁所以存款线程必须等待。类似的,取款线程企图获得与ft引用对象相关的锁。但是取款线程不能获得那个锁因为存款线程(它正在等待)控制着它。因此,取款线程也必须等待。两个线程都不能操作因为两个线程都不能释放它控制着的锁。两个线程不能释放它控制着的锁是因为每个线程都正在等待。每个线程都死锁,并且程序冻结。
技巧:为了避免死锁,仔细分析你的源代码看看当一个同步方法调用其它同步方法时什么地方可能出现线程互相企图获得彼此的锁。你必须这样做因为JVM不能探测并防止死锁。
回顾
为了使用线程达到优异性能,你将遇到你的多线程程序需要连载访问关键代码部份的情形。同步可以有效地阻止在奇怪程序行为中产生的不一致。你能够使用synchronized声明以保护一个方法的部份,或同步整个方法。但应仔细检查你的代码以防止可能造成同步失败或死锁的故障。


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/freedom119/archive/2008/01/13/2041926.aspx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值