Java(6)-java线程

线程是一个单独程序流程。多线程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。也就是说,多个线程可以同时在一个程序中运行,并且每一个线程完成不同的任务。程序可以通过控制线程来控制程序的运行,例如线程的等待、休眠、唤起线程等。

一. 线程的基本知识


        线程是程序运行的基本单位,一个程序中可以同时运行多个线程。如果程序被设置为多线程, 可以提高程序运行的效率和处理速度。

1. 什么是线程 

        传统的程序设计语言同一时刻只能执行单任务操作,效率非常低,如果网络程序在接收数据时发生阻塞,只能等到程序接收数据之后才能继续运行。随着 Internet 的飞速发展,这种单任务运行的状况越来越不被接受。如果网络接收数据阻塞,后台服务程序就会一直处于等待状态而不能继续任何操作。 这种阻塞情况经常发生, 这时的 CPU资源完全处于闲置状态。         

        多线程实现后台服务程序可以同时处理多个任务,并不发生阻塞现象。多线程是 Java 语言的一个很重要的特征。 多线程程序设计最大的特点就是能够提高程序执行效率和处理速度。Java 程序可同时并行运行多个相对独立的线程。例如创建一个线程来接收数据,另一个线程发送数据,既使发送线程在接收数据时被阻塞,接受数据线程仍然可以运行。 线程(Thread)是控制线程(Thread of Control)的缩写,它是具有一定顺序的指令序列(即所编写的程序代码)、存放方法中定义局部变量的栈和一些共享数据。线程是相互独立的,每个方法的局部变量和其他线程的局部变量是分开的,因此,任何线程都不能访问除自身之外的其他线程的局部变量。如果两个线程同时访问同一个方法,那每个线程将各自得到此方法的一个拷贝。      

       Java 提供的多线程机制使一个程序可同时执行多个任务。线程有时也被称为小进程,它是从一个大进程里分离出来的小的独立的线程。由于实现了多线程技术,Java 显得更健壮。多线程带来的好处是更好的交互性能和实时控制性能。多线程是强大而灵巧的编程工具,但要用好它却不是件容易的事。在多线程编程中,每个线程都通过代码实现线程的行为,并将数据供给代码操作。编码和数据有时是相当独立的,可分别向线程提供。多个线程可以同时处理同一代码和同一数据,不同的线程也可以处理各自不同的编码和数据。

2 .创建线程方法

       Java程序都是声明一个公共类,并在类内实现一个 main 方法。事实上,这些程序就是一个单线程程序。当它执行完main 方法的程序后,线程正好退出,程序同时结束运行。

public class OnlyThread {
	public static void main(String args[]) {
		run(); // 调用静态run()方法
	}
	/**
	 * 实现run()方法
	 */
	public static void run() {
		// 循环计算输出的*数目
		for (int count = 1, row = 1; row < 10; row++, count++) {
			for (int i = 0; i < count; i++) { // 循环输出指定的count数目的*
				System.out.print('*'); 
			}
			System.out.println();
		}
	}
}


 这只是建立了一个单一线程并执行的普通小程序,并没有涉及到多线程的概念。

二. 创建线程


在 Java程序中,创建线程方法:
一是对 Thread 类进行派生并覆盖 run方法;
二是通过实现 runnable接口创建,当你打算多重继承时,优先选择实现Runnable。
三是实现 Callable 接口
四是使用 Executors 工具类创建线程池

1. Thread创建线程

        在程序中创建新的线程的方法之一是继承 Thread 类, 并通过 Thread子类声明线程对象。继承Thread 类并覆盖 Thread类的 run 方法完成线程类的声明, 通过new创建派生线程类的线程对象。run 中的代码实现了线程的行为。 

      java.lang.Thread 类是一个通用的线程类,由于默认情况下 run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过派生 Thread 类,并用具体程序代码覆盖Thread 类中的 run 方法,实现具有各种不同功能的线程类。

1) Thread 创建线程步骤:

     (1)创建一个新的线程类,继承 Thread 类并覆盖 Thread 类的 run()方法。

    (2)创建一个线程类的对象,创建方法与一般对象的创建相同,使用关键字new完成。 

    (3)启动新线程对象,调用 start()方法。

    (4)线程自己调用 run()方法。

2) Thread创建一个线程

     下面是通过Thread创建线程的例子:产生一个新的线程

class ThreadDemo1 extends Thread {
	ThreadDemo1() {
	}

	// 声明ThreadDemo1带参数的构造方法
	ThreadDemo1(String szName) {
		super(szName);
	}

	// 重载run函数
	public void run() {
		for (int count = 1, row = 1; row < 10; row++, count++) {
			for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*
				System.out.print('*');
			}
			System.out.println();
		}
	}

	public static void main(String argv[]) {
		ThreadDemo1 td = new ThreadDemo1(); // 创建,并初始化ThreadDemo1类型对象td
		td.start(); // 调用start()方法执行一个新的线程
	}
}


    OnlyThread.java程序与程序ThreadDemo1.java表面上看运行结果相同,但是仔细对照会发现,程序OnlyThread.java中对 run方法的调用在程序ThreadDemo1.java中变成了对 start 方法的调用,并且程序ThreadDemo1.java确派生 Thread类,创建新的线程类。

    3) Thread创建多个线程

//文件:程序10.3 ThreadDemo2.java   描述:产生三个新的线程

public class ThreadDemo2 extends Thread {
	// 声明无参数,空构造方法
	ThreadDemo2() {
	}

	// 声明带有字符串参数的构造方法
	ThreadDemo2(String szName) {
		super(szName); // 调用父类的构造方法
	}

	// 重载run函数
	public void run() {
		for (int count = 1, row = 1; row < 10; row++, count++) {
			for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*
				System.out.print('*');
			}
			System.out.println();
		}
	}

	public static void main(String argv[]) {
		ThreadDemo2 td1 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td1
		ThreadDemo2 td2 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td2
		ThreadDemo2 td3 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td3
		td1.start(); // 启动线程td1
		td2.start(); // 启动线程td2
		td3.start(); // 启动线程td3
	}
}

