第14章 多线程
在学习这一章之前,有几个要点
.什么是线程 .线程安全的集合
.中断线程 .Callable与Future
.线程状态 .执行器
.线程属性 .同步器
.同步 .线程与Swing
.阻塞队列
什么是线程?
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常,每一个任务称为一个线程( thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序( multithreaded)。
那么,多进程与多线程有哪些区别呢?本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。这听起来似乎有些风险,的确也是这样,在本章稍后将可以看到这个问题。然而,共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。
中断线程?
1.stop方法,现在已经被弃用了
2.interrupt方法
向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个 sleep调用阻塞,那么, Interruptedexception异常被抛出。
可以调用isInterruptd方法进行检测是否有请求中止,调用不改变中断状态。
调用interrupted方法,测试 当前线程(即正在执行这一命令的线程)是否中断,会将当前线程中断状态更改为false.。
在使用线程的时候可以调用Thread.currentThread()获取当前线程。
注意:没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后,继续执行,而不理会中断。但是,更普遍的情况是,线程将简单地将中断作为一个终止的请求。
线程状态?
线程可以有如下6中状态:
New(新创建)
Runnable(可运行)
Blocked(被阻塞)
Waiting(等待)
Timed waiting(计时等待)
Termminated(被终止)
要确定一个线程的当前状态,课调用getState方法。
>New(新创建):
当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行。
>Runnable(可运行):
一旦调用 start方法,线程处于 runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。(Java的规范说明没有将它作为一个单独状态。一个正在运行中的线程仍然处于可运行状态。旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会(见图14-4)。当选择下一个线程时,操作系统考虑线程的优先级—更多的内容见第144.1节。
>Blocked(被阻塞)
>Waiting(等待)
>Timed waiting(计时等待)
1).当一个线程试图获取一个内部的对象锁(而不是 java util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态(我们在第14.53节讨论 javautil. concurrent锁,在第14.55节讨论内部对象锁)。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
2).当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。我们在第14.5.4节来讨论条件。在调用 Object. wait方法或 Thread, join方法,或者是等待javautil. concurrent库中的Lock或 Condition时,就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。
3).有几个方法有一个超时参数。调用它们导致线程进入计时等待( timed waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread. sleep和 Object. wait、 Thread join、 Lock. try Lock以及 Condition. await的计时版。
>Termminated(被终止)
1).因为run方法正常退出而自然死亡。·
2.因为一个没有捕获的异常终止了run方法而意外死亡
Thread类的API
//等待终止指定的线程
void join()
//等待指定的线程死亡或者经过指定的毫秒数
void join(long millis)
//得到这以线程的状态;NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING或TERMINATED之一。
Thread.State getState() 5.0
//停止该线程。这一方法已过时。
void stop()
//暂停这一线程的执行。这一方法已过时。
void susped()
//恢复线程。这一方法仅仅在调用susped()之后调用。这一方法已过时。
线程属性?
下面将讨论线程的各种属性,其中包括:线程优先级、守护线程、线程组以及处理未捕获异常的处理器。
>线程优先级
线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
例如, Windows有7个优先级别。一些Java优先级将映射到相同的操作系统优先级。在Sun为 Linux提供的Java虚拟机,线程的优先级被忽略——所有线程具有相同的优先级。
初级程序员常常过度使用线程优先级。为优先级而烦恼是事出有因的。不要将程序构建为功能的正确性依赖于优先级。
Thread类API
//设置线程的优先级。优先级必须在 Thread. MIN PRIORITY与 Thread. MAX PRIORITY之间。一般使用 Thread. NORM PRIORITY优先级。
void setPriority (int newpriority)
//线程的最小优先级。最小优先级的值为1。
static int MIN_PRIORITY
//线程的默认优先级。默认优先级为5。
static int NORM_PRIORITY
//线程的最高优先级。最高优先级的值为10。
static int MAX_PRIORITY
//导致当前执行线程处于让步状态如果有其他的向运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度a注意,这是一个静态方法。
static void yield()
守护线程
t.setDaemon(true);
将线程转换为守护线程( daemon thread)。这样一个线程没有什么神奇。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定田时地发送“i计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
守护线程有时会被初学者错误地使用,他们不打算考虑关机( shutdown)动作。但是,这是很危险的。守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
>线程组以及处理未捕获异常的处理器
线程的run方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止。在这种情况下,线程就死亡了。但是,不需要任何 catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现 Thread. Uncaught Exceptionhandler接口的类。这个接口只有一个方法。
void uncaughtException(Thread t,Throwable e);
可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用 Thread类的静态方法 setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup对象。
注释:线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。
ThreadGroup类实现 Thread.UncaughtExceptionHandler接口。
同步?
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生什么呢?可以想象,线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生讹误的对象。这样一个情况通常称为竞争条件 (race condition)。
>锁对象
有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized关键字达到这一目的,并且 Java Se5.0引入了 Reentrant Lock类。 synchronized关键字自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的。但是,我们相信在读者分别阅读了锁和条件的内容之后,理解 synchronized关键字是很轻松的事情。 java. util.concurrent框架为这些基础机制提供独立的类,在此以及第14.54节加以解释这个内容。读者理解了这些构建块之后,将讨论第14.5.5节。用 Reentrantlock保护代码块的基本结构如下:
myLock.lock(); try{ }finally{ myLock.unlock(); }
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
注释:如果使用锁,就不能使用带资源的try语句。首先,解锁方法名不是 close。不过,即使将它重命名,带资源的try语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,你可能想使用多个线程共享的那个变量(而不是新变量)。
注意:每一个Bank对象有自己的 Reentrantlock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每个线程得到不同的锁对扌象,两个线程都不会发生阻塞。本该如此,因为线程在操纵不同的Bank实例的时候,线程之间不会相互影响。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(holdcount)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用 unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
/* *class: java.util.concurrent.locks.Lock */ //获得这个锁;如果锁同时被另一个线程拥有则发生阻塞。 void lock(); //释放这个锁。 void unlock(); /* *class: java.util.concurrent.locks.ReentrantLock */ //构建一个可以被用来保护临界区的可重入锁。 ReentrantLock(); //构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程,这一公平的保证将大大降低性能。所以,在默认情况下,锁没有被强制为公平的。 ReentrantLock(boolean fair);
>条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
一个锁对象可以有一个或多个相关的条件对象。
class Bank{ private Condition sufficientFunds; public Bank(){ sufficientFunds = bankLock.newCondition(); } }
如果当前线程不满足执行条件则sufficientFunds.await(),当前线程现在被阻塞了,并放弃了锁。
等待获得锁的线程和调用 await方法的线程存在本质上的不同。一旦一个线程调用 await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 signalAll方法时为止。
此时,线程应该再次测试该条件。由于无法确保该条件被满足— signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
至关重要的是最终需要某个其他线程调用 signalAll方法。当一个线程调用 await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock)现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
>synchronized关键字
在前面一节中,介绍了如何使用Lock和 Condition对象。在进一步深入之前,总结一下有关锁和条件的关键之处:
- ·锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- ·锁可以管理试图进入被保护代码段的线程。
- ·锁可以拥有一个或多个相关的条件对象。
- ·每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock和 Condition接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用 synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中, notifyAll /notify方法解除等待线程的阻塞状态。
内部锁和条件存在一些局限。包括
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件,可能是不够的。
>同步阻塞
每个java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized(obj){ //code }
有时程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定( clientside locking)。
>监视器的概念
锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器( monitor),这一概念最早是由 Per Brinch Hansen和 Tony Hoare在20世纪70年代提出的。用Java的术语来讲,监视器具有如下特性:
- 监视器是只包含私有域的类。
- 每个监视器类的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj. method),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
- 该锁可以有任意多个相关条件。
Java设计计者以不是很精确的方式采用了监视器概念,Java中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized关键字声明,那么,它表现的就像是一个监视器方法。通过调用wait/ notify All/notify来访问条件变量。
然而,在下述的3个方面Java对象不同于监视器,从而使得线程的安全性下降
- 域不要求必须是 private
- 方法不要求必须是 synchronized
- 内部锁对客户是可用的。
>Volatile域
注释: Brian goetz给出了下述“同步格言”:“如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步”。
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
警告: Volatile变量不能提供原子性
https://blog.csdn.net/shenmegui_32/article/details/70153821
被volatile修饰的变量可以保证该变量的可见性,但并不能保证操作的原子性。
当需要使用被volatile修饰的变量时,线程会从主内存中重新获取该变量的值,但当该线程修改完该变量的值写入主内存的时候,并没有判断主内存内该变量是否已经变化,故可能出现非预期的结果。如主内存内有被volatile修饰变量 a,值为3,某线程使用该变量时,重新从主存内读取该变量的值,为3,然后对其进行+1操作,此时该线程内a变量的副本值为4。但此时该线程的时间片时间到了,等该线程再次获得时间片的时候,主存内a的值已经是另外的值,如5,但是该线程并不知道,该线程继续完成其未完成的工作,将线程内的a副本的值4写入主存,这时,主存内a的值就是4了。这样,之前修改a的值为5的操作就相当于没有发生了,a的值出现了意料之外的结果。
被synchronize修饰的变量则可以保证变量操作的原子性,因为当某线程使用变量a时,其他线程无法使用变量a,只能等该线程对a操作结束,释放a的锁后才能对a进行操作。
>final变量
还有一种情况可以安全地访问一个共享域,即这个域声明为 final时。考虑以下声明:final Map<String, Double> accounts = new HashMap()其他线程会在构造函数完成构造之后才看到这个 accounts变量。如果不使用 final,就不能保证其他线程看到的是 accounts更新后的值,它们可能都只是看到null,而不是新构造的 HashMap当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。
>原子性
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
java.util. concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如, AtomicInteger类提供了方法 icrementAndget和 decrementAdget,它们分别以原子方式将一个整数自增或自减。可以安全地使用Atomicinteger作为共享计数器而无须同步。另外这个包中还包含 Atomicboolean、 Atomiclong和 Atomicreference以及 Boolean值、整数、long值和引用的原子数组。应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。