您的位置:首页 > 编程语言 > Java开发

Java中的线程Thread解析及用途

2015-06-04 16:23 267 查看

Java中的线程

进程和线程
在并发性程序中,有两个基本的执行单元:进程和线程。在Java编程语言中,并发编程大多数情况下都是和线程相关。然而,进程也是很重要的。

一个计算机系统中通常都有很多活动的进程和线程。这一点即使是在只有一个执行核心,并且在给定时刻只能执行一个线程的系统中都是存在的。单一核心的处理时间是由整个操作系统的“时间片”特性来在众多的进程和线程中共享的。

现在,计算机系统中有多个处理器或者是有多核处理器的情况越来越普遍。这就大大增强了系统执行多个进程和线程的并发性。

进程
一个进程就是一个独立的执行环境。进程有着完整的,私有的基本的运行时资源,尤其是每个进程都有自己的内存空间。

进程通常会被看作是程序或者是应用程序的同义词。然而,用户看到的应用程序实际上可能是多个相互协作的进程。为了方便进程间通讯,绝大多数的操作系统都支持IPC(Inter Process Communication , 进程间通讯),诸如管道(pipe)和套接字(socket)。 IPC不仅可用于统一系统中进程间的相互通讯,还可以用于不同系统间的进程通讯。

大多数的java虚拟机的实现都是作为一个单独的进程的。通过使用ProcessBuilder,Java应用程序可以创建额外的进程。多进程的应用程序超出了本节讨论的范围。

线程
线程有时被称为是轻型的进程。进程和线程都提供了一种运行环境。但是创建一个新的线程比创建一个新的进程需要更少的资源。

线程存在于进程之中——每个进程中至少有一个线程。同一进程的多个线程间共享进程的资源,包括内存和文件。这样做是出于效率的考虑,但是可能带来潜在的通信问题。

多线程是Java平台的一个重要特性。如果我们将进行诸如内存管理和信号处理的线程算上的“系统”线程计算上的话,那么每一个应用程序至少都有一个线程,或者是多个线程。但是从应用程序的编程人员的角度来看,我们总是从一个叫做主线程的线程开始。该线程能够创建其他的线程,这点我们会在下一节中进行讨论。

线程对象
每一个线程都是和类Thread的实例相关联的。在Java中,有两种基本的使用Thread对象的方式,可用来创建并发性程序。

在应用程序需要发起异步任务的时候,只要生成一个Thread对象即可,这样可以直接控制线程的创建并对其进行管理。
把应用程序的任务交给执行器(executor),这样可以将对线程的管理从程序中抽离出来。

本节将对使用Thread对象这种方式进行讨论。采用执行器的方式可以参阅高级的并发性对象(high-levelconcurrency objects)。

定义并启动一个线程
创建线程的应用程序必须提供线程运行时的代码。有两种方式:

提供一个可运行的对象(Runnableobject)。接口Runnable中定义了唯一的方法:run, 意思就是说其中含有运行时需要执行的代码。该Runnable对象被传递给Thread的构造函数,如下:

[java]
view plaincopy

public classHelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from athread!");
}

public static void main(String args[]) {
(new Thread(newHelloRunnable())).start();
}

}

继承Thread类。类Thread本身已经实现了Runnable,尽管其实现中什么也没有做。应用程序中,可以继承该类,并提供自己的run实现,如下:

[java]
view plaincopy

public class HelloThread extends Thread {

public void run() {
System.out.println("Hello from a thread!");
}

public static void main(String args[]) {
(new HelloThread()).start();
}

}<span style="font-weight: bold; "> </span>

—————————————————————————————————————————————————————————————————————————————

注意:上面两个示例程序中都调用了Thread.start来启动一个新的线程。—————————————————————————————————————————————————————————————————————————————

那我们到底应该选用上面两种方式中的哪种呢?第一种方式更通用一些,因为Runnable对象可以是从Thread类之外的别的类派生而来的。而第二种方式在一些简单的程序中用起来则更方法;但是这种方式中线性了我们的任务类只能是从Thread派生而来的(可以直接派生,也可以是间接派生)。本书将聚焦于第一种方式,也就是将可运行的任务和执行该任务的Thread对象向分离的这种方式。这不仅仅是因为这种方式更灵活,还由于这种方式更适合我们后面要讨论的用于管理线程的高级API。

类Thread中定义了一些用于线程管理的方法。这其中就有一些是静态方法。他们可用来获取调用这些方法的线程的信息;或者是用来修改调用这些方法的线程的状态。而其他的一些方法则由涉及到对这些线程和Thread对象进行管理的线程来调用。我们将会在接着的小节中对其中的一些方法进行讨论。

使用sleep来暂停线程的执行
调用方法Thread.sleep可以让当前线程挂起一个指定的时间段。这是一种让进程的其他线程得到处理器时间的一个有效手段;或者是系统中其他应用程序得到处理器时间的一个很好方式。该方法还可用协调节奏,正如下面的实例程序中的那样,可用来等待别的需要一定时间要求来处理其任务的线程。

该方法有着两种重载形式:以毫秒为单位指定睡眠时间和以纳秒为单位指定睡眠时间。但是,其中指定的时间并不能保证很精确。这是因为这点取决于操作系统。另外,睡眠是可以通过中断而结束的,这点我们将会在本小节的后面看到。总之,我们不能见底调用该方法就可以确保线程被精确地挂起指定的时间长度。

示例程序SleepMessages中使用sleep来实现间隔4秒钟打印消息:

[java]
view plaincopy

public class SleepMessages {
public static void main(String args[])
throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};

for (int i = 0;
i < importantInfo.length;
i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}<span style="background-color: rgb(238, 236, 225); "> </span>

—————————————————————————————————————————————————————————————————————————————注意:在main的声明表明它会抛出InterruptedException异常。这种异常是当sleep出于激活态时被别的程序中断是会抛出的异常。由于在上面的示例程序中没有别的线程来进行中断,因此没有必要捕获这个异常。

—————————————————————————————————————————————————————————————————————————————

中断
中断就是一种指示:线程应该停止正在进行的事情。至于线程是如何响应这种中断指示完全是由程序员决定的,但是一般情况下都是终止该线程。这中使用方法也是在本节内容中所要强调的。

线程通过调用需要被中断的线程的interrupt方法来发送中断指示。为了能够是的中断机制正常工作,被中断的线程必须支持自身的中断。

支持中断
线程如何支持自身的中断了?这取决于它正在做的事情。如果线程中频繁调用能够抛出InterruptedException异常的方法,那么在捕获这种异常后,可以只是简单地退出线程。例如,在SleepMessages示例程序中,循环打印消息的代码段是在线程可运行对象的run方法中,那么可以按照如下方式对其进行修改,以便支持中断:

[java]
view plaincopy

for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// We've been interrupted: no more messages.
return;
}
// Print a message
System.out.println(importantInfo[i]);
}

许多抛出InterruptedException的方法,例如sleep方法的设计都是在接收到中断指示的时候立刻中断当前的操作并返回。

如果一个线程长时间运行而没有调用到可以抛出InterruptedException的异常,又该怎么样了?他必须周期性地调用Thread.interrupted来检查是否接收到中断指示。该方法在接收到中断指示的时候返回true。例如:

[java]
view plaincopy

for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// We've been interrupted: no more crunching.
return;
}
}

在上面的这个简单示例程序中,我们仅仅是对中断指示进行了简单的检测,并在检测到该指示后退出线程。在更为复杂的应用程序中,抛出这种InterruptedException则显得根伟合理些。

[java]
view plaincopy

if (Thread.interrupted()) {
throw new InterruptedException();
}

中断指示标记
中断机制的实现是通过内部的中断状态来完成的。调用Thread.interrupt会设置该状态。当一个线程通过调用静态的Thread.interrupted方法来检查另外一个线程的中断状态时,该状态会被清除掉。而使用非静态的isInterrupted方法来检查一个线程的中断状态则不会修改状态的值。

一般的做法是:任何通过抛出InterruptedException异常而退出的方法都会清除中断状态。然而,中断状态很快又被别的线程通过调用interrupt而再次设置也是有可能的。

线程的连接(join)
join方法将会使得一个线程等待另外一个线程执行结束。假如t是目前正在执行的线程线程对象:

t.join();

将会导致当前线程暂停,直到t的线程终止。join方法的重载版本使得程序员可以指定等待时长。然而,和sleep一样,这取决于操作系统,因此不能假定join方法的确可以等待那么长时间。

和sleep类似,join方法可以相应中断:通过抛出InterruptedException而退出。

SimpleThreads示例程序
下面的程序中用到了前面提到的部分概念。SimpleThreads程序中含有两个线程。第一个就是每个Java程序都有的main线程。在main线程中,通过Runnable对象创建了一个线程:MessageLoop,并等待期执行完毕。如果MessageLoop线程执行的时间过长,main线程就会中断它。

MessageLoop线程打印出一系列的信息。如果在完成信息打印之前被中断,该线程就会打印一个消息并退出。

[java]
view plaincopy

public class SimpleThreads {

// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}

private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}

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

// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;

// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}

threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();

threadMessage("Waiting for MessageLoop thread to finish");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}

首先来看一张图,下面这张图很清晰的说明了线程的状态与Thread中的各个方法之间的关系,很经典的!



在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。

要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。可通过这种方法实现多个线程的资源共享。

线程的生命周期:

1.新建状态(New):用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内存

2.就绪状态(Runnable):当一个线程创建了以后,其他的线程调用了它的start()方法,该线程就进入了就绪状态。处于这个状态的 线程位于可运行池中,等待获得CPU的使用权

3.运行状态(Running): 处于这个状态的线程占用CPU,执行程序的代码

4.阻塞状态(Blocked): 当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到 运行状态。

阻塞状态分为三种情况:

1)、 位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,java虚拟机就回把线程放到这个对象的等待池中

2)、 位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的琐池中。

3)、 其它的阻塞状态:当前线程执行了sleep()方法,或者调用了其它线程的join()方法,或者发出了I/O请求时,就会进入这个状态中。

一、创建并运行线程

当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。

二、挂起和唤醒线程

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

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

三、终止线程的三种方法

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

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

2. 使用stop方法强行终止线程(线程中调用了阻塞代码)(这个方法不推荐使用,因为stop是依靠抛出异常来结束线程的,也可能发生不可预料的结果)。如果没有调用阻塞代码,可以正常结束线程。

3. 使用interrupt方法中断线程(线程中调用了阻塞代码)(其实这种方法也是通过抛出异常来结束线程的)。如果没有调用阻塞代码,可以通过判断线程的中断标志位来介绍线程。

线程的几个方法:

join():等待此线程死亡后再继续,可使异步线程变为同步线程,join方法是不会释放锁

interrupt()
:中断线程,被中断线程会抛InterruptedException

wait():等待获取锁:表示等待获取某个锁执行了该方法的线程释放对象的锁,JVM会把该线程放到对象的等待池中。该线程等待其它线程唤醒

notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。使线程由阻塞队列进入就绪状态

sleep():让当前正在执行的线程休眠,有一个用法可以代替yield函数——sleep(0)

yield():暂停当前正在执行的线程对象,并执行其他线程。也就是交出CPU一段时间(其他同样的优先级或者更高优先级的线程可以获取到运行的机会)

sleep和yield区别:

1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。

2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。

3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常

4、sleep()方法比yield()方法具有更好的移植性

如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下的办法之一:

1、调整各个线程的优先级

2、让处于运行状态的线程调用Thread.sleep()方法

3、让处于运行状态的线程调用Thread.yield()方法

4、让处于运行状态的线程调用另一个线程的join()方法

首先,wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。

(1).常用的wait方法有wait()和wait(long timeout):

void wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

void wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。

wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。

wait()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized代码块中进行调用。如果在non- synchronized函数或non-synchronized代码块中进行调用,虽然能编译通过,但在运 行时会发生IllegalMonitorStateException的异常。

(2).Thread.sleep(long millis),必须带有一个时间参数。

sleep(long)使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;

sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;

sleep(long)是不会释放锁标志的

(3).yield()没有参数。

sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出CPU占有权,但让出的时间是不可设定的。yield()也不会释放锁标志

实际上,yield()方法对应了如下操作: 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程些时获得CPU占有权。 在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。所以yield()只能使同优先级的线程有执行的机会。

volitile 语义

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。

volatile包含以下语义:

(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

数据同步:

线程同步的特征:

1、如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得对象的同步锁而互相牵制)

2、每个对象都有唯一的同步锁

3、在静态方法前面可以使用synchronized修饰符,但是要注意的是锁对象是类(用Object.class而不能用this),而不是这个类的对象。

4、当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或者执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。

5、synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非用synchronized修饰。

6、synchronized 关键字能够修饰一个对象实例中的函数或者代码块。 在一个非静态方法中 this 关键字表示当前的实例对象。 在一个 synchronized 修饰的静态的方法中,这个方法所在的类使用 Class 作为实例对象
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: