2012年08月2日--Java多线程管理

Apache Log4j 1.2.17日志框架详解与实战配置 Apache Log4j 是 Java 平台上最早且最广泛使用的日志框架之一,由 Ceki Gülcü 于 1999 年发起,后成为 Apache 软件基金会的顶级项目。它为开发者提供了灵活、高效、可扩展的日志记录机制,广泛应用于企业级 Java 应用中。Log4j 的核心架构由三个关键组件构成:Logger:负责捕获日志事件,按日志级别(如 DEBUG、INFO、ERROR)进行过滤。Appender:决定日志输出的目的地,如控制台、文件、数据库等。Layout。 阅读详情

一、线程概述

    线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程。

   进程和线程是现代操作系统中两个必不可少的运行模型。在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程);一个进程中可以有一个或多个线程。进程和进程之间不共享内存,也就是说系统中的进程是在各自独立的内存空间中运行的。而一个进程中的线可以共享系统分派给这个进程的内存空间。

    线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈,是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。

    注意:任何一个线程在建立时都会执行一个函数,这个函数叫做线程执行函数。也可以将这个函数看做线程的入口点(类似于程序中的main函数)。无论使用什么语言或技术来建立线程,都必须执行这个函数(这个函数的表现形式可能不一样,但都会有一个这样的函数)。如在Windows中用于建立线程的API函数CreateThread的第三个参数就是这个执行函数的指针。

    在操作系统将进程分成多个线程后,这些线程可以在操作系统的管理下并发执行,从而大大提高了程序的运行效率。虽然线程的执行从宏观上看是多个线程同时执行,但实际上这只是操作系统的障眼法。由于一块CPU同时只能执行一条指令,因此,在拥有一块CPU的计算机上不可能同时执行两个任务。而操作系统为了能提高程序的运行效率,在一个线程空闲时会撤下这个线程,并且会让其他的线程来执行,这种方式叫做线程调度。我们之所以从表面上看是多个线程同时执行,是因为不同线程之间切换的时间非常短,而且在一般情况下切换非常频繁。假设我们有线程A和B.在运行时,可能是A执行了1毫秒后,切换到B后,B又执行了1毫秒,然后又切换到了A,A又执行1毫秒。由于1毫秒的时间对于普通人来说是很难感知的,因此,从表面看上去就象A和B同时执行一样,但实际上A和B是交替执行的。

    二、线程给我们带来的好处

    如果能合理地使用线程,将会减少开发和维护成本,甚至可以改善复杂应用程序的性能。如在GUI应用程序中,还以通过线程的异步特性来更好地处理事件;在应用服务器程序中可以通过建立多个线程来处理客户端的请求。线程甚至还可以简化虚拟机的实现,如Java虚拟机(JVM)的垃圾回收器(garbage collector)通常运行在一个或多个线程中。因此,使用线程将会从以下五个方面来改善我们的应用程序:

    1. 充分利用CPU资源

    现在世界上大多数计算机只有一块CPU.因此,充分利用CPU资源显得尤为重要。当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状态。这将造成大量的计算资源的浪费。而在程序中使用多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。这样CPU就很难有空闲的时候。因此,CPU资源就得到了充分地利用。

    2. 简化编程模型

    如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。但要完成多项任务,如果还使用单线程的话,那就得在在程序中判断每项任务是否应该执行以及什么时候执行。如显示一个时钟的时、分、秒三个指针。使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。这样有助于开发人员对程序的理解和维护。

    3. 简化异步事件的处理

    当一个服务器应用程序在接收不同的客户端连接时最简单地处理方法就是为每一个客户端连接建立一个线程。然后监听线程仍然负责监听来自客户端的请求。如果这种应用程序采用单线程来处理,当监听线程接收到一个客户端请求后,开始读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,也就是说,这个线程将无法再监听客户端请求了。而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O.但使用异步I/O方式比使用同步I/O更难以控制,也更容易出错。因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。

    4. 使GUI更有效率

    使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。

    在现代的GUI框架(如SWING、AWT和SWT)中都使用了一个单独的事件分派线程(event dispatch thread,EDT)来对GUI事件进行扫描。当我们按下一个按钮时,按钮的单击事件函数会在这个事件分派线程中被调用。由于EDT的任务只是对GUI事件进行扫描,因此,这种方式对事件的反映是非常快的。

    5. 节约成本

    提高程序的执行效率一般有三种方法:

    (1)增加计算机的CPU个数。

    (2)为一个程序启动多个进程

    (3)在程序中使用多进程。

    第一种方法是最容易做到的,但同时也是最昂贵的。这种方法不需要修改程序,从理论上说,任何程序都可以使用这种方法来提高执行效率。第二种方法虽然不用购买新的硬件,但这种方式不容易共享数据,如果这个程序要完成的任务需要必须要共享数据的话,这种方式就不太方便,而且启动多个线程会消耗大量的系统资源。第三种方法恰好弥补了第一种方法的缺点,而又继承了它们的优点。也就是说,既不需要购买CPU,也不会因为启太多的线程而占用大量的系统资源(在默认情况下,一个线程所占的内存空间要远比一个进程所占的内存空间小得多),并且多线程可以模拟多块CPU的运行方式,因此,使用多线程是提高程序执行效率的最廉价的方式。

    三、Java的线程模型

    由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。Java通过Thread类将线程所必须的功能都封装了起来。要想建立一个线程,必须要有一个线程执行函数,这个线程执行函数对应Thread类的run方法。Thread类还有一个start方法,这个方法负责建立线程,相当于调用Windows的建立线程函数CreateThread.当调用start方法后,如果线程建立成功,并自动调用Thread类的run方法。因此,任何继承Thread的Java类都可以通过Thread类的start方法来建立线程。如果想运行自己的线程执行函数,那就要覆盖Thread类的run方法。

    在Java的线程模型中除了Thread类,还有一个标识某个Java类是否可作为线程类的接口Runnable,这个接口只有一个抽象方法run,也就是Java线程模型的线程执行函数。因此,一个线程类的唯一标准就是这个类是否实现了Runnable接口的run方法,也就是说,拥有线程执行函数的类就是线程类。

    从上面可以看出,在Java中建立线程有两种方法,一种是继承Thread类,另一种是实现Runnable接口,并通过Thread和实现Runnable的类来建立线程,其实这两种方法从本质上说是一种方法,即都是通过Thread类来建立线程,并运行run方法的。但它们的大区别是通过继承Thread类来建立线程,虽然在实现起来更容易,但由于Java不支持多继承,因此,这个线程类如果继承了Thread,就不能再继承其他的类了,因此,Java线程模型提供了通过实现Runnable接口的方法来建立线程,这样线程类可以在必要的时候继承和业务有关的类,而不是Thread类。


Java多线程初学者指南(2):用Thread类创建线程

    在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。在使用Runnable接口时需要建立一个Thread实例。因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。Thread类的构造方法被重载了八次,构造方法如下:

public Thread( );
public
 Thread(Runnable target);
public
 Thread(String name);
public
 Thread(Runnable target, String name);
public
 Thread(ThreadGroup group, Runnable target);
public
 Thread(ThreadGroup group, String name);
public
 Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

    Runnable target

    实现了Runnable接口的类的实例。要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。

    String name

    线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名子,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。

    ThreadGroup group

    当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中。关于线程组的细节将在后面的章节详细讨论。

    long stackSize

    线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB.在x86平台下,默认的线程栈大小是12KB.

    一个普通的Java类只要从Thread类继承,就可以成为一个线程类。并可通过Thread类的start方法来执行线程代码。虽然Thread类的子类可以直接实例化,但在子类中必须要覆盖Thread类的run方法才能真正运行线程的代码。下面的代码给出了一个使用Thread类建立线程的例子:

  001  package mythread;
  002  
  003  
