Java并发(1)

1.创建线程的几种方法及其优缺点

(1)继承Thread(Thread类也是实现了Runnable接口)

(2)实现Runnable接口或Callable接口

通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。因为实现Runnable接口相比继承Thread类有如下优势:

1)可以避免由于Java的单继承特性而带来的局限;

2)增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;

3)适合多个相同程序的线程处理同一资源的情况;

4)性能方面

Runnable实现线程可以对线程进行复用,因为Runnable是轻量级的对象,重复new不会耗费太大资源,而Thread则不然,它是重量级对象,而且线程执行结束就完了,无法再次利用。

注意:

一个线程只能start一次,多次调用start方法,会出现java.lang.IllegalStateException。

CallableRunnable不同点:

(1)Callable规定的方法是call(),而Runnable规定的方法是run();

(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的;

(3)call()方法可抛出异常,而run()方法是不能抛出异常的;

(4)使用Callable需要与Future类配合使用。运行Callable任务可拿到一个Future对象,Future表示异步计算的结果

2.线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

Servlet线程安全吗

答案是否定的。这是由于Servlet在tomcat中是以单例模式存在的,也就是说,当客户端第一次请求servlet,tomcat容器会根据web.xml生成对应的servlet实例,当客户端第二次请求同一个servlet,tomcat容器不会再生成新的。因此,不同请求共享同一个servlet实例的资源,从而导致线程不安全的问题

3.死锁

死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法推进。

死锁产生的原因

(1)系统资源不足。(2)进程推进的顺序非法。

(3)资源分配不当。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

(1)互斥条件:在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源,则请求进程只能等待。

(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能被其他进程强行夺走。

(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁情景(哲学家就餐问题)

死锁代码(锁顺序死锁)

public classLeftRightDeadlock {

private final Object left = new Object();

private final Object right = new Object();

public void leftRight() {

synchronized (left) {

synchronized (right) {

doSomething();

}

}

}

public void rightLeft() {

synchronized (right) {

synchronized (left) {

doSomethingElse();

}

}

}

}

预防死锁

1)加锁顺序

当多个线程需要相同的锁,但以不同的顺序获取锁时,这时死锁就很容易发生。如果所有的锁都是按照相同的顺序获取,那么死锁是不会出现的。

2)加锁超时

另一种预防死锁的机制就是试图获取锁时设置超时。如果线程在规定时间内没有获得锁,则会放弃,并释放自身锁持有的锁,等待一个随机时间,然后重试。在随机等待时间内,给予其他线程获取相同的锁,从而避免死锁发生。

3)死锁检测

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。

Java死锁检测

(1)通过线程转储信息来分析死锁。使用JDK自带工具,jps命令+jstack命令监测是否有死锁。

(2)JConsole是JDK自带的图形化界面工具,含有线程“检测死锁”的功能。

4.线程的状态(线程生命周期)

在Thread类里有一个枚举类型State,定义了线程的几种状态,分别有:

(1)NEW(新建):线程创建之后,但是还没有启动

(2)RUNNABLE(运行):Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行也有可能正在等待着CPU为它分配执行时间

(3)WAITING(无限期等待):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒。以下方法会让线程陷入无限期的等待状态:

lObject的wait方法,没有使用timeout参数;

lThread的join方法,没有使用timeout参数;

lLockSupport的park方法。

(4)TIMED_WAITING(限期等待):有等待时间的等待状态,处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:

lThread.sleep方法

lObject的wait方法,带有时间

lThread.join方法,带有时间

lLockSupport的parkNanos方法,带有时间

lLockSupport的parkUntil方法,带有时间

(5)BLOCKED(阻塞):“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

(6)TERMINATED(结束):线程终止的状态,线程已经结束执行。

5.进程与线程的区别

1)调度

线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。

2)并发性(并发粒度)

不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。

3)拥有资源

进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

4)系统开销

在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

6.进程间通信方式

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。


1)共享存储区:通过系统调用创建共享存储区。多个进程可以通过系统调用连接同一个共享内存区,通过访问共享内存区实现进程之间的数据交换。使用共享内存区时需要利用信号量解决同步互斥问题。

2)消息传递:在消息传递系统中,进程间的数据交换是以格式化的消息为单位的。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接受消息两个原语进行数据交换。

A)直接通信方式:发送进程直接把消息发送给接受进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列上取得消息。

B)间接通信方式:发送进程把消息发送到某个中间实体中,接收进程从中间实体中取得消息。这种中间实体一般称为信箱,这种通信方式又称为信箱通信方式。

3)管道系统管道是先进先出的信息流,允许多个进程向管道写入数据,允许多个进程从管道读出数据。在读/写过程中,操作系统保证数据的写入顺序和读出顺序是一致的。进程通过读/写管道文件或通道设备实现彼此之间的通信。

4)共享文件:利用操作系统提供的文件共享功能实现进程之间的通信。这时,也需要信号量解决文件共享操作中的同步和互斥问题。

7.谈谈你对Java内存模型的理解

1)主内存与工作内存:Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。


2)可见性、原子性、有序性