创建了 3 个线程 td1、td2、td3,它们分别执行自己的 run方法。在实际中运行的结果并不是想要的直角三角形, 而是一些乱七八糟的 “*” 行,长短并没有一定的规律,这是因为线程并没有按照程序中调用的顺序来执行, 而是产生了多个线程赛跑现象。 运行结果:

注意:Java线程并不能按调用顺序执行,而是并行执行的单独代码。如果要想得到完整的直角三角形,需要在执行一个线程之前,判断程序前面的线程是否终止,如果已经终止,再来调用该线程。 
 

2. Runnable 接口创建线程 

        通过实现 Runnable 接口的方法是创建线程类的第二种方法。利用实现 Runnable 接口来创建线程的方法可以解决 Java 语言不支持的多重继承问题。                Runnable 接口提供了 run()方法的原型,因此创建新的线程类时,只要实现此接口,即只要特定的程序代码实现Runnable接口中的 run()方法,就可完成新线程类的运行。

      扩展Thread类创建线程的方式,适合编写简单的应用程序代码,而实现Runnable接口创建线程,能够避免Java单继承的局限,适合同一代码的多线程处理同一资源的情况,代码具有良好的一致性,是更符合面向对象思想的设计方式。

     1) Runnable 创建线程步骤

   (1)创建一个实现 Runnable 接口的类,并且在这个类中重写 run 方法。

class ThreadType implements Runnable{ 
     public void run(){ 
         …… 
     } 
} 

   (2)使用关键字 new新建一个 ThreadType 的实例。

Runnable rb = new ThreadType (); 

   (3)通过 Runnable 的实例创建一个线程对象,在创建线程对象时,调用的构造函数是new Thread(ThreadType),它用 ThreadType 中实现的 run()方法作为新线程对象的 run()方法。

Thread td = new Thread(rb); 

   (4)通过调用 ThreadType 对象的 start()方法启动线程运行。

td.start(); 

 2) Runnable 创建线程

class ThreadDemo3 implements Runnable {
	// 重载run函数
	public void run() {
		for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目
			for (int i = 0; i < count; i++){ // 循环输出指定的count数目的*
				System.out.print('*'); 
			}
			System.out.println(); 
		}
	}

	public static void main(String argv[]) {
		Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb
		Thread td = new Thread(rb); // 通过Thread创建线程
		td.start(); // 启动线程td
	}
}
  • 一些基本API:isAlive(),sleep(),getId(),yield()等。

    • isAlive()测试线程是否处于活动状态

    • sleep()让“正在执行的线程”休眠

    • getId()取得线程唯一标识

    • yield()放弃当前的 CPU 资源

JDK1.0定义了stop和suspend方法,stop用来直接终止线程,suspend会阻塞线程直到另一个线程调用resume.
      stop和suspend都有一些共同的点:都试图专横的控制一个给定了的线程的行为.

 从JDK1.2开始,这两个方法都被弃用了.stop天生就不安全,因为可能产生数据不同步等问题。

3.实现Callable 接口

 借助FutureTask执行具体步骤:FutureTask类同时实现了两个接口,Future和Runnable接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

1)、实现Callable接口的类myCallable

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + "Callable  call()方法");
        return 1;
    }

}

2)、以myCallable为参数创建FutureTask对象

3)、将FutureTask作为参数创建Thread对象
4)、调用线程对象的start()方法

5)、通过futuretask可以得到myCallable的call()的运行结果: futuretask.get();

public class TCallable {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("MyCallable:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main方法执行完成");
    }

}

借助线程池来运行

线程池中执行Callable任务的原型例如:

public interface ExecutorService extends Executor {

    //提交一个Callable任务,返回值为一个Future类型

     <T> Future<T> submit(Callable<T> task);


     //other methods...

 }

借助线程池来运行Callable任务的一般流程为:

ExecutorService exec = Executors.newCachedThreadPool();

Future<Integer> future = exec.submit(new MyCallableTask());

通过future可以得到MyCallableTask的call()的运行结果: future.get();

4.使用 Executors 工具类创建线程池

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。具体详看《java(7)-多线程和线程池》

public class SingleThreadExecutor implements Runnable{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        SingleThreadExecutor runnable = new SingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnable);
        }
        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }
}

Thread和Runnable:

  • Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
  • Thread 自身实现了Runnable接口
  • 一般情况下使用Runnable方式创建线程。除非你需要重写Thread类除了Run()方法外的其他方法来自定义线程,否则不建议使用继承Thread的方式来创建。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应将类归为子类。

三. 线程的状态和上下文切换


传统的进(线)程状态一般划分如下:

    
 

1、线程周期状态

JVM虚拟机的线程整个周期从创建到最终的消亡,要经历若干个状态。一般来说,线程包括以下这几个状态:创建(new)、就绪(runnable)、Blocked(blocked、time waiting、waiting)、消亡(dead)。这些状态之间的转化是通过线程提供的一些方法完成的。可以通过jstack查看。

 

 1、创建(new)状态当需要新起一个线程来执行某个子任务时,就创建了一个线程。调用 new方法产生一个线程对象后、调用 start() 方法前所处的状态。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,譬如程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态runnable

 2、可运行(runnable)状态:当线程对象执行 start()方法后,线程就转到可运行状态runnablerunnable状态不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。因为在单处理器系统中运行多线程程序时,一个时间点只有一个线程运行,系统通过调度机制实现宏观意义上的运行线程共享处理器。 因此一个线程是否在运行,除了线程必须处于 Runnable 状态之外,还取决于优先级和调度。 通过jstack可以查看:

注意:   

当得到CPU执行时间之后,线程便真正进入运行状态RUNNING。实际上RUNNING状态是针对操作系统而言,JVM虚拟机并没有这个状态。处于 runnable 状态下的线程正在* Java 虚拟机中执行,但它可能正在等待*来自于操作系统的其它资源,比如处理器。”JVM 把那些都视作资源,cpu 也好,硬盘,网卡也罢,有东西在为线程服务,它就认为线程在“执行”。

如果使用Java 阻塞 I/O 模型读取数据,将会导致线程阻塞,线程将会进入休眠,从而让出 CPU 的执行权,直到数据读取完成。这个期间如果使用 jstack 查看线程状态,却可以发现Java 线程状态是处于 RUNNABLE。

这里说的线程阻塞进入休眠状态,其实是操作系统层面线程实际状态。而我们使用 jstack 查看的线程状态却是 JVM 中的线程状态。

   3、阻塞(Block)状态:线程处于不可运行状态是由于线程被挂起或者发生阻塞,例如对一个线程调用 wait()函数后,它就可能进入阻塞状态;调用线程的notify或notifyAll 方法后它才能再次回到可执行状态。

阻塞的情况分三种:

    一). 等待WAITING运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。进入该状态的线程需要等待其他线程唤醒(通知或中断),也就是等待唤醒状态。

   

    等待WAITING的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。


     二). 阻塞BLOCKED:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

       比如 synchronize 机制有可能让线程进入BLOCKED 状态,如我们在一个方法或者代码块中使用Synchronized时,同一时间有2个线程进入该方法的时候,先获取到锁的线程执行。而没有获得锁的线程就处于BLOCKED 状态。

     三).超时等待TIMED_WAITING(睡眠或等待一定的事件):运行(running)的线程执行Thread.sleep(long ms)、t.join(参数)方法发出了 I/O 请求时JVM会把该线程置为TIMED_WAITING状态或者。当sleep()状态超时、join()等待线程终止或者超时,线程重新转入可运行(runnable)状态。

    

       TIMED_WAITING状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。TIMED_WAITING 与 WAITING 间的联系还是很紧密的,主要差异在时限(timeout)参数上。

一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定 
当wait,join,park方法调用时,进入waiting状态。前提是这个线程已经拥有锁了。

   4、退出(Dead)状态:一个线程可以从任何一个状态中调用 stop 方法进入退出状态。线程一旦进入退出状态就不存在了,不能再返回到其他的状态。除此之外,如果单线程(不是线程池) run方法执行完,main() 方法执行结束,或者因异常退出了run()方法,也会自动进入退出状态,即则该线程结束生命周期。死亡的线程不可再次复生。

2、线程状态转换

线程状态转换函数:              

方法所属类描述有效状态 目的状态
start()  Thread开始执行一个线程:但并不是立即获取到CPU时间片NewRunnable
sleep(long m)Thread暂停一段时间m毫秒RunnableNonRunnable
sleep(long m,int nanoseconds)Thread暂停m时间(默认毫秒),可以精确到纳秒RunnableNonRunnable
yield()Thread明确放弃执行RunnableRunnable
wait()Object进入WAITING阻塞状态,并释放所持有的对象锁,需要唤醒RunnableRunnable
wait(long m)Object

线程wait对象的毫秒数后, 就可再次获取锁执行wait后续代码。

不需被唤醒,。

RunnableNonRunnable
join() Thread进入阻塞状态RunnableNonRunnable
join(long millis)ThreadRunnableNonRunnable
join(long millis,int nanoseconds)ThreadRunnableNonRunnable
notify() Object阻塞状态解除NonRunnable Runnable
interrupt()中断线程RunnableNonRunnable 
stop()结束执行一个线程  New或RunnableDone
suspend() 挂起执行Runnable            NonRunnable
resume() 恢复执行 NonRunnableRunnable

1.线程进入可执行状态 
当以下几种情况发生时,线程进入可执行状态。 
(1)唤醒:线程被notify()/ notifyAll()方法唤醒。
         notify 仅仅唤醒一个线程并允许它获得锁,notifyAll 唤醒所有等待这个对象的线程,并允许它们获得锁。
(2)超时等待TIMED_WAITING结束:线程调用 sleep(millis)方法,millis毫秒之后线程会进入可执行状态。 
        在 millis 毫秒数内让当前正在执行的线程进入休眠状态,等到时间过后,该线程会自动苏醒并继续执行。sleep方法的精确度受到系统计数器的影响。 
       sleep(long millis, int nanos) 在毫秒数(millis)加纳秒数(nanos)内让当前正在执行的线程进入休眠状态,此操作的精确度也受到系统计数器的影响。

(3)IO完成:线程对I/O操作的完成。 

2.线程进入阻塞状态
进入WAITING状态:

一个线程进入 WAITING 状态是因为调用了以下方法:

  • 不带时限的 Object.wait (): 使当前线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
  • 不带时限的 Thread.join(): 等待线程执行完毕,底层调用的是Object实例的wait方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

然后会等其它线程执行一个特别的动作,比如:

  • 一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 或 Object.notifyAll()。
  • 一个调用了 Thread.join 方法的线程会等待指定的线程结束。

没有参数的 wait() 等价于 wait(0),而 wait(0) 它不是等0毫秒,恰恰相反,它的意思是永久的等下去,到天荒地老,除非收到通知。

进入TIMED_WAITING 状态:

带指定的等待时间的等待线程所处的状态。一个线程处于这一状态是因为用一个指定的正的等待时间(为参数)调用了以下方法中的其一:

  • Thread.sleep(long millis):使当前线程睡眠指定时间
  • 带时限(timeout)的 Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • 带时限(timeout)的 Thread.join(long millis):等待当前线程最多执行millis毫秒
  • LockSupport.parkNanos(long nanos):除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

TIMED_WAITING 与 WAITING 间的联系还是很紧密的,主要差异在时限(timeout)参数上

进入BLOCKED状态:

使得线程变成blocked状态有以下方法:

  • 进入一个synchronized代码块/方法失败
  • 在调用了object.wait之后,被其他线程object.notify唤醒之后,重新进入同步代码块或者同步方法失败。

注意:

1、stop()、suspend()和 resume()方法现在已经不提倡使用,这些方法在虚拟机中可能引起“死锁”现象。suspend()和 resume()方法的替代方法是 wait()和 sleep()。线程的退出通常采用自然终止的方法,建议不要人工调用 stop()方法。 

2、sleep和yield

        1)sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

      2)、yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入到就绪状态。即让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

  调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,当某个线程调用了yield()方法之后,只有优先级与当前线程相同或者比当前线程更高的处于就绪状态的线程才会获得执行机会。

  注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

3、wait和sleep

wait:使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁,直到它被其他线程通过notify()或者notifyAll唤醒
sleep:使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
区别:
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。

3、wait和join

1)wait方法在synchronized同步块中调用,而且必须是调用当前线程(main线程)和被调用线程(t1)才可以。join没有这个要求。
2)wait在调用后,可以由notify通知后提前返回主程序。而join必须等被调用线程结束后才能返回
3)即使调用了notify函数,也不是立即返回,需要被调用函数执行完当前的synchronized块后才可以返回。

4、wait 方法要用在while的死循环中、来避免一些虚假的唤醒(该线程醒来的时候可能判断条件的状态已经不是它沉睡时候的状态,所以必须再判断一次条件,才能允许它往下运行,所以必须放在while循环里面)。

3、上下文切换

       对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

  由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。

  因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

  说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。

  虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

四. 线程调度


1、在早期的java1.1中,JVM自己实现线程调度,而不依赖于底层的平台。后来都是通过调用操作系统提供的系统函数创建。也就是说看java是运行在什么操作系统上,java这个应用程序进程的线程调度算法就是什么。

2、在Java SE 8 API规范的Thread类说明中算是找到了线程调度的有关描述:每个线程有一个优先级(从1级到10级),较高优先级的线程比低优先级线程先执行[4]。程序员可以通过Thread.setPriority(int)设置线程的优先级,默认的优先级是NORM_PRIORITY。Java SE 还声明JVM可以任何方式实现线程的优先级,甚至忽略它的存在。

3、在Linux上Java线程一对一地映射到内核级线程上。不过Linux中是不区分进程和线程的,同一个进程中的线程可以看作是共享程度较高的一组进程。Linux也是通过优先级来实现CPU分配的,应用程序可以通过调整nice值(谦让值)来设置进程的优先级。nice值反映了线程的谦让程度,该值越高说明这个线程越有意愿把CPU让给别的线程,nice的值可以由线程自己设定。所以JVM需要实现Java线程的优先级到nice的映射,即从区间[1,10]到[19, -20]的映射。把自己线程的nice值设置高了,说明你的人品很谦让,当然使用CPU的机会就会少一点。

linux调度器实现了一个抢占的、基于优先级的调度算法,支持两种类型的进程的调度:实时进程的优先级范围为[0,99],普通进程的优先级范围为[100,140]。

进程的优先权越高,所获得的时间片就越大。每个就绪进程都有一个时间片。内核将就绪进程分为活动的(active)和过期的(expired)两类:只要进程的时间片没有耗尽,就一直有资格运行,称为活动的;当进程的时间片耗尽后,就没有资格运行了,称为过期的。调度程序总是在活动的进程中选择优先级最高的进程执行,直到所有的活动进程都耗尽了他们的时间片。当所有的活动进程都变成过期的之后,调度程序再将所有过期的进程置为活动的,并为他们分配相应的时间片,重新进行新一轮的调度。所以Linux的线程调度也不会出现饥饿现象。
 

       java多线程应用程序的每一个线程的重要性和优先级可能不同,例如有多个线程都在等待获得CPU的时间片, 那么优先级高的线程就能抢占CPU并得以执行; 当多个线程交替抢占CPU时,优先级高的线程占用的时间应该多。因此,高优先级的线程执行的效率会高些,执行速度也会快些。

       在 Java 中,CPU的使用通常是抢占式调度模式不需要时间片分配进程。抢占式调度模式是指许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行直到结束,或者进入不可运行状态,或者具有更高优先级的线程变为可运行状态,它将会让出 CPU。线程与优先级相关的方法如下:

      public final void setPriority(int newPriority) 设置线程的优先级为 newPriority :         

      newPriority 的值必须在 MIN_PRIORITY 到MAX_PRIORITY范围内,通常它们的值分别是1和10。目前Windows系统只支持3个级别的优

先级, 它们分别是Thread.MAX_PRIORITY、 Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。  

      public final int getPriority() 获得当前线程的优先级。

     线程优先级的例子:

class  InheritThread extends Thread { 
    //自定义线程的run()方法 
    public void run(){ 
         System.out.println("InheritThread is running…"); //输出字符串信息 
         for(int i=0;i<10;i++){ 
              System.out.println(" InheritThread: i="+i);  //输出信息 
              try{ 
                  Thread.sleep((int)Math.random()*1000); //线程休眠 
             } 
             catch(InterruptedException e)     //捕获异常 
             {} 
        } 
    } 
} 

       通过Runnable接口创建的另外一个线程 :

class RunnableThread implements Runnable {
	// 自定义线程的run()方法
	public void run() {
		System.out.println("RunnableThread is running…"); // 输出字符串信息
		for (int i = 0; i < 10; i++) {
			System.out.println("RunnableThread : i=" + i); // 输出i
			try {
				Thread.sleep((int) Math.random() * 1000); // 线程休眠
			} catch (InterruptedException e) { // 捕获异常
			}
		}
	}
}

public class ThreadPriority {
	public static void main(String args[]) {
		// 用Thread类的子类创建线程
		InheritThread itd = new InheritThread();
		// 用Runnable接口类的对象创建线程
		Thread rtd = new Thread(new RunnableThread());
		itd.setPriority(5); // 设置myThread1的优先级5
		rtd.setPriority(5); // 设置myThread2的优先级5
		itd.start(); // 启动线程itd
		rtd.start(); // 启动线程rtd
	}
}

在程序ThreadPriority.java中,线程 rtd 和 itd 具有相同的优先级,所以它们交互占用 CPU,宏观上处于并行运行状态。结果如图3. 

重新设定优先级: 

itd.setPriority(1);  //设置myThread1的优先级1 
rtd.setPriority(10); //设置myThread2的优先级10 
运行程序结果如图4所示。 

       图3相同优先级                                             图2  不同的优先级

从运行结构可以看出程序ThreadPriority.java修改后,由于设置了线程itd和 rtd 的优先级,并且 rtd的优先级较高,基本上是 rtd都优先抢占 CPU资源。 

五. 守护线程


        在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

        守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。

       调用t.setDaemon(true)将线程转换成守护线程。守护线程的唯一用途是为其他线程提供服务。比如说,JVM的垃圾回收、内存管理等线程都是守护线程。

package com.demo.test.testapp;


public class TestDaemon {
    public static void main(String[] args) {
       
        
        Thread myDaemon = new Thread(new MyDaemon());
        myDaemon.setDaemon(true); //设置为守护线程
        myDaemon.start();

         Thread myCommon = new MyCommon();
        myCommon.start();
    }
}
class MyCommon extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程1第" + i + "次执行!");
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class MyDaemon implements Runnable {
    @Override
    public void run() {
        for (long i = 0; i < 9999999L; i++) {
            System.out.println("后台线程第" + i + "次执行!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


从上面的执行结果可以看出: 前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。 

实际上:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题。

注意: 

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。 
(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。 
 


 

守护线程应用场景

前面说了那么多,那么Daemon Thread的实际应用在那里呢?举个例子,Web服务器中的Servlet,在容器启动时,后台都会初始化一个服务线程,即调度线程,负责处理http请求,然后每个请求过来,调度线程就会从线程池中取出一个工作者线程来处理该请求,从而实现并发控制的目的。也就是说,一个实际应用在Java的线程池中的调度线程。

六. 线程同步


    Java 应用程序中的多线程可以共享资源,例如文件、数据库、内存等。当线程以并发模式访问共享数据时,共享数据可能会发生冲突。Java引入线程同步的概念,以实现共享数据的一致性。线程同步机制让多个线程有序的访问共享资源,而不是同时操作共享资源。

1  . 同步概念 

    在线程异步模式的情况下,同一时刻有一个线程在修改共享数据,另一个线程在读取共享数据,当修改共享数据的线程没有处理完毕,读取数据的线程肯定会得到错误的结果。如果采用多线程的同步控制机制,当处理共享数据的线程完成处理数据之后,读取线程读取数据。 
      通过分析多线程出售火车票的例子,可以更好得理解线程同步的概念。线程 Thread1 和线程 Thread2 都可以出售火车票,但是这个过程中会出现数据与时间信息不一致的情况。线程 Thread1 查询数据库,发现某张火车票 T 可以出售,所以准备出售此票;此时系统切换到线程Thread2执行, 它在数据库中查询存票, 发现上面的火车票T可以出售, 所以线程Thread2将这张火车票 T 售出;当系统再次切换到线程 Thread1 执行时,它又卖出同样的票 T。这是一个典型的由于数据不同步而导致的错误。 
     下面举一个线程异步模式访问数据的例子。

//文件:程序ThreadNoSynchronized.java   描述:多线程不同步的原因 
class ShareData {
	public static String szData = ""; // 声明,并初始化字符串数据域,作为共享数据

}
class ThreadDemo extends Thread {
	private ShareData oShare; // 声明,并初始化ShareData 数据域
	ThreadDemo() {
	} // 声明,并实现ThreadDemo 构造方法

	// 声明,并实现ThreadDemo 带参数的构造方法
	ThreadDemo(String szName, ShareData oShare) {
		super(szName); // 调用父类的构造方法
		this.oShare = oShare; // 初始化oShare域
	}
	public void run() {
		for (int i = 0; i < 5; i++) {
			if (this.getName().equals("Thread1")) {
				oShare.szData = "这是第 1 个线程";
				// 为了演示产生的问题,这里设置一次睡眠
				try {
					Thread.sleep((int) Math.random() * 100); // 休眠
				} catch (InterruptedException e) { // 捕获异常
				}
				System.out.println(this.getName() + ":" + oShare.szData); // 输出字符串信息
			} else if (this.getName().equals("Thread2")) {
				oShare.szData = "这是第 2 个线程";
				// 为了演示产生的问题,这里设置一次睡眠
				try {
					Thread.sleep((int) Math.random() * 100); // 线程休眠
				} catch (InterruptedException e) // 捕获异常
				{
				}
				System.out.println(this.getName() + ":" + oShare.szData); // 输出字符串信息
			}
		}
	}
}

public class ThreadNoSynchronized {
	public static void main(String argv[]) {
		ShareData oShare = new ShareData(); // 创建,初始化ShareData对象oShare
		ThreadDemo th1 = new ThreadDemo("Thread1", oShare); // 创建线程th1
		ThreadDemo th2 = new ThreadDemo("Thread2", oShare); // 创建线程th2
		th1.start(); // 启动线程th1
		th2.start(); // 启动线程th2
	}
}

运行结果如下:

Thread1:这是第 2 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程

        程序中预想的结果是:“Thead1:这是第1 个线程”或“Thead2:这是第2 个线程”,但是线程对数据的异步操作导致运行结果出现了差错。  上面程序是由于线程不同步而导致错误。 为了解决此类问题,Java 提供了“锁”机制实现线程的同步。 

        锁机制的原理是每个线程进入共享代码之前获得锁,否则不能进入共享代码区,并且在退出共享代码之前释放该锁,这样就解决了多个线程竞争共享代码的情况,达到线程同步的目的。Java中锁机制的实现方法是共享代码之前加入 synchronized 关键字。 

        在一个类中,用关键字 synchonized 声明的方法为同步方法。Java 有一个专门负责管理线程对象中同步方法访问的工具——同步模型监视器,它的原理是为每个具有同步代码的对象准备惟一的一把“锁”。当多个线程访问对象时,只有取得锁的线程才能进入同步方法,其他访问共享对象的线程停留在对象中等待,如果获得锁的线程调用wait方法放弃锁,那么其他等待获得锁的线程将有机会获得锁。当某一个等待线程取得锁,它将执行同步方法,而其他没有取得锁的线程仍然继续等待获得锁。 
       Java 程序中线程之间通过消息实现相互通信,wait()、notify()及 notifyAll()方法可完成线程间的消息传递。例如,一个对象包含一个 synchonized 同步方法,同一时刻只能有一个获得锁的线程访问该对象中的同步方法, 其他线程被阻塞在对象中等待获得锁。 当线程调用 wait()方法可使该线程进入阻塞状态,其他线程调用notify()或 notifyAll()方法可以唤醒该线程。 

2 .同步格式 

    当把一语句块声明为 synchornized,在同一时间,它的访问线程之一才能执行该语句块。

    1) 方法同步:用关键字 synchonized 可将方法声明为同步,格式如下。 

class 类名{ 
     public synchonized 类型名称 方法名称(){ 
           ...... 
     } 
} 

    2)语句块同步: 对于同步块,synchornized 获取的是参数中的对象锁。 

synchornized(obj) 
{  
  //………………….  
} 

     当线程执行到这里的同步块时,它必须获取 obj 这个对象的锁才能执行同步块;否则线程只能等待获得锁。必须注意的是obj对象的作用范围不同,控制情况不尽相同。示例如下。  

public void method() 
{  
  Object obj= new Object(); //创建局部Object类型对象obj 
  synchornized(obj)   //同步块 
  { 
      //……………..  
  }  
}  

      上面的代码创建了一个局部对象obj。由于每一个线程执行到 Object obj = new Object()时都会产生一个 obj 对象,每一个线程都可以获得创建的新的 obj对象的锁,不会相互影响,因此这段程序不会起到同步作用。

     3)同步类的属性:如果同步的是类的属性,情况就不同了。同步类的成员变量的一般格式如下。 

class method 
{  
    Object o = new Object();  //创建Object类型的成员变量o 
public void test() 
{  
synchornized(o)  //同步块 
{  
            //……………………… 
        }  
    }  
} 

     当两个并发线程访问同一个对象的 synchornized(o)同步代码块时,一段时间内只能有一个线程运行。另外的线程必须等到当前线程执行完同步代码块释放锁之后,获得锁的线程将执行同步代码块。

     有时可以通过下面的格式声明同步块。 

public void method() 
{  
synchornized(this)  //同步块 
{  
    //……………………… 
    }  
} 


    当有一个线程访问某个对象的 synchornized(this)同步代码块时,另外一个线程必须等待该线程执行完此代码块,其他线程可以访问该对象中的非 synchornized(this)同步代码。如果类中包含多个 synchornized(this)同步代码块,如果同步线程有一个访问其中一个代码块,则其他线程不能访问该对象的所有 synchornized(this)同步代码块。对于下面形式的同步块而言,调用 ClassName 对象实例的并行线程中只有一个线程能够访问该对象。 

synchornized(ClassName.class) 
{  
    //……………………. 
} 

    4) synchronized 静态方法与非静态方法:

synchronized 关键字加 static 静态方法上是给 Class 类上锁,可以对类的所有实例对象起作用;synchronized 关键字加到非 static 静态方法上是给对象上锁,对该对象起作用。这两个锁不是同一个锁。

七. 线程通信


1. 生产者与消费者

    生产者与消费者是个很好的线程通信的例子,生产者在一个循环中不断生产共享数据,而消费者则不断消费生产者生产的共享数据。程序必须保证有共享数据,如果没有,消费者必须等待生产新的共享数据。两者之间的数据关系如下:
1) 生产者生产前,如果共享数据没有被消费,则生产等待;生产者生产后,通知消费者消费。
2)消费者消费前,如果共享数据已经被消费完,则消费者等待;消费者消费后,通知生产者生产。

wait 使线程停止运行,notify 使停止的线程继续运行:

  • 在调用 notify()之前,线程必须获得该对象的对象级别锁;

  • 执行完 notify()方法后,不会马上释放锁,要直到退出 synchronized 代码块,当前线程才会释放锁。

  • notify()一次只随机通知一个线程进行唤醒

  • 在调用 wait()之前,线程必须获得该对象的对象级别锁;

  • 执行 wait()方法后,当前线程立即释放锁;

  • 从 wait()返回前,线程与其他线程竞争重新获得锁

  • 当线程呈 wait()状态时,调用线程的 interrup()方法会出现 InterrupedException 异常

  • wait(long)是等待某一时间内是否有线程对锁进行唤醒,超时则自动唤醒。

  • wait():将当前执行代码的线程进行等待,置入”预执行队列”。

  • notify():通知可能等待该对象的对象锁的其他线程。随机挑选一个呈wait状态的线程,使它等待获取该对象的对象锁。

  • notifyAll()notify()差不多,只不过是使所有正在等待队中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。

    为了解决生产者和消费者的矛盾,引入了等待/通知(wait/notify)机制。

class Producer extends Thread {
	Queue q;

	Producer(Queue q) {
		this.q = q;
	}
	public void run() {
		for (int i = 1; i < 5; i++) {
			q.put(i);
		}
	}
}

class Consumer extends Thread {
	Queue q; // 声明队列q
	Consumer(Queue q){ 
		this.q = q; // 队列q初始化
	}
	public void run() {
		while (true) {// 循环消费元素
			q.get(); // 获取队列中的元素
		}
	}
}


    Producer 是一个生产者类,该生产者类提供一个以共享队列作为参数的构造方法,它的run 方法循环产生新的元素,并将元素添加于共享队列;

   Consumer 是一个消费者类,该消费者类提供一个以共享队列作为参数的构造方法,它的 run 方法循环消费元素,并将元素从共享队列删除。

2.共享队列

共享队列类是用于保存生产者生产、消费者消费的共享数据。共享队列有两个域:value(元素的数目)、isEmpty(队列的状态)。共享队列提供了put和 get 两个方法。

class Queue {
	int value = 0; // 声明,并初始化整数类型数据域value
	boolean isEmpty = true; // 声明,并初始化布尔类型数据域isEmpty,用于判断队列的状态

	// 生产者生产方法
	public synchronized void put(int v) {
		// 如果共享数据没有被消费,则生产者等待
		if (!isEmpty) {
			try {
				System.out.println("生产者等待");
				wait(); // 进入等待状态
			} catch (Exception e) // 捕获异常
			{
				e.printStackTrace(); // 异常信息输出
			}
		}
		value += v; // value值加v
		isEmpty = false; // isEmpty赋值为false
		System.out.println("生产者共生产数量:" + v);
		notify();
	}

	public synchronized int get() {
		if (isEmpty) {
			try {
				System.out.println("消费者等待");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		value--;
		if (value < 1) {

			isEmpty = true;
		}
		System.out.println("消费者消费一个,剩余:" + value);
		notify();
		return value;
	}
}

    生产者调用put方法生产共享数据,如果共享数据不为空,生产者线程进入等待状态;否则将生成新的数据,然后调用notify方法唤醒消费者线程进行消费;
消费者调用get方法消费共享数据,如果共享数据为空,消费者进入等待状态,否则将消费共享数据,然后提调用notify方法唤醒生产者线程进行生产。
 

3. 运行生产者与消费者

  下面是生产者与消费者程序的主程序。

public class ThreadCommunication {
	public static void main(String[] args) {
		Queue q = new Queue();
		Producer p = new Producer(q);
		Consumer c = new Consumer(q);
		c.start();
		p.start();
	}
}


注意:考虑到程序的安全性,多数情况下使用 notifiAll(),除非明确可以知道唤醒哪一个线程。wait方法调用的前提条件是当前线程获取了这个对象的锁,也就是说 wait方法必须放在同步块或同步方法中。 

八. 线程死锁


      为了保证数据安全使用 synchronized同步机制, 当线程进入堵塞状态 (不可运行状态和等待状态)时,其他线程无法访问那个加锁对象(除非同步锁被解除),所以

一个线程会一直处于等待另一个对象的状态, 而另一个对象又会处于等待下一个对象的状态,以此类推,这个线程“等待”状态链会发生很糟糕的情形,即封闭环状态(也就是说最后那个对象在等待第一个对象的锁)。此时,所有的线程都陷入毫无止境的等待状态中,无法继续运行,这种情况就称为“死锁。虽然这种情况发生的概率很小,一旦出现,程序的调试变得困难而且查错也是一件很麻烦的事情。

    下面举一个死锁的例子。 

public class ThreadLocked implements Runnable {
	public static boolean flag = true; // 起一个标志作用
	private static Object A = new Object(); // 声明,并初始化静态Object数据域A

	private static Object B = new Object(); // 声明,并初始化静态Object数据域B

	public static void main(String[] args) throws InterruptedException {
		Runnable r1 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r1
		Thread t1 = new Thread(r1); // 创建线程t1
		Runnable r2 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r2
		Thread t2 = new Thread(r2); // 创建线程t2
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}

	public void AccessA() {
		flag = false; // 初始化域flag
		// 同步代码快
		synchronized (A) { // 声明同步块,给对象A加锁
			System.out.println("线程t1 : 我得到了A的锁"); // 输出字符串信息
			try {
				// 让当前线程睡眠,从而让另外一个线程可以先得到对象B的锁
				Thread.sleep(1000); // 休眠
			} catch (InterruptedException e) { // 捕获异常
				e.printStackTrace(); // 异常信息输出
			}
			System.out.println("线程t1 : 我还想要得到B的锁");
			// 在得到A锁之后,又想得到B的锁
			// 同步块内部嵌套同步块
			synchronized (B) { // 声明内部嵌套同步块,指定对象B的锁
				System.out.println("线程t1 : 我得到了B的锁"); // 输出字符串信息
			}
		}
	}

	public void AccessB() {
		flag = true; // 修改flag的值
		// 同步代码块
		synchronized (B) { // 指定同步块,给B加锁
			System.out.println("线程t2 : 我得到了B的锁"); // 输出字符串信息
			try {
				// 让当前线程睡眠,从而让另外一个线程可以先得到对象A的锁
				Thread.sleep(1000); // 休眠
			} catch (InterruptedException e) { // 捕获异常InterruptedException
				e.printStackTrace(); // 异常信息输出
			}
			System.out.println("线程t2 : 我还想要得到A的锁"); // 字符串信息输出
			// 在得到B锁之后,又想得到A的锁
			// 同步块内部嵌套内部快
			synchronized (A) { // 指定同步块,给A加锁
				System.out.println("线程t2 : 我得到了A的锁"); // 输出字符串信息
			}
		}
	}

	public void run() {
		if (flag){ // 当flag为true,执行下面语句
			AccessA(); // 调用AccessA方法
		} else {
			AccessB(); // 调用AccessB方法
		}
	}

}


        程序 ThreadLocked.java中创建了两个线程 t1 和 t2,并且声明两个方法:AccessA和 AccessB。在运行过程中,线程t1 先获得了 A 的锁,然后又要求获得 B 的锁;而 t2
先获得B 的锁,然后又要求获得 A的锁,此时便进入了无休止的相互等待状态,即死锁。 

Java 语言本身并没有提供防止死锁的具体方法,但是在具体程序设计时必须要谨慎,以防止出现死锁现象。通常在程序设计中应注意,不要使用 stop()、suspend()、resume()以及 destroy()方法。 stop()方法不安全,它会解除由该线程获得的所有对象锁,而且可能使对象处于不连贯状态,如果其他线程此时访问对象,而导致的错误很难检查出来。suspend()/resume ()方法也极不安全,调用 suspend()方法时,线程会停下来,但是该线程并没有放弃对象的锁,导致其他线程并不能获得对象锁。调用destroy()会强制终止线程,但是该线程也不会释放对象锁。 

九. 线程定时器


     Timer是一个普通的类,其中有几个重要的方法;而TimerTask则是一个抽象类,其中有一个抽象方法run(),类型线程中的run()方法。我们使用Timer创建一个他的对象,然后使用这对象的schedule方法来完成这种间隔的操作。
    Timer就是一个线程,使用schedule方法完成对TimerTask的调度,多个TimerTask可以共用一个Timer,也就是说Timer对象调用一次schedule方法就是创建了一个线程,并且调用一次schedule后TimerTask是无限制的循环下去的,使用Timer的cancel()停止操作。当然同一个Timer执行一次cancel()方法后,所有Timer线程都被终止。

常用 API

方法说明
schedule(TimerTask task, Date time)在指定的日期执行某一次任务
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)在指定的日期之后按指定的间隔周期,无限循环的执行某一任务
schedule(TimerTask task, long delay)以执行此方法的当前时间为参考时间,在此时间基础上延迟指定的毫秒数后执行一次TimerTask任务
schedule(TimerTask task, long delay, long period)以执行此方法的当前时间为参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一TimerTask任务
  • schedule 和 scheduleAtFixedRate 的区别:schedule 不具有追赶执行性;scheduleAtFixedRate 具有追赶执行性。下面是示列代码:

java.util.Timer timer = new java.util.Timer(true);  
 /*true 说明这个timer以daemon方式运行(优先级低, 程序结束timer也自动结束),注意,javax.swing  包中也有一个Timer类,如果import中用到swing包,  要注意名字的冲突。 
 */ 
     
TimerTask task = new TimerTask() {  
    public void run() {  
    ... //每次需要执行的代码放到这里面。  
    }  
};  
     
//以下是几种调度task的方法:     
timer.schedule(task, time);  
// time为Date类型:在指定时间执行一次。  
     
timer.schedule(task, firstTime, period);  
// firstTime为Date类型,period为long  
// 从firstTime时刻开始,每隔period毫秒执行一次。  
     
timer.schedule(task, delay)  
// delay 为long类型:从现在起过delay毫秒执行一次  
     
timer.schedule(task, delay, period)  
// delay为long,period为long:从现在起过delay毫秒以后,每隔period  
// 毫秒执行一次。 

完整的示例代码:
1、定制任务:
import java.util.Timer;       
public class TimerTaskTest extends java.util.TimerTask{ 
     
    @Override 
    public void run() { 
       // TODO Auto-generated method stub 
       System.out.println("start"); 
    } 

2.调用java.util.Timer :

import java.util.Timer;       
public class Test { 
    public static void main(String[] args){ 
        Timer timer = new Timer();
        timer.schedule(new TimerTaskTest(), 1000, 2000); 
     } 
 } 

以上代码表示1秒后开始每个2秒钟执行一次TimerTaskTest中的run()方法。

十. 线程相关问题


1、什么时候抛出InvalidMonitorStateException异常,为什么?

调用wait()/notify()/notifyAll()中的任何一个方法时,如果当前线程没有获得该对象的锁,那么就会抛出IllegalMonitorStateException的异常(也就是说程序在没有执行对象的任何同步块或者同步方法时,仍然尝试调用wait()/notify()/notifyAll()时)。由于该异常是RuntimeExcpetion的子类,所以该异常不一定要捕获(尽管你可以捕获只要你愿意).作为RuntimeException,此类异常不会在wait(),notify(),notifyAll()的方法签名提及。

2、Sleep()、suspend()和wait()之间有什么区别?

Thread.sleep()使当前线程在指定的时间处于“非运行”(Not Runnable)状态。线程一直持有对象的监视器。比如一个线程当前在一个同步块或同步方法中,其它线程不能进入该块或方法中。如果另一线程调用了interrupt()方法,它将唤醒那个“睡眠的”线程。

注意:sleep()是一个静态方法。这意味着只对当前线程有效,一个常见的错误是调用t.sleep(),(这里的t是一个不同于当前线程的线程)。即便是执行t.sleep(),也是当前线程进入睡眠,而不是t线程。t.suspend()是过时的方法,使用suspend()导致线程进入停滞状态,该线程会一直持有对象的监视器,suspend()容易引起死锁问题。

object.wait()使当前线程出于“不可运行”状态,和sleep()不同的是wait是object的方法而不是thread。调用object.wait()时,线程先要获取这个对象的对象锁,当前线程必须在锁对象保持同步,把当前线程添加到等待队列中,随后另一线程可以同步同一个对象锁来调用object.notify(),这样将唤醒原来等待中的线程,然后释放该锁。基本上wait()/notify()与sleep()/interrupt()类似,只是前者需要获取对象锁。

3、在静态方法上使用同步时会发生什么事?

同步静态方法时会获取该类的“Class”对象,所以当一个线程进入同步的静态方法中时,线程监视器获取类本身的对象锁,其它线程不能进入这个类的任何静态同步方法。它不像实例方法,因为多个线程可以同时访问不同实例同步实例方法。

4、当一个同步方法已经执行,线程能够调用对象上的非同步实例方法吗?

可以,一个非同步方法总是可以被调用而不会有任何问题。实际上,Java没有为非同步方法做任何检查,锁对象仅仅在同步方法或者同步代码块中检查。如果一个方法没有声明为同步,即使你在使用共享数据Java照样会调用,而不会做检查是否安全,所以在这种情况下要特别小心。一个方法是否声明为同步取决于临界区访问(critial section access),如果方法不访问临界区(共享资源或者数据结构)就没必要声明为同步的。

5、 在一个对象上两个线程可以调用两个不同的同步实例方法吗?

不能,因为一个对象已经同步了实例方法,线程获取了对象的对象锁。所以只有执行完该方法释放对象锁后才能执行其它同步方法。看下面代码示例非常清晰:Common 类 有synchronizedMethod1()和synchronizedMethod2()方法,MyThread调用这两个方法。

public class Common {
  public synchronized void synchronizedMethod1() {
     System.out.println("synchronizedMethod1 called");
      try {
         Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
      System.out.println("synchronizedMethod1 done");
  }
 
   public synchronized void synchronizedMethod2() {
       System.out.println("synchronizedMethod2 called");
       try {
          Thread.sleep(1000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
       System.out.println("synchronizedMethod2 done");
   }
}

public class MyThread extends Thread {
   private int id = 0;
   private Common common;
 
   public MyThread(String name, int no, Common object) {
       super(name);
      common = object;
      id = no;
   }
 
    public void run() {
       System.out.println("Running Thread" + this.getName());
       try {
          if (id == 0) {
             common.synchronizedMethod1();
           } else {
           common.synchronizedMethod2();
          }
       } catch (Exception e) {
        e.printStackTrace();
    }
}
 
public static void main(String[] args) {
  Common c = new Common();
  MyThread t1 = new MyThread("MyThread-1", 0, c);
  MyThread t2 = new MyThread("MyThread-2", 1, c);
  t1.start();
  t2.start();
}
}

  • 17
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值