public class Thread1 extends
 Thread
  004  {
  005      
public void
 run()
  006      {
  007          System.out.println(
this
.getName());
  008      }
  009      
public static void
 main(String[] args)
  010      {
  011          System.out.println(Thread.currentThread().getName());
  012          Thread1 thread1 = 
new
 Thread1();
  013          Thread1 thread2 = 
new
 Thread1 ();
  014          thread1.start();
  015          thread2.start();
  016      }
  017  }

    上面的代码建立了两个线程:thread1和thread2.上述代码中的005至008行是Thread1类的run方法。当在014和015行调用start方法时,系统会自动调用run方法。在007行使用this.getName()输出了当前线程的名字,由于在建立线程时并未指定线程名,因此,所输出的线程名是系统的默认值,也就是Thread-n的形式。在011行输出了主线程的线程名。

    上面代码的运行结果如下:

main
Thread-0
Thread-1

    从上面的输出结果可以看出,第一行输出的main是主线程的名子。后面的Thread-1和Thread-2分别是thread1和thread2的输出结果。

    注意:任何一个Java程序都必须有一个主线程。一般这个主线程的名子为main.只有在程序中建立另外的线程,才能算是真正的多线程程序。也就是说,多线程程序必须拥有一个以上的线程。

Thread类有一个重载构造方法可以设置线程名。除了使用构造方法在建立线程时设置线程名,还可以使用Thread类的setName方法修改线程名。要想通过Thread类的构造方法来设置线程名,必须在Thread的子类中使用Thread类的public Thread(String name)构造方法,因此,必须在Thread的子类中也添加一个用于传入线程名的构造方法。下面的代码给出了一个设置线程名的例子:

  001  package mythread;
  002  
  003  
public class Thread2 extends Thread
  004  {
  005      
private String who;
  006  
  007      
public void run()
  008      {
  009          System.out.println(who + ":" + 
this.getName());
  010      }
  011      
public Thread2(String who)
  012      {
  013          
super();
  014          
this.who = who;
  015      }
  016      
public Thread2(String who, String name)
  017      {
  018          
super(name);
  019          
this.who = who;
  020      }
  021      
public static void main(String[] args)
  022      {
  023          Thread2 thread1 = 
new Thread2 ("thread1", "MyThread1");
  024          Thread2 thread2 = 
new Thread2 ("thread2");
  025          Thread2 thread3 = 
new Thread2 ("thread3");
  026          thread2.setName("MyThread2");
  027          thread1.start();
  028          thread2.start();
  029          thread3.start();
  030      }
  031

    在类中有两个构造方法:

    第011行:public sample2_2(String who)

    这个构造方法有一个参数:who.这个参数用来标识当前建立的线程。在这个构造方法中仍然调用Thread的默认构造方法public Thread( )。

    第016行:public sample2_2(String who, String name)

    这个构造方法中的who和第一个构造方法的who的含义一样,而name参数就是线程的名名。在这个构造方法中调用了Thread类的public Thread(String name)构造方法,也就是第018行的super(name)。

    在main方法中建立了三个线程:thread1、thread2和thread3.其中thread1通过构造方法来设置线程名,thread2通过setName方法来修改线程名,thread3未设置线程名。

 

    运行结果如下:

thread1:MyThread1
thread2:MyThread2
thread3:Thread-2

    从上面的输出结果可以看出,thread1和thread2的线程名都已经修改了,而thread3的线程名仍然为默认值:Thread-2.thread3的线程名之所以不是Thread-1,而是Thread-2,这是因为在024行建立thread2时已经将Thread-1占用了,因此,在025行建立thread3时就将thread3的线程名设为Thread-2.然后在026行又将thread2的线程名修改为MyThread2.因此就会得到上面的输出结果。

    注意:在调用start方法前后都可以使用setName设置线程名,但在调用start方法后使用setName修改线程名,会产生不确定性,也就是说可能在run方法执行完后才会执行setName.如果在run方法中要使用线程名,就会出现虽然调用了setName方法,但线程名却未修改的现象。

    Thread类的start方法不能多次调用,如不能调用两次thread1.start()方法。否则会抛出一个IllegalThreadStateException异常。

  实现Runnable接口的类必须使用Thread类的实例才能创建线程。通过Runnable接口创建线程分为两步:

    1. 将实现Runnable接口的类实例化。

    2. 建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法。

    最后通过Thread类的start方法建立线程。

    下面的代码演示了如何使用Runnable接口来创建线程:

package mythread;

public class MyRunnable implements Runnable
{
    
public void run()
    {
        System.out.println(Thread.currentThread().getName());
    }
    
public static void main(String[] args)
    {
        MyRunnable t1 = 
new MyRunnable();
        MyRunnable t2 = 
new MyRunnable();
        Thread thread1 = 
new Thread(t1, "MyThread1");
        Thread thread2 = 
new Thread(t2);
        thread2.setName("MyThread2");
        thread1.start();
        thread2.start();
    }
}

    上面代码的运行结果如下:

MyThread1
MyThread2

   与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。下面给出了Thread类中和这四种状态相关的方法。

    // 开始线程
    public void start( );
    public void run( );

    
// 挂起和唤醒线程
    public void resume( );     // 不建议使用
    public void suspend( );    // 不建议使用
    public static void sleep(long millis);
    public static void sleep(long millis, int nanos);

    // 终止线程
    public void stop( );       // 不建议使用
    public void interrupt( );

    // 得到线程状态
    public boolean isAlive( );
    
public boolean isInterrupted( );
    public static boolean interrupted( );

    // join方法
    public void join( ) throws InterruptedException;

    一、创建并运行线程

    线程在建立后并不马上执行run方法中的代码,而是处于等待状态。线程处于等待状态时,可以通过Thread类的方法来设置线程不各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。

    当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。下面的代码演示了线程的创建、运行和停止三个状态之间的切换,并输出了相应的isAlive返回值。

package chapter2;

public class LifeCycle extends Thread
{
    
public void run()
    {
        
int n = 0;
        
while ((++n) < 1000);        
    }
     
    
public static void main(String[] args) throws Exception
    {
        LifeCycle thread1 = 
new LifeCycle();
        System.out.println("isAlive: " + thread1.isAlive());
        thread1.start();
        System.out.println("isAlive: " + thread1.isAlive());
        thread1.join();  
// 等线程thread1结束后再继续执行 
        System.out.println("thread1已经结束!");
        System.out.println("isAlive: " + thread1.isAlive());
    }
}

    要注意一下,在上面的代码中使用了join方法,这个方法的主要功能是保证线程的run方法完成后程序才继续运行,这个方法将在后面的文章中介绍

    上面代码的运行结果:

isAlive: false
isAlive: true
thread1
已经结束!
isAlive: false

  二、挂起和唤醒线程

    一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep.在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。

    虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成一些不可预料的事情发生,因此,这两个方法被标识为deprecated(抗议)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。下面的代码演示了sleep、suspend和resume三个方法的使用。

package chapter2;

public class MyThread extends
 Thread
{
    
class SleepThread extends
 Thread
    {
        
public void
 run()
        {
            
try

            {
                sleep(2000);
            }
            
catch (Exception e)
            {
            }
        }
    }
    
public void
 run()
    {
        
while (true
)
            System.out.println(
new
 java.util.Date().getTime());
    }
    
public static void main(String[] args) throws
 Exception
    {
        MyThread thread = 
new
 MyThread();
        SleepThread sleepThread = thread.
new
 SleepThread();
        sleepThread.start(); 
// 
开始运行线程sleepThread
        sleepThread.join();  // 
使线程sleepThread延迟2秒
        thread.start();
        
boolean flag = false
;
        
while (true
)
        {
            sleep(5000);  
// 
使主线程延迟5秒
            flag = !flag;
            
if
 (flag)
                thread.suspend(); 
            
else

                thread.resume();
        }
    }
}

    从表面上看,使用sleep和suspend所产生的效果类似,但sleep方法并不等同于suspend.它们之间最大的一个区别是可以在一个线程中通过suspend方法来挂起另外一个线程,如上面代码中在主线程中挂起了thread线程。而sleep只对当前正在执行的线程起作用。在上面代码中分别使sleepThread和主线程休眠了2秒和5秒。在使用sleep时要注意,不能在一个线程中来休眠另一个线程。如main方法中使用thread.sleep(2000)方法是无法使thread线程休眠2秒的,而只能使主线程休眠2秒。

    在使用sleep方法时有两点需要注意:

    1. sleep方法有两个重载形式,其中一个重载形式不仅可以设毫秒,而且还可以设纳秒(1,000,000纳秒等于1毫秒)。但大多数操作系统平台上的Java虚拟机都无法精确到纳秒,因此,如果对sleep设置了纳秒,Java虚拟机将取最接近这个值的毫秒。

    2. 在使用sleep方法时必须使用throws或try{……}catch{……}.因为run方法无法使用throws,所以只能使用try{……}catch{……}.当在线程休眠的过程中,使用interrupt方法(这个方法将在2.3.3中讨论)中断线程时sleep会抛出一个InterruptedException异常。sleep方法的定义如下:

public static void sleep(long millis)  throws InterruptedException
public static void sleep(long millis,  int nanos)  throws InterruptedException

  三、终止线程的三种方法

    有三种方法可以使终止线程。

    1.  使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

    2.  使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。

    3.  使用interrupt方法中断线程。

 

    1. 使用退出标志终止线程

    当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。下面给出了一个利用退出标志终止线程的例子。

package chapter2;

public class ThreadFlag extends Thread
{
    
public volatile boolean exit = false;

    
public void run()
    {
        
while (!exit);
    }
    
public static void main(String[] args) throws Exception
    {
        ThreadFlag thread = 
new ThreadFlag();
        thread.start();
        sleep(5000); 
// 主线程延迟5秒
        thread.exit = true;  // 终止线程thread
        thread.join();
        System.out.println("
线程退出!");
    }
}

    在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,

    2. 使用stop方法终止线程

    使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:

thread.stop();

    虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。

    3. 使用interrupt方法终止线程

    使用interrupt方法来终端线程可分为两种情况:

    (1)线程处于阻塞状态,如使用了sleep方法。

    (2)使用while(!isInterrupted()){……}来判断线程是否被中断。

    在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。下面的代码演示了在第一种情况下使用interrupt方法。

package chapter2;

public class ThreadInterrupt extends Thread
{
    
public void run()
    {
        
try
        {
            sleep(50000);  
// 延迟50秒
        }
        
catch (InterruptedException e)
        {
            System.out.println(e.getMessage());
        }
    }
    
public static void main(String[] args) throws Exception
    {
        Thread thread = 
new ThreadInterrupt();
        thread.start();
        System.out.println("
在50秒之内按任意键中断线程!");
        System.in.read();
        thread.interrupt();
        thread.join();
        System.out.println("线程已经退出!");
    }
}

    上面代码的运行结果如下:

    在50秒之内按任意键中断线程!

    sleep interrupted
    线程已经退出!

    在调用interrupt方法后, sleep方法抛出异常,然后输出错误信息:sleep interrupted.

    注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。

  在上面的例子中多次使用到了Thread类的join方法。我想大家可能已经猜出来join方法的功能是什么了。对,join方法的功能就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。下面的代码演示了join的用法。

package mythread;

public class JoinThread extends
 Thread
{
    
public static volatile int
 n = 0;

    
public void
 run()
    {
        
for (int
 i = 0; i < 10; i++, n++)
            
try

            {
                sleep(3);  
// 为了使运行结果更随机,延迟3毫秒
            }
            
catch
 (Exception e)
            {
            }                                      
    }
    
public static void main(String[] args) throws
 Exception
    {
        Thread threads[] = 
new
 Thread[100];
        
for (int i = 0; i < threads.length; i++)  // 
建立100个线程
            threads[i] = new JoinThread();
        
for (int i = 0; i < threads.length; i++)   // 
运行刚才建立的100个线程
            threads[i].start();
        
if
 (args.length > 0)  
            
for (int i = 0; i < threads.length; i++)   // 100
个线程都执行完后继续
                threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

    在例程2-8中建立了100个线程,每个线程使静态变量n增加10.如果在这100个线程都执行完后输出n,这个n值应该是1000.

    1.  测试1

    使用如下的命令运行上面程序:

java mythread.JoinThread

    程序的运行结果如下:

n=442

    这个运行结果可能在不同的运行环境下有一些差异,但一般n不会等于1000.从上面的结果可以肯定,这100个线程并未都执行完就将n输出了。

    2.  测试2

    使用如下的命令运行上面的代码:

    在上面的命令行中有一个参数join,其实在命令行中可以使用任何参数,只要有一个参数就可以,这里使用join,只是为了表明要使用join方法使这100个线程同步执行。

    程序的运行结果如下:

n=1000

    无论在什么样的运行环境下运行上面的命令,都会得到相同的结果:n=1000.这充分说明了这100个线程肯定是都执行完了,因此,n一定会等于1000.

  volatile关键字相信了解Java多线程的读者都很清楚它的作用。volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:

package mythread;

public class JoinThread extends
 Thread
{
    
public staticvolatileint
 n = 0;
   
public void
 run()
    {
        
for (int
 i = 0; i < 10; i++)
            
try

        {
                n = n + 1;
                sleep(3); 
// 为了使运行结果更随机,延迟3毫秒

            }
            
catch (Exception e)
            {
            }
    }

    
public static void main(String[] args) throws
 Exception
    {

        Thread threads[] = 
new
 Thread[100];
        
for (int
 i = 0; i < threads.length; i++)
            
// 
建立100个线程
            threads[i] = new JoinThread();
        
for (int
 i = 0; i < threads.length; i++)
            
// 
运行刚才建立的100个线程
            threads[i].start();
        
for (int
 i = 0; i < threads.length; i++)
            
// 100
个线程都执行完后继续
            threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

    如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:

n = n + 1;
n++;

    如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:

package mythread;

public class JoinThread extends
 Thread
{
    
public staticint
 n = 0;

    
public static synchronized void
 inc()
    {
        n++;
    }
    
public void
 run()
    {
        
for (int
 i = 0; i < 10; i++)
            
try

            {
                inc(); 
// n = n + 1 改成了 inc();
                sleep(3); // 
为了使运行结果更随机,延迟3毫秒

            }
            
catch (Exception e)
            {
            }
    }

    
public static void main(String[] args) throws
 Exception
    {

        Thread threads[] = 
new
 Thread[100];
        
for (int
 i = 0; i < threads.length; i++)
            
// 
建立100个线程
            threads[i] = new JoinThread();
        
for (int
 i = 0; i < threads.length; i++)
            
// 
运行刚才建立的100个线程
            threads[i].start();
        
for (int
 i = 0; i < threads.length; i++)
            
// 100
个线程都执行完后继续
            threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

    上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile.

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。本文就以上原因介绍了几种用于向线程传递数据的方法,在下一篇文章中将介绍从线程中返回数据的方法。

    欲先取之,必先予之。一般在使用线程时都需要有一些初始化数据,然后线程利用这些数据进行加工处理,并返回结果。在这个过程中最先要做的就是向线程中传递数据。

    一、通过构造方法传递数据

    在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:

package mythread;

public class MyThread1 extends
 Thread
{
    
private
 String name;

    
public
 MyThread1(String name)
    {
        
this
.name = name;
    }
    
public void
 run()
    {
        System.out.println("hello " + name);
    }
    
public static void
 main(String[] args)
    {
        Thread thread = 
new
 MyThread1("world");
        thread.start();       
    }
}

    由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

    二、通过变量和方法传递数据

    向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置name变量:

package mythread;

public class MyThread2 implements
 Runnable
{
    
private
 String name;

    
public void
 setName(String name)
    {
        
this
.name = name;
    }
    
public void
 run()
    {
        System.out.println("hello " + name);
    }
    
public static void
 main(String[] args)
    {
        MyThread2 myThread = 
new
 MyThread2();
        myThread.setName("world");
        Thread thread = 
new
 Thread(myThread);
        thread.start();
    }
}

三、通过回调函数传递数据

    上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个value是无法事先就传入线程类的。

package mythread;

class
 Data
{
    
public int
 value = 0;
}
class
 Work
{
    
public void
 process(Data data, Integer  numbers)
    {
        
for (int n : numbers)
        {
            data.value += n;
        }
    }
}
public class MyThread3 extends
 Thread
{
    
private
 Work work;

    
public
 MyThread3(Work work)
    {
        
this
.work = work;
    }
    
public void
 run()
    {
        java.util.Random random = 
new
 java.util.Random();
        Data data = 
new
 Data();
        
int
 n1 = random.nextInt(1000);
        
int
 n2 = random.nextInt(2000);
        
int
 n3 = random.nextInt(3000);
        work.process(data, n1, n2, n3);   
// 
使用回调函数
        System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
                + String.valueOf(n3) + "=" + data.value);
    }
    
public static void
 main(String[] args)
    {
        Thread thread = 
new MyThread3(new
 Work());
        thread.start();
    }
}

    在上面代码中的process方法被称为回调函数。从本质上说,回调函数就是事件函数。在Windows API中常使用回调函数和调用API的程序之间进行数据交互。因此,调用回调函数的过程就是最原始的引发事件的过程。在这个例子中调用了process方法来获得数据也就相当于在run方法中引发了一个事件。

  从线程中返回数据和向线程传递数据类似。也可以通过类成员以及回调函数来返回数据。但类成员在返回数据和传递数据时有一些区别,下面让我们来看看它们区别在哪。

    一、通过类变量和方法返回数据

    使用这种方法返回数据需要在调用start方法后才能通过类变量或方法得到数据。让我们先来看看例程2-13会得到什么结果。

package mythread;

public class MyThread extends
 Thread
{
   
private
String value1;
    
private
 String value2;

    
public void
 run()
    {
        value1 = "
通过成员变量返回数据";
        value2 = "
通过成员方法返回数据";
    }
    
public static void main(String[] args) throws
 Exception
    {
        MyThread thread = 
new
 MyThread();
        thread.start();
        System.out.println("value1:" + thread.value1);
        System.out.println("value2:" + thread.value2);
    }
}

    运行上面的代码有可能输出如下的结果:

value1:null
value2:null

    从上面的运行结果看很不正常。在run方法中已经对value1和value2赋了值,而返回的却是null.发生这种情况的原因是调用start方法后就立刻输出了value1和value2的值,而这里run方法还没有执行到为value1和value2赋值的语句。要避免这种情况的发生,就需要等run方法执行完后才执行输出value1和value2的代码。因此,我们可以想到使用sleep方法将主线程进行延迟,如可以在thread.start()后加一行如下的语句:

sleep(1000);

    这样做可以使主线程延迟1秒后再往下执行,但这样做有一个问题,就是我们怎么知道要延迟多长时间。在这个例子的run方法中只有两条赋值语句,而且只创建了一个线程,因此,延迟1秒已经足够,但如果run方法中的语句很复杂,这个时间就很难预测,因此,这种方法并不稳定。

    我们的目的就是得到value1和value2的值,因此,只要判断value1和value2是否为null.如果它们都不为null时,就可以输出这两个值了。我们可以使用如下的代码来达到这个目的:

while (thread.value1 == null || thread.value2 == null);

    使用上面的语句可以很稳定地避免这种情况发生,但这种方法太耗费系统资源。大家可以设想,如果run方法中的代码很复杂,value1和value2需要很长时间才能被赋值,这样while循环就必须一直执行下去,直到value1和value2都不为空为止。因此,我们可以对上面的语句做如下的改进:

while (thread.value1 == null || thread.value2 == null)
    sleep(100);

    在while循环中第判断一次value1和value2的值后休眠100毫秒,然后再判断这两个值。这样所占用的系统资源会小一些。

    上面的方法虽然可以很好地解决,但Java的线程模型为我们提供了更好的解决方案,这就是join方法。在前面已经讨论过,join的功能就是使用线程从异步执行变成同步执行。当线程变成同步执行后,就和从普通的方法中得到返回数据没有什么区别了。因此,可以使用如下的代码更有效地解决这个问题:


thread.start();
thread.join();

    在thread.join()执行完后,线程thread的run方法已经退出了,也就是说线程thread已经结束了。因此,在thread.join()后面可以放心大胆地使用MyThread类的任何资源来得到返回数据。

    二、通过回调函数返回数据

    其实这种方法已经在《向线程传递数据的三种方法》中介绍了。在《向线程传递数据的三种方法》一文的例子中通过Work类的process方法向线程中传递了计算结果,但同时,也通过process方法从线程中得到了三个随机数。因此,这种方法既可以向线程中传递数据,也可以从线程中获得数据。

Java中的变量分为两类:局部变量和类变量。局部变量是指在方法内定义的变量,如在run方法中定义的变量。对于这些变量来说,并不存在线程之间共享的问题。因此,它们不需要进行数据同步。类变量是在类中定义的变量,作用域是整个类。这类变量可以被多个线程共享。因此,我们需要对这类变量进行数据同步。

    数据同步就是指在同一时间,只能由一个线程来访问被同步的类变量,当前线程访问完这些变量后,其他线程才能继续访问。这里说的访问是指有写操作的访问,如果所有访问类变量的线程都是读操作,一般是不需要数据同步的。

    那么如果不对共享的类变量进行数据同步,会发生什么情况呢?让我们先看看下面的代码会发生什么样的事情:

package test;

public class MyThread extends
 Thread
{
    
public static int
 n = 0;

    
public void
 run()
    {
        
int
 m = n;
        yield();
        m++;
        n = m;
    }
    
public static void main(String[] args) throws
 Exception
    {
        MyThread myThread = 
new
 MyThread ();
        Thread threads[] = 
new
 Thread[100];
        
for (int
 i = 0; i < threads.length; i++)
            threads[i] = 
new
 Thread(myThread);
        
for (int
 i = 0; i < threads.length; i++)
            threads[i].start();
        
for (int
 i = 0; i < threads.length; i++)
            threads[i].join();
        System.out.println("n = " + MyThread.n);
    }
}

    在执行上面代码的可能结果如下:

n = 59

    看到这个结果,可能很多读者会感到奇怪。这个程序明明是启动了100个线程,然后每个线程将静态变量n加1.最后使用join方法使这100个线程都运行完后,再输出这个n值。按正常来讲,结果应该是n = 100.可偏偏结果小于100.

    其实产生这种结果的罪魁祸首就是我们经常提到的“脏数据”。而run方法中的yield()语句就是产生“脏数据”的始作俑者(不加yield语句也可能会产生“脏数据”,但不会这么明显,只有将100改成更大的数,才会经常产生“脏数据”,在本例中调用yield就是为了放大“脏数据”的效果)。yield方法的作用是使线程暂停,也就是使调用yield方法的线程暂时放弃CPU资源,使CPU有机会来执行其他的线程。为了说明这个程序如何产生“脏数据”,我们假设只创建了两个线程:thread1和thread2.由于先调用了thread1的start方法,因此,thread1的run方法一般会先运行。当thread1的run方法运行到第一行(int m = n;)时,将n的值赋给m.当执行到第二行的yield方法后,thread1就会暂时停止执行,而当thread1暂停时,thread2获得了CPU资源后开始运行(之前thread2一直处于就绪状态),当thread2执行到第一行(int m = n;)时,由于thread1在执行到yield时n仍然是0,因此,thread2中的m获得的值也是0.这样就造成了thread1和thread2的m获得的都是0.在它们执行完yield方法后,都是从0开始加1,因此,无论谁先执行完,最后n的值都是1,只是这个n被thread1和thread2各赋了一遍值。这个过程如下图如示:

    也许有人会问,如果只有n++,会产生“脏数据”吗?答案是肯定的。那么n++只是一条语句,又如何在执行过程中将CPU交给其他的线程呢?其实这只是表面现象,n++在被Java编译器编译成中间语言(也叫做字节码)后,并不是一条语言。让我们看看下面的Java代码将会被编译成什么样的Java中间语言。

    Java源代码

public void run()
{
    n++;
}

    被编译后的中间语言代码

  001  public void run()
  002  {
  003      aload_0         
  004      dup             
  005      getfield
  006      iconst_1        
  007      iadd            
  008      putfield       
  009      
return
          
  010  }


    大家可以看到在run方法中只有n++一条语句,而在编译后,却有7条中间语言语句。我们并不需要知道这些语句的功能是什么,只看一下第005、007和008行语句。在005行是getfield,根据它的英文含义可知是要得到某个值,因为这里只有一个n,所以毫无疑问,是要得到n的值。而在007行的iadd也不难猜测是将这个得到的n值加1.在008行的putfield的含义我想大家可能已经猜出来了,它负责将这个加1后的n再更新回类变量n.说到这,可能大家还有一个疑惑,执行n++时直接将n加1不就行了,为什么要如此费周折。其实这里涉及到一个Java内存模型的问题。

    Java的内存模型分为主存储区和工作存储区。主存储区保存了Java中所有的实例。也就是说,在我们使用new来建立一个对象后,这个对象及它内部的方法、变量等都保存在这一区域,在MyThread类中的n就保存在这个区域。主存储区可以被所有线程共享。而工作存储区就是我们前面所讲的线程栈,在这个区域里保存了在run方法以及run方法所调用的方法中定义的变量,也就是方法变量。在线程要修改主存储区中的变量时,并不是直接修改这些变量,而是将它们先复制到当前线程的工作存储区,在修改完后,再将这个变量值覆盖主存储区的相应的变量值。

    在了解了Java的内存模型后,就不难理解为什么n++也不是原子操作了。它必须经过一个拷贝、加1和覆盖的过程。这个过程和在MyThread类中模拟的过程类似。大家可以想象,如果在执行到getfield时,thread1由于某种原因被中断,那么就会发生和MyThread类的执行结果类似的情况。要想彻底解决这个问题,就必须使用某种方法对n进行同步,也就是在同一时间只能有一个线程操作n,这也称为对n的原子操作。

要想解决“脏数据”的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下:

public synchronized void run()
{
    
 
}

    从上面的代码可以看出,只要在void和public之间加上synchronized关键字,就可以使run方法同步,也就是说,对于同一个Java类的对象实例,run方法同时只能被一个线程调用,并当前的run执行完后,才能被其他的线程调用。即使当前线程执行到了run方法中的yield方法,也只是暂停了一下。由于其他线程无法执行run方法,因此,最终还是会由当前的线程来继续执行。先看看下面的代码:

    sychronized关键字只和一个对象实例绑定

  class Test
  {
        
public synchronized void
 method()
       {
           
 
       }
  }
   
  
public class Sync implements Runnable
  {
       
private
 Test test;
       
public void
 run()
       {
            test.method();
       }
      
public
 Sync(Test test)
       {
          
this
.test = test;
       }
      
public static void main(String[] args) throws
 Exception
       {
           Test test1 =  
new
 Test();
           Test test2 =  
new
 Test();
           Sync sync1 = 
new
 Sync(test1);
           Sync sync2 = 
new
 Sync(test2);
          
new
 Thread(sync1).start();
          
new
 Thread(sync2).start(); 
       }
   }

    在Test类中的method方法是同步的。但上面的代码建立了两个Test类的实例,因此,test1和test2的method方法是分别执行的。要想让method同步,必须在建立Sync类的实例时向它的构造方法中传入同一个Test类的实例,如下面的代码所示:

Sync sync1 = new Sync(test1);

    不仅可以使用synchronized来同步非静态方法,也可以使用synchronized来同步静态方法。如可以按如下方式来定义method方法:

class Test 
{
    public static synchronized void
 method() {   }
}

    建立Test类的对象实例如下:

Test test = new Test();

    对于静态方法来说,只要加上了synchronized关键字,这个方法就是同步的,无论是使用test.method(),还是使用Test.method()来调用method方法,method都是同步的,并不存在非静态方法的多个实例的问题。

    在23种设计模式中的单件(Singleton)模式如果按传统的方法设计,也是线程不安全的,下面的代码是一个线程不安全的单件模式。

package test;

// 
线程安全的Singleton模式
class Singleton
{
    
private static
 Singleton sample;

    
private
 Singleton()
    {
    }
    
public static
 Singleton getInstance()
    {
        
if (sample == null
)
        {
            Thread.yield(); 
// 
为了放大Singleton模式的线程不安全性
            sample = new Singleton();
        }
        
return
 sample;
    }
}
public class MyThread extends
 Thread
{
    
public void
 run()
    {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.hashCode());
    }
    
public static void
 main(String[] args)
    {
        Thread threads[] = 
new
 Thread[5];
        
for (int
 i = 0; i < threads.length; i++)
            threads[i] = 
new
 MyThread();
        
for (int
 i = 0; i < threads.length; i++)
            threads[i].start();
    }
}

    在上面的代码调用yield方法是为了使单件模式的线程不安全性表现出来,如果将这行去掉,上面的实现仍然是线程不安全的,只是出现的可能性小得多。

    程序的运行结果如下:

25358555
26399554
7051261
29855319
5383406

    上面的运行结果可能在不同的运行环境上有所有同,但一般这五行输出不会完全相同。从这个输出结果可以看出,通过getInstance方法得到的对象实例是五个,而不是我们期望的一个。这是因为当一个线程执行了Thread.yield()后,就将CPU资源交给了另外一个线程。由于在线程之间切换时并未执行到创建Singleton对象实例的语句,因此,这几个线程都通过了if判断,所以,就会产生了建立五个对象实例的情况(可能创建的是四个或三个对象实例,这取决于有多少个线程在创建Singleton对象之前通过了if判断,每次运行时可能结果会不一样)。

    要想使上面的单件模式变成线程安全的,只要为getInstance加上synchronized关键字即可。代码如下:

public static synchronized Singleton getInstance() {   }

    当然,还有更简单的方法,就是在定义Singleton变量时就建立Singleton对象,代码如下:

private static final Singleton sample = new Singleton();

    然后在getInstance方法中直接将sample返回即可。这种方式虽然简单,但不知在getInstance方法中创建Singleton对象灵活。读者可以根据具体的需求选择使用不同的方法来实现单件模式。

在使用synchronized关键字时有以下四点需要注意:

    1.  synchronized关键字不能继承。

    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:

    在子类方法中加上synchronized关键字

class Parent
{
   
public synchronized void method() {     }
}
class Child extends Parent
{
   
public synchronized void method() {     }
}

    在子类方法中调用父类的同步方法

class Parent
{
    public synchronized void method() {     }
}
class Child extends Parent
{
    
public void method() { super.method();     }
}

    2.  在定义接口方法时不能使用synchronized关键字。

    3.  构造方法不能使用synchronized关键字,但可以使用下节要讨论的synchronized块来进行同步。

    4.  synchronized可以自由放置。

    在前面的例子中使用都是将synchronized关键字放在方法的返回类型前面。但这并不是synchronized可放置唯一位置。在非静态方法中,synchronized还可以放在方法定义的最前面,在静态方法中,synchronized可以放在static的前面,代码如下:

public synchronized void method();
synchronized public void method();
public static synchronized void method();
public synchronized static void method();
synchronized public static void method();

    但要注意,synchronized不能放在方法返回类型的后面,如下面的代码是错误的:

public void synchronized method();
public static void synchronized method();

    synchronized关键字只能用来同步方法,不能用来同步类变量,如下面的代码也是错误的。

public synchronized int n = 0;
public static synchronized int n = 0;

    虽然使用synchronized关键字同步方法是最安全的同步方式,但大量使用synchronized关键字会造成不必要的资源消耗以及性能损失。虽然从表面上看synchronized锁定的是一个方法,但实际上synchronized锁定的是一个类。也就是说,如果在非静态方法method1和method2定义时都使用了synchronized,在method1未执行完之前,method2是不能执行的。静态方法和非静态方法的情况类似。但静态和非静态方法不会互相影响。看看如下的代码:

package test;

public class MyThread1 extends Thread
{
    
public String methodName;

    
public static void method(String s)
    {
        System.out.println(s);
        
while (true)
            ;
    }
    
public synchronized void method1()
    {
        method("
非静态的method1方法");
    }
    public synchronized void method2()
    {
        method("
非静态的method2方法");
    }
    public static synchronized void method3()
    {
        method("
静态的method3方法");
    }
    public static synchronized void method4()
    {
        method("
静态的method4方法");
    }
    public void run()
    {
        
try
        {
            getClass().getMethod(methodName).invoke(
this);
        }
        
catch (Exception e)
        {
        }
    }
    
public static void main(String[] args) throws Exception
    {
        MyThread1 myThread1 = 
new MyThread1();
        
for (int i = 1; i <= 4; i++)
        {
            myThread1.methodName = "method" + String.valueOf(i);
            
new Thread(myThread1).start();
            sleep(100);
        }
    }
}

    运行结果如下:

非静态的method1方法
静态的method3方法

    从上面的运行结果可以看出,method2和method4在method1和method3未结束之前不能运行。因此,我们可以得出一个结论,如果在类中使用synchronized关键字来定义非静态方法,那将影响这个中的所有使用synchronized关键字定义的非静态方法。如果定义的是静态方法,那么将影响类中所有使用synchronized关键字定义的静态方法。这有点象数据表中的表锁,当修改一条记录时,系统就将整个表都锁住了,因此,大量使用这种同步方式会使程序的性能大幅度下降。

synchronized关键字有两种用法。第一种就是在《使用Synchronized关键字同步类方法》一文中所介绍的直接用在方法的定义中。另外一种就是synchronized块。我们不仅可以通过synchronized块来同步一个对象变量。也可以使用synchronized块来同步类中的静态方法和非静态方法。

    synchronized块的语法如下:

public void method()
{
    … …
    
synchronized(表达式)
    {
        … …
    }
}

    一、非静态类方法的同步

    从《使用Synchronized关键字同步类方法》一文中我们知道使用synchronized关键字来定义方法就会锁定类中所有使用synchronzied关键字定义的静态方法或非静态方法,但这并不好理解。而如果使用synchronized块来达到同样的效果,就不难理解为什么会产生这种效果了。如果想使用synchronized块来锁定类中所有的同步非静态方法,需要使用this做为synchronized块的参数传入synchronized块国,代码如下:

    通过synchronized块同步非静态方法

  001  public class SyncBlock
  002  {
  003      
public void method1()
  004      {
  005          
synchronized(this)  // 相当于对method1方法使用synchronized关键字
  006          {
  007              … …
  008          }
  009      }
  010      
public void method2()
  011      {
  012          
synchronized(this)  // 相当于对method2方法使用synchronized关键字
  013          {
  014              … …
  015          }
  016      }
  017      
public synchronized void method3()  
  018      {
  019          … …
  020      }
  021  }

    在上面的代码中的method1和method2方法中使用了synchronized块。而第017行的method3方法仍然使用synchronized关键字来定义方法。在使用同一个SyncBlock类实例时,这三个方法只要有一个正在执行,其他两个方法就会因未获得同步锁而被阻塞。在使用synchronized块时要想达到和synchronized关键字同样的效果,必须将所有的代码都写在synchronized块中,否则,将无法使当前方法中的所有代码和其他的方法同步。

    除了使用this做为synchronized块的参数外,还可以使用SyncBlock.this作为synchronized块的参数来达到同样的效果。

    在内类(InnerClass)的方法中使用synchronized块来时,this只表示内类,和外类(OuterClass)没有关系。但内类的非静态方法可以和外类的非静态方法同步。如在内类InnerClass中加一个method4方法,并使method4方法和SyncBlock的三个方法同步,代码如下:

    使内类的非静态方法和外类的非静态方法同步

public class SyncBlock
{
    … …
    class InnerClass
    {
       
public void method4()
        {
           
synchronized(SyncBlock.this)
            {
                … … 
            }
        }
    }
    … …
}

    在上面SyncBlock类的新版本中,InnerClass类的method4方法和SyncBlock类的其他三个方法同步,因此,method1、method2、method3和method4四个方法在同一时间只能有一个方法执行。

    Synchronized块不管是正常执行完,还是因为程序出错而异常退出synchronized块,当前的synchronized块所持有的同步锁都会自动释放。因此,在使用synchronized块时不必担心同步锁的释放问题。

二、静态类方法的同步

    由于在调用静态方法时,对象实例不一定被创建。因此,就不能使用this来同步静态方法,而必须使用Class对象来同步静态方法。代码如下:

    通过synchronized块同步静态方法

   public class StaticSyncBlock
   {
       
public static void method1()
       {
           
synchronized(StaticSyncBlock.class)  
           {
               … …
           }
       }
       
public static synchronized void method2()  
       {
           … …
       }
   }

    在同步静态方法时可以使用类的静态字段class来得到Class对象。在上例中method1和method2方法同时只能有一个方法执行。除了使用class字段得到Class对象外,还可以使用实例的getClass方法来得到Class对象。上例中的代码可以修改如下:

    使用getClass方法得到Class对象

public class StaticSyncBlock
{
    public static StaticSyncBlock instance; 
    public StaticSyncBlock()
    {
        instance = 
this;
    }
    public static void method1()
    {
       
synchronized(instance.getClass())
       {
             
       }
    }
     
}

    在上面代码中通过一个public的静态instance得到一个StaticSyncBlock类的实例,并通过这个实例的getClass方法得到了Class对象(一个类的所有实例通过getClass方法得到的都是同一个Class对象,因此,调用任何一个实例的getClass方法都可以)。我们还可以通过Class对象使不同类的静态方法同步,如Test类的静态方法method和StaticSyncBlock类的两个静态方法同步,代码如下:

    Test类的method方法和StaticSyncBlock类的method1、method2方法同步

   public class Test
   {
       
public static void method()
       {
           
synchronized(StaticSyncBlock.class)
           {
                 
           }
       }
   }

    注意:在使用synchronized块同步类方法时,非静态方法可以使用this来同步,而静态方法必须使用Class对象来同步。它们互不影响。当然,也可以在非静态方法中使用Class对象来同步静态方法。但在静态方法中不能使用this来同步非静态方法。这一点在使用synchronized块同步类方法时应注意。

我们可以通过synchronized块来同步特定的静态或非静态方法。要想实现这种需求必须为这些特性的方法定义一个类变量,然后将这些方法的代码用synchronized块括起来,并将这个类变量作为参数传入synchronized块。下面的代码演示了如何同步特定的类方法:

  001  package mythread;
  002  
  003  
public class SyncThread extends
 Thread
  004  {
  005      
private static
 String sync = "";
  006      
private
 String methodType = "";
  007  
  008      
private static void
 method(String s)
  009      {
  010          
synchronized
 (sync)
  011          {
  012              sync = s;
  013              System.out.println(s);
  014              
while (true
);
  015          }
  016      }
  017      
public void
 method1()
  018      {
  019          method("method1");
  020      }
  021      
public static void
 staticMethod1()
  022      {
  023          method("staticMethod1");
  024      }
  025      
public void
 run()
  026      {
  027          
if
 (methodType.equals("static"))
  028              staticMethod1();
  029          
else if
 (methodType.equals("nonstatic"))
  030              method1();
  031      }
  032      
public
SyncThread(String methodType)
  033      {
  034          
this
.methodType = methodType;
  035      }
  036      
public static void main(String[] args) throws
 Exception
  037      {
  038          SyncThread sample1 = 
new
SyncThread("nonstatic");
  039          SyncThread sample2 = 
new
SyncThread("static");
  040          sample1.start();
  041          sample2.start();
  042      }
  043  }

    运行结果如下:

method1
staticMethod1

    看到上面的运行结果很多读者可能感到惊奇。在上面的代码中method1和staticMethod1方法使用了静态字符串变量sync进行同步。这两个方法只能有一个同时执行,而这两个方法都会执行014行的无限循环语句。因此,输出结果只能是method1和staticMethod1其中之一。但这个程序将这两个字符串都输出了。

    出现这种结果的愿意很简单,我们看一下012行就知道了。原来在这一行将sync的值改变了。在这里要说一下Java中的String类型。String类型和Java中其他的复杂类型不同。在使用String型变量时,只要给这个变量赋一次值,Java就会创建个新的String类型的实例。如下面的代码所示:

String s = "hello";
System.out.println(s.hashCode());
s = "world";
System.out.println(s.hashCode()); 

    在上面的代码中。第一个s和再次赋值后的s的hashCode的值是不一样的。由于创建String类的实例并不需要使用new,因此,在同步String类型的变量时要注意不要给这个变量赋值,否则会使变量无法同步。

    由于在013行已经为sync创建了一个新的实例,假设method1先执行,当method1方法执行了013行的代码后,sync的值就已经不是最初那个值了,而method1方法锁定的仍然是sync变量最初的那个值。而在这时,staticMethod1正好执行到synchronized(sync),在staticMethod1方法中要锁定的这个sync和method1方法锁定的sync已经不是一个了,因此,这两个方法的同步性已经被破坏了。

    解决以上问题的方法当然是将013行去掉。在本例中加上这行,只是为了说明使用类变量来同步方法时如果在synchronized块中将同步变量的值改变,就会破坏方法之间的同步。为了彻底避免这种情况发生,在定义同步变量时可以使用final关键字。如将上面的程序中的005行可改成如下形式:

private final static String sync = "";

    使用final关键字后,sync只能在定义时为其赋值,并且以后不能再修改。如果在程序的其他地方给sync赋了值,程序就无法编译通过。在Eclipse等开发工具中,会直接在错误的地方给出提示。

    我们可以从两个角度来理解synchronized块。如果从类方法的角度来理解,可以通过类变量来同步相应的方法。如果从类变量的角度来理解,可以使用synchronized块来保证某个类变量同时只能被一个方法访问。不管从哪个角度来理解,它们的实质都是一样的,就是利用类变量来获得同步锁,通过同步锁的互斥性来实现同步。

    注意:在使用synchronized块时应注意,synchronized块只能使用对象作为它的参数。如果是简单类型的变量(如int、char、boolean等),不能使用synchronized来同步。


运放+三极管恒流源电路设计实战:从原理到仿真一步到位(附Multisim文件) 本文详细解析了运放与三极管构建恒流源电路的核心原理,重点阐述了负反馈机制如何实现精准的电流控制。通过Multisim仿真实战,从电路搭建、参数计算到负载调整率与动态响应分析,一步步指导设计过程,并提供常见故障的调试技巧与PCB设计要点,帮助工程师从理论平稳过渡到可靠实践。 阅读详情

相关推荐

ShardingSphere-JDBC-若依框架集成(SpringBoot)

ShardingSphere基础知识、ShardingSphere-JDBC如何集成进若依框架中 使用的是若依框架前后端版本、动态数据源,可自行切换,默认数据源为达梦8代理端,

编程小菜鸟的笔记 8168

Java多线程管理

文章目录1,线程状态2,线程状态切换经典生产者消费者问题:3,多线程死锁4,守护(后台)线程 1,线程状态 NEW 刚创建(new) RUNNABLE 就绪态(start):调用一个线程的start方法,那么这个线程就进入了就绪态 RUNNING 运行中(run):等待的线程被CPU调度后就进入Running状态 BLOCK 阻塞(sleep):由于某一个条件没有满足,暂时不需要CPU TERMINATED 结束 2,线程状态切换 线程的阻塞和唤醒 sleep:线程自我休眠,时间一到自己就会醒来

antique的学习笔记 1191

从零到一:STM32 CubeMX与WS2812B的DMA驱动艺术

本文详细介绍了如何使用STM32 CubeMX和DMA技术驱动WS2812B RGB LED灯条,从硬件设计到软件实现的全过程。通过CubeMX配置定时器和DMA,实现高效的PWM波形生成与数据传输,解决传统GPIO控制资源占用高的问题。文章还提供了颜色编码、动画效果实现及常见问题解决方案,助力开发者快速掌握智能LED控制技术。

weixin_28278377的博客 978

什么是线程,如何在Java中创建和管理线程?

一听到线程,你可能会想:“线程,我为什么要关心这个?我可以专注于我的代码和数据。” 但事实是,线程是构建并发应用程序的关键部分。所以,让我们一起探索一下线程是什么,以及如何在Java中创建和管理它们。首先,我们来谈谈线程的基本概念。线程是一个微小的“单元”,它可以在同一时间执行多个任务。想象一下,如果你有两个线程,一个在厨房煮饭,另一个在客厅打扫,那么这两个线程就可以同时进行,而不会相互干扰。这就是线程的魔力所在。那么,如何在Java中创建和管理线程呢?

scjrw的博客 543

别再手动算LLC了!用Mathcad 15分钟搞定谐振参数与增益曲线(附已验证产品案例)

本文详细介绍了如何利用Mathcad在15分钟内高效完成LLC谐振变换器的参数计算与增益曲线绘制。通过对比传统工具,突出Mathcad在自然数学表达、实时单位换算和动态参数关联方面的独特优势,帮助电源工程师大幅提升设计效率。文章还提供了已验证的产品案例和实用模板,助力工程师快速掌握谐振参数优化技巧。

weixin_42548888的博客 548

uniapp [全端兼容] - 实现接入使用天地图及卫星视图切换功能,在国家天地图上自由切换地图显示类型(卫星视图/地形视图/卫星混合/地形混合/普通视图)天地图切换视图显示改成卫星风格,切换地图图层

uni-app,h5移动端网页,微信小程序,支付宝小程序,Android安卓,苹果ios,app,nvue,天地图,视图,卫星,地图类型切换,uniAPP天地图,改成卫星视角,在线地图切换,来回切换卫星图,天地图如何实现来回切换卫星图,地形图,卫星视图,VUE3引入天地图卫星版,详细教程示例源码代码,天地图切换卫星用什么api,卫星视角API方法,vue3卫星天地图,能改地图的显示方式,vue3中使用天地图自定义样式切换地图图层,切换报错,加载出错,卫星图太卡,地图图层更换,vue3安装集成天地图详细示例源

🏆 前端领域优质创作者 1万+

Java 中的线程是什么,如何创建和管理线程-上(十一)

创建线程后,我们需要对线程进行管理,包括控制线程状态、线程优先级、线程同步等。Java 中的线程是基于操作系统原生线程实现的,但是 Java 提供了更为简单和方便的线程 API,使得线程的创建和管理变得更加容易。线程的管理包括控制线程状态、线程优先级、线程同步等。Java 中的线程有 6 种状态,分别是新建(NEW)、就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、计时等待(TIMED_WAITING)和终止(TERMINATED)。继承 Thread 类是一种创建线程的方式。

Dakaring的专栏 1665

★教程3:Simulink学习教程入门60例目录

1.订阅本教程用户可以免费获得本博任意1个(包括所有免费专栏和付费专栏)博文对应代码;2.本Simulink课程的所有案例(部分理论知识点除外)均由博主编写而成,供有兴趣的朋友们自己订阅学习使用。未经本人允许,禁止任何形式的商业用途;3.本课程除了介绍常见的Simulink模块介绍之外,我们更侧重于各种实例的完整设计介绍。从通信,控制器,图像处理,语音处理,电力系统等诸多常用领域介绍了相关案例,如果对于某个较为复杂的案例,初学者无法正确复现,也可以私信博主获得完整Simulink模型文件。

FPGA/MATLAB学习教程/源码/项目合作开发 1万+

Java核心知识体系-线程管理

在Java程序开发中,线程管理是一个至关重要的方面。它涉及到如何有效地创建、调度、同步和销毁线程,以确保程序的性能、响应性和稳定性。以下是对Java线程管理的详细探讨。

dsgdauigfs的博客 1023

告警不是闹钟:AI 如何把告警过滤成真正的“求救信号”?

告警不是闹钟:AI 如何把告警过滤成真正的“求救信号”?

Echo_Wish 242

手把手教你学veriolg(二十六)-- Verilog CIC 滤波器设计

目录 Verilog CIC 滤波器设计1. CIC 滤波器的基本原理2. Verilog 实现3. 说明4. 测试激励5. 总结 CIC(Cascade-Integrator-Comb)滤波器是一种特殊的数字滤波器,常用于多速率信号处理中,尤其是用于信号的上采样(插值)和下采样(抽取)。CIC 滤波器的特点在于它具有简单的硬件实现、不需要乘法器(仅使用加法器和延迟元件)、且具有线性相位特性。然而,CIC 滤波器的缺点是它会产生较大的量化误差,因此在设计时需要特别注意字长的选择以避免溢出。1. CIC 滤波

MHD0815的博客 890

Java多线程——线程管理

前面两篇博客介绍了多线的基本概念,以及线程同步与协作。本文将继续探讨线程的管理与多线程的性能优化,具体内容包括:线程池、Callable接口,以及如何优雅第终止线程或者如何管理可能需要立即终止的线程。 一、线程池 在第一篇博客中已经介绍过“线程”和“任务”的关系,如果“任务”是货车,“线程”则是供货车行驶的车道。在之前的介绍中,都是一辆“货车”对应一条“车道”。“车道”也是一个对象,他也需要占

a530516618的博客 3953

SAE ARP 4761A内容详解

SAE ARP 4761A主要为民用航空系统及设备的安全评估提供指南,帮助制造商和设计者满足FAA的FAR 25.1309等适航标准的要求。该文档旨在为航空器和航空系统设计中涉及的安全评估流程提供一个结构化的框架,并结合一系列工具和方法,如故障树分析(FTA)、依赖图分析(DD)、马尔可夫分析(MA)等,来系统地评估潜在的安全隐患。这一标准不仅适用于大型航空器的设计和评估,还可以应用于各种类型的飞行系统,如无人驾驶飞行器(UAV)和新型的电动垂直起降飞机(eVTOL)。

qq_41854911的博客 3149

JAVA--多线程管理

转自:  2012年08月2日--Java多线程管理 一、线程概述     线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程。    进程和线程是现代操作

chencangui的专栏 816

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

JAVA领域10位大神

在Java出现以前,Internet上的信息内容都是一些乏味死板的HTML文档。这对于那些迷恋于WEB浏览的人们来说简直不可容忍。Java是一种简单的,面向对象的,分布式的,解释型的,健壮安全的,结构中立的,可移植的,性能优异、多线程的动态语言。下面给大家介绍一些在Java开发史上的名人。 James Gosling 1983年,Gosling获得了卡尔加里大学的计算机科学学士学位。1990年,他获得了卡内基梅隆大学的计算机科学博士学位,师从Bob Sproull。在攻读博士期间,他自己开发了一款e

hhkun0120的专栏 7853

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Java 开发的 10 位牛人,你们都知道吗?

转自:Java译站在Java出现以前,Internet上的信息内容都是一些乏味死板的HTML文档。这对于那些迷恋于WEB浏览的人们来说简直不可容忍。Java是一种简单的,面向对象的,分布...

Java和Android架构 963

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Java开发的10位牛人

在Java出现以前,Internet上的信息内容都是一些乏味死板的HTML文档。这对于那些迷恋于WEB浏览的人们来说简直不可容忍。Java是一种简单的,面向对象的,分布式的,解释型的,健壮安全的,结构中立的,可移植的,性能优异、多线程的动态语言。下面给大家介绍一些在Java开发史上的名人。 James Gosling 1983年,Gosling获得了卡尔加里大学的计算机科学学士

私_有_云 1179

【Rust自学】1.0. 简介

Rust简介

A Rookie in San Domenico School 1313

muduo网络库:18---muduo简介之(muduo库的由来、编译安装、目录结构、代码结构、线程模型)

一、由来 2010年3月我写了一篇《学之者生,用之者死——ACE历史与简 评》1,其中提到“我心目中理想的网络库”的样子: ·线程安全,原生支持多核多线程。 ·不考虑可移植性,不跨平台,只支持Linux,不支持Windows。 ·主要支持x86-64,兼顾IA32。(实际上muduo也可以运行在ARM 上。) ·不支持UDP,只支持TCP。 ·不支持IPv6,只支持IPv4。 ·不考虑广域网应...

董哥的黑板报 5711

JAVASE知识点总结

DOS命令 DOS命令大全(经典收藏):http://www.douban.com/note/185257401/ Java单词缩写解释 JDK: Java Development Kit---------------------java开发工具 JRE: Java Runtime Environment----------------java运行环境 IDE: In...

weixin_30765319的博客 163

人人网——揭秘社交网络之开放平台技术

人人网——揭秘社交网络之开放平台技术 《《点击详情 人人网是由千橡集团将旗下著名的校内网更名而来。2009年8月4日,将旗下著名的校内网更名为人人网,社会上所有人都可以来到这里,从而跨出了校园内部这个范围。人人网为整个中国互联网用户提供服务的SNS社交网站,给不同身份的人提供了一个互动交流平台,提高用户之间的交流效率,通过提供发布日志、保存相册、音乐视频等站内外资源分享等功能搭建了一个

Li_soso的专栏 1125

博客园中前辈技术总结会不会过期

前辈们2009年以前写的.net技术总结会不会已经过期? 主要是: asp.net的Webform; 多线程; asp.net webform, MVC登录验证; WebForm控件方式开发等。 博客园首页左侧,有很多推荐博客和博问专家。博客混得多了,常跑到前辈的地盘,看着园龄都六年、七年,甚至十几年了,深感前辈很多。 个人感觉,1997一个技术阶段,2002一个技术阶段...

weixin_30468137的博客 142

[特殊字符] Rust概述:系统编程的革命者

✅像C++一样快,但不会有内存泄漏✅像Java一样安全,但没有垃圾回收的性能损耗✅像Python一样现代,但能直接操作硬件✅像Go一样并发,但在编译时就能发现数据竞争这就是Rust!Rust不仅仅是一门编程语言,更是一种编程哲学的革新🛡️ 安全性 + ⚡ 性能 + 🎯 生产力 = 🦀 Rust🎯“学习Rust,不仅是学习一门语言,更是学习一种更安全、更高效的编程方式。🛡️编写安全代码的能力⚡优化性能的深度理解🎯现代化的开发体验🚀面向未来的技术储备。

未来已来,AI时代,学AI编程,做AI量化,就来大鹏AI教育。 2003

nginx中location介绍

http://www.cnblogs.com/lidabo/p/4169396.html   博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅  :: 管理 ::   2409 随笔 :: 2 文章 :: 203 评论 :: 0 引用

wtswjtu的专栏 1093

friso mysql_friso: Friso是使用C语言开发的一款高性能中文分词器,使用流行的mmseg算法实现。完全基于模块化设计和实现,可以很方便的植入到其他程序中,例如:MySQL,PHP等...

Friso是什么?Friso 是使用 c 语言开发的一款开源的高性能中文分词器,使用流行的mmseg算法实现。完全基于模块化设计和实现,可以很方便的植入其他程序中,例如:MySQL,PHP,并且提供了php5, php7, ocaml, lua的插件实现。源码无需修改就能在各种平台下编译使用,加载完 20 万的词条,内存占用稳定为 14.5M.Friso核心功能:中文分词:mmseg算法 + Fr...

weixin_35389904的博客 345

friso mysql_GitHub - 452896915/friso: High performance Chinese tokenizer with both GBK and UTF-8 cha...

Friso是什么?Friso 是使用 c 语言开发的一款开源的高性能中文分词器,使用流行的mmseg算法实现。完全基于模块化设计和实现,可以很方便的植入其他程序中,例如:MySQL,PHP,并且提供了php5, php7, ocaml, lua的插件实现。源码无需修改就能在各种平台下编译使用,加载完 20 万的词条,内存占用稳定为 14.5M.Friso核心功能:中文分词:mmseg算法 + Fr...

weixin_42367221的博客 227
上一篇: 2012年08月29日--如何在JSP页面中提高响应速度
下一篇: XML解析工具类
Javamickey
博客等级 码龄17年 3粉丝 18原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值