可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

有序性:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。

(3)Java语言中有一个“先行发生”(happenbefore)的规则,它是Java内存模型中定义的两项操作之间的偏序关系如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。

8.锁优化策略

1)自旋锁

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。而在很多应用上,共享数据的锁定状态只会持续很短的一段时间。若实体机上有多个处理器,能让两个以上的线程同时并行执行,我们就可以让后面请求锁的那个线程原地自旋(不放弃CPU时间),看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是自旋锁。

如果锁长时间被占用,则浪费处理器资源,因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。JDK1.6引入自适应的自旋锁:自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

2)锁削除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除(主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行)。

3)锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部(由多次加锁编程只加锁一次)。

4)轻量级锁

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

5)偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

9.乐观锁和悲观锁

互斥同步最主要的问题就是进行线程阻塞和唤醒带来的性能问题,因此这种同步也被称为阻塞同步。同时,这也是一种悲观的并发策略总是认为只要不去做正确的同步措施就肯定会出问题。

随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗的说就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据存在竞争,就再进行补偿措施(最常见的就是不断重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作被称为非阻塞同步

CAS

CAS就是Compare and Swap的意思,比较并交换。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。但无论是否更新了V的值,都会返回V的旧值。这个过程是个原子操作。

java.util.concurrent.atomic包下面的所有的原子变量类型中,比如AtomicInteger,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作。

CAS缺点

1ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2)只能保证一个共享变量的原子操作

3)循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

10.原子操作类

java.util.concurrent.atomic包下面的所有的原子变量类型都实现了CAS。

AtomicInteger内部有一个变量UnSafe

private static final Unsafe unsafe =

Unsafe.getUnsafe();

Unsafe类是一个可以执行不安全、容易犯错的操作的一个特殊类。虽然Unsafe类中所有方法都是public的,但是这个类只能在一些被信任的代码中使用。

Unsafe类可以执行以下几种操作:

(1)分配内存,释放内存:在方法allocateMemory,reallocateMemory,freeMemory中,有点类似c中的malloc,free方法;

(2)可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法;

(3)挂起和恢复线程,被封装在LockSupport类中供使用;

(4)CAS操作(CompareAndSwap,比较并交换,是一个原子操作)。

AtomicInteger中用的就是Unsafe的CAS操作。

public final int incrementAndGet() {

for (;;){

intcurrent = get();

intnext = current + 1;

if(compareAndSet(current, next))

return next;

}

}

AtomicReference是作用是对"对象引用"进行原子操作。

原子操作类分类:

(1)原子更新基本类型类

AtomicBoolean:原子更新布尔类型。

AtomicInteger:原子更新整型

AtomicLong:原子更新长整型

(2)原子更新数组

AtomicIntegerArray:原子更新整型数组里的元素

AtomicLongArray:原子更新长整型数组里的元素

AtomicReferenceArray:原子更新引用类型数组里的元素

(3)原子更新引用类型

AtomicReference:原子更新引用类型

AtomicReferenceFieldUpdater:原子更新引用类型里的字段

AtomicMarkableReference:原子更新带有标记位的引用类型

(4)原子更新字段类

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

AtomicLongFieldUpater:原子更新长整型的字段的更新器

AtomicStampedReference:原子更新带有版本号的引用类型

11.volatile关键字

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。

(1)可见性,当一个线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。

(2)禁止指令重排序

底层原理

加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写回系统内存

(3)这个写回内存的操作会导致其他CPU中对应的缓存行无效

应用场景

使用volatile必须具备以下2个条件:

a)运算结果并不依赖于当前值,或者能确保只有单一的线程能够修改变量的值。

b)变量不需要和其他的状态变量共同参与不变约束

1)状态标记

2)DCL单例模式(double check lock

12.synchronized关键字

实现原理

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorentermonitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Synchronized的修饰对象有几种

(1)修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

(2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

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

l构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

lsynchronized关键字不能被继承。如果子类覆盖了父类的被synchronized关键字修饰的方法,那么子类的该方法只要没有synchronized关键字,那么就默认没有同步,也就是说,不能继承父类的synchronized。

(3)修改一个静态的方法其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

(4)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

l当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

l当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

l尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

l以上规则对其它对象锁同样适用。

13.volatile与synchronized的比较

(1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块

(2)多线程访问volatile不会出现阻塞,而synchronized会出现阻塞

(3)volatile能保证数据的可见性,但不保证原子性(double和long除外);而synchronized可以保证原子性,也可以间接保证可见性

14.重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取的锁的操作的粒度是“线程”而不是“调用”。重入的一种现实方法是,为每个锁关联一个获取计数值和一个所有者线程

15.锁的等级:方法锁、对象锁、类锁

注意:方法锁也是对象锁。

类锁与对象锁区别

类锁和对象锁不是同1个东西,1个是类的Class对象的锁,1个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

对象锁Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待,synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

类锁对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,Java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个Java对象,只不过有点特殊而已。由于每个Java对象都有1个互斥锁,而类的静态方法是需要Class对象。所谓的类锁,不过是Class对象的锁而已

16.Java线程间的通信

(1)volatilesynchronized关键字

保证了线程对变量访问的可见性。这种方式本质上是“共享数据”,而非“传递数据”;只是从结果来看,数据好像是从写线程传递到了读线程。总的来说,这种方式并不是真正意义上的“通信”,而是“共享”。

(2)等待/通知机制

等待/通知机制是指一个线程A调用了对象Owait方法进入等待状态,而另一个线程B调用了对象O的notify或者notifyAll方法,线程A收到通知后从对象O的wait方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的waitnotify/notifyAll的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作

(3)管道输入/输出流

它主要用于线程之间的数据传输,而传输的媒介是内存。

(4)Thread.join的使用

join方法能将并发执行的多条线程串行执行。join方法属于Thread类,通过一个thread对象调用。当在线程B中执行threadA.join()时,线程B将会被阻塞(底层调用wait方法),等到threadA线程运行结束后才会返回join方法。

17.sleep与wait方法的区别

1)持有者

sleep是Thread的静态方法,sleep的作用是让线程休眠指定的时间,在时间到达后继续执行本线程后续操作。

wait是Object的方法,所以任何对象都可以调用wait方法。调用wait方法后调用者的线程将挂起,直到其他线程调用同一个对象的notify或者notifyAll方法才会继续执行本线程后续操作。wait属于两个线程间的交互。

2)同步锁是否释放

sleep会一直持有同步锁,继续占用CPU资源。

wait会释放掉同步锁,使得其他线程可以使用同步控制块或者方法。想想也是如果不释放同步锁那么谁来notify它呢。

3)与synchronized搭配使用

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,否则报告java.lang.IllegalMonitorStateException异常。

sleep可以在任何地方执行。

补充Thread.yield方法

yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

yieldwait的比较

wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而yield()的作用是让步,它也会让当前线程离开“运行状态”。它们的区别是:

1) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。

2) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

18.Thread类中的start和run方法区别

start:它的作用是启动一个新线程,新线程会执行相应的run方法。start不能被重复调用。

run:run就和普通的成员方法一样,可以被重复调用。单独调用run的话,会在当前线程中执行run,而并不会启动新线程!

本质:start方法实际上是通过本地方法start0方法启动线程的。而start0方法会新运行一个线程,新线程会调用run方法。

19.Daemon线程

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

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。

守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出。

虽然守护线程可能非常有用,但必须小心确保其它所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。还有几点:

1)可以通过Thread的setDaemon方法将线程设置为Daemon线程,该方法必须在线程启动之前调用;

2)Daemon线程创建的线程也是Daemon线程;

3)Daemon不应该访问数据库、文件等资源,因为它随时有可能被中断(甚至无法执行finall中的语句)。

20.终止线程有几种方式

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

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

需要while()循环在某以特定条件下退出,最直接的办法就是设一个boolean标志,并通过设置这个标志来控制循环是否退出。由于是多线程处理,所以最好加上vilatile,以此保证变量可见性。这种方法适合于那种可以不断地去查询变量状态的情况。但是如果线程被某些操作长时间锁住,如IO阻塞,则无法响应线程的停止请求,这个时候可以使用线程中断的另一机制:Interrupt。

(2)使用Thread.stop方法强行终止线程(这个方法不推荐使用)

(3)使用interrupt方法中断线程。

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

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

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

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

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

interruptedisInterrupted区别

1)interrupted()是类的静态方法,测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能。

2)isInterrupted()是类的实例方法,测试Thread对象是否已经是中断状态,但不清除状态标志。

补充

1)终止处于“阻塞状态”的线程

通常,我们通过“中断”方式终止处于“阻塞状态”的线程。当线程由于被调用了sleep,

wait, join等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:

public void run() {

try {

while (true) {

//执行任务...

}

} catch (InterruptedExceptionie) {

//由于产生InterruptedException异常,退出while(true)循环,线程终止!

}

}

说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!

注意:对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理。形式如下:

public void run() {

while (true) {

try {

//执行任务...

} catch(InterruptedException ie) {

// InterruptedException在while(true)循环体内。

//当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出

break;

}

}

}

说明:上面的InterruptedException异常的捕获在whle(true)之内。当产生InterruptedException异常时,被catch处理之外,仍然在while(true)循环体内;要退出while(true)循环体,需要额外的执行退出while(true)的操作。

2)终止处于“运行状态”的线程

通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。

通过“中断标记”终止线程。

public void run() {

while (!isInterrupted()) {

//执行任务...

}

}

说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。

注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

通过“额外添加标记”。

private volatile boolean flag= true;

protected void stopTask() {

flag = false;

}

@Override

public void run() {

while (flag) {

//执行任务...

}

}

说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。

综合线程处于“阻塞状态”和“运行状态”的终止方式,通用终止线程的形式如下:

public void run() {

try {

// 1. isInterrupted()保证,只要中断标记为true就终止线程。

while (!isInterrupted()) {

//执行任务...

}

} catch (InterruptedExceptionie) {

// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。

}

}

推荐阅读更多精彩内容