现在可能有单台拥有多个CPU的计算机,但是,并发执行的进程数目并不是由CPU数目限制的。操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉。
多线程程序在较低的层次上拓展了多任务的概念:一个程序同时执行多个任务。通常,每一个任务称为一个线程,它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序。
那么多进程与多线程有哪些区别呢?本质的区别在于每个进程里拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通讯比进程之间的通讯更有效、更容易。此外,在有些操作系统中,与进程比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。
(1)线程状态
线程可以有6中状态:
*New(新创建)
*Runable(可运行)
*Blocked(被阻塞)
*Waiting(等待)
*Timed Waiting(计时等待)
*Terminated(被终止)
-
新创建线程
当用new操作符创建一个新线程时,如new Thread(() -> {})
,该线程还没有开始运行。这意味着它的状态时new
。当一个线程处于新创建时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。 -
可运行线程
一旦调用start
方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。(Java的规范说明没有将它作为一个单独的状态。一个正在运行中的线程仍然处于可运行状态。)
一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级
。
现在所有的桌面以及服务器操作系统都要使用抢占式调度。但是,像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用yield方法、或者被阻塞或等待时,线程才失去控制权。
在具有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当然,如果线程数目多于处理器的数目,调度器依然采用时间片机制。
在任何给定的时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。 -
被阻塞线程和等待线程
当线程处于被阻塞或等待时,它暂时不活动。它不运行任何代码且消耗最少的资源。知道线程调度器诚信激活它时。细节取决于它是怎样达到非活跃状态的。
*当一个线程试图获取内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
*当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait
方法或Thread.join
方法,或者是等待java.util.concurrent
库中的Lock
或Condition
时,就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。
*有几个方法有一个超时参数。调用它们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep
和Object.wait、Thread.join、Lock.tryLock
以及Condition.wait
的计时版。 -
被终止的线程
线程因如下两个原因之一而被终止:
*因为run
方法正常退出而自然死亡。
*因为一个没有捕获的异常终止了run
方法而意外死亡。
void join()
等待终止指定的线程
void join(long millis)
等待指定的线程死亡或者经过指定的毫秒数
Thread.state getState()
5.0,得到这一线程的状态;New、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING
或TERMINATED
之一。
void stop();
停止该线程。这一方法已弃用。
void suspend()
; 暂停这一线程的执行。这一方法已弃用。
void resume()
;恢复线程。这一方法仅仅在调用supsend()
之后调用。这一方法已弃用。
(2)线程属性
下面将讨论线程的各种属性,其中包括:线程优先级、守护线程、线程组以及处理未捕获异常的处理。
-
线程优先级
在Java
程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它父线程的优先级。可以用setPriority
方法提高或降低任何一个线程的优先级。可以将优先级设置为MIN_PRIORITY
(在Thread
类中定义为1)与MAX_PRIORITY
(定义为10)之间的任何值。NORM_PRIORITY
定义为5。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java
线程的优先级被映射到宿主平台的优先级上,优先级的个数也许更多或者更少。
例如,Window有7个优先级别。一些Java
优先级映射到相同的操作系统优先级。在Oracle为Linux提供的JVM中,线程优先级被忽略–所有线程具有相同优先级。初级程序员常常过度使用线程优先级。为优先级而烦恼是事出有因的。不要讲程序构建为功能的正确性依赖于优先级。
如果确实要使用优先级,应该避免一个常犯的错误。如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死
void setPriority(int newPriority)
;设置线程的优先级。优先级必须在Thread.MIN_PRIORITY
与Thread.MAX_PRIORITY
之间。一般使用Thread.NORM_PRIORITY
优先级。
static int MIN_PRIORITY
;线程的最小优先级。最小优先级的值为1.
static int NORM_PRIORITY
;线程的默认优先级。默认优先级为5.
static int MAX_PRIORITY
;线程的最高优先级。最高优先级的值为10.
static void yield()
;导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。 -
守护线程
可以通过调用t.setDaemon(true)
;将线程换为守护线程。这样一个线程没有什么神奇。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器滴答”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。 -
未捕获异常处理器
线程的run方法不能抛出任何受查对象,但是,非受查异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须属于一个实现Thread.UncaughtExceptionHandler
接口的类。这个接口只有一个方法:void uncaughtException(Thread t, Throwable e)
可以用
setUncaughtExceptionHandler
方法为任何线程安装一个处理器。也可以用Thread
类的静态方法setDefaultUncaughtExceptionHandler
为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup
对象。
线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组
ThreadGroup类实现Thread.UncaughtExceptionHandler
接口。它的uncaughtException
方法做如下操作:
1)如果该线程组有父线程组,那么父线程组的uncaughtException
方法被调用。
2)否则,如果Thread.getDefaultExceptionHandler
方法返回一个非空的处理器,则调用该处理器。
3)否则,如果Throwable是ThreadDeath的一个实例,什么都不做。
4)否则,线程的名字以及Throwable的栈轨迹被输出到System.err上。
(3)同步
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对统一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生什么呢?可以想象,线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生讹误的对象。这样一个情况通常称为竞争条件。
-
竞争条件的一个例子
为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。
在下面的测试程序中,模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱款的交易。每一个账户有一个线程。每一笔交易中,会从线程所服务的账户中随机转移一定数目的钱款到另一个账户(还没有考虑账户的余额)。public class UnsynchBankTest { public static final int NACCOUNTS = 100; public static final double INITIAL_BALANCE = 1000; public static final double MAX_AMOUNT = 1000; public static final int DELAY = 10; public static void main(String[] args) { Bank bank new Bank(NACCOUNTS, INITIAL_BALANCE); for(int i = 0; i < NACCOUNTS; i ++) { int fromAccount = i; Runable r = () -> { try { while(true) { int toAccount = (int)(bank.size() * Math.random()); double amount = MAX_AMOUNT * Math.random(); bank.transfer(fromAccount, toAccount, amount) ; Thread.sleep((int)(DELAY * Math.randow())); } } catch(InterruptedException e) { } }; Thread t = new Thread(r); t.start(); } } } public class Bank { private final double[] accounts; public Bank(int n, double initialBalance) { accounts = new double[n]; Arrays.fill(accounts, initialBalance); } public void transfer(int from, int to, double amount) { if(accounts[from] < amount) return; System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); } public double getTotalBalance() { double sum = 0; for(double a : accounts) sum += a; return sum; } public int size() { return accounts.length; } }
-
竞争条件详解
上例中讲到几个线程更新银行账户余额。一段时间后,错误不知不觉的出现了,总额要么增加,要么减少。当两个线程试图同时更新同一个账户的时候,这个问题就出现了。假定两个线程同时执行指令。
accounts[to] += amount;
问题在于这不是原子操作。该指令可能被处理如下:
1)将account[to]加载到了寄存器。
2)增加amount。
3)将结果写回account[to]。
现在,假定第1个线程执行步骤1和2,然后,它剥夺了运行权。假定第2个线程被唤醒并修改了accounts数组中的同一项。然后,第1个线程被唤醒并完成其第3步。
这样,这一动作就擦去了第二个线程所做的更新。于是,总金额不在正确。 -
锁对象
有两种机制防止代码块受并发访问的干扰。Java
语言提供一个synchronized
关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock
类。Synchronized
关键字自动提供一个锁以及相关的条件,对于大多数需要显式锁的情况,这是很便利的。myLock.lock(); // a ReentrantLock object try { critical section } finally { myLock.unlock(); //make sure the lock is unlocked even if an exception is thrown }
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,他们被阻塞,直到第一个线程释放锁对象。
把解锁操作括在finally子句之内是至关重要的。如果把临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
锁是可重入的,因为线程可以重复地获取已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
例如,transfer方法调用getTotalBalance
方法,这也会封锁bankLock对象,此时bankLock对象的持有计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为0。线程释放锁。
通常,可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同对象。
听起来公平锁更合理一些,但是使用公平锁比使用常规锁要慢很多。只有当你确实了解自己要做什么并且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。即使使用公平锁,也无法确保线程调度是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,嘛呢就没有机会公平地处理这个锁了。 -
条件对象
通常,线程进入临界区,却发现在某一条件满足之后才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
现在来细化银行的模拟程序。我们避免选择没有足够资金的账户作为转出账户。则不能使用下面的代码:if(bank.getBalance(from) >= amount) { bank.transfer(from, to, amount); } //当前线程完全有可能在成功地返回测试,且在调用transfer方法之前将被中断。 if(bank.getBalance(from) >= amount) { //thread might be deactive at this point bank.transfer(from, to, amount); }
在线程再次运行前,账户余额可能已经低于提款金额。必须确保没有其他线程在本检测余额与账户活动之间修改余额。公国使用锁来保护检查与转账动作来做到这点:
public void transfer(int from, int to, int amount) { bankLock.lock(); try { while(accounts[from] < amount) { //wait ... } //transfer funds ... } finally { bankLock.unlock(); } }
现在,当账户中没有足够余额时,当前线程会等待另一个线程向账户中注入资金。但是,这一线程刚刚获得了对bankLock的排他性访问,因此别的线程没有机会进行存款操作。这就是为什么我们要条件对象的原因。
一个锁对象可以有一个或多个相关的条件对象。你可以用欧冠newCondition方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如,在此设置一个条件对象来表达“余额满足”条件:class Bank { private Condition sufficientFunds; ... public Bank() { ... sufficientFunds = bankLock.newCondition(); } } //如果transfer方法发余额不足,它调用 sufficientFunds.await();
当前线程现在被阻塞了,并且放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,知道另一个线程调用同一条件上的signalAll方法时为止。当另一个线程转账时,它应该调用sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集移除时,他们再次成为可运行的,调度器将再次激活他们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从阻塞的地方继续执行。
此时,线程应该再次测试条件。由于无法确保该条件被满足–signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去监测该条件。
通常,对await的调用应该在如下形式的循环中:while(!(ok to proceed)) condition.await();
至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,他没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致情人不快的死锁现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用了await方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么程序就挂起了。
应该何时调用signalAll呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用signalAll。例如,当一个账户余额发生改变时,等待线程会应该有机会检查余额。在例子中,当完成了转账时,调用signalAll方法。public void transfer(int from, int to, int amount) { bankLock.lock(); try { while(accounts[from] < amount) { sufficientFunds.await(); // transfer funds ... sufficientFunds.signalAll(); } finally { bankLock.unlock(); } } }
注意调用
signalAll
不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对象的访问。
另一个方法singal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果米有其他线程再次调用signal,那么系统就死锁了。
***当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signalAll或者signal方法。
*Condition newCondition()
返回一个与该锁相关的条件对象
*void await()
将该线程放到条件的等待集中。
*viod signalAll()
解除该条件的等待集中的所有线程的阻塞状态
*void signall()
从该条件的等待集中随机选择一个线程,解除其阻塞状态。 -
synchronized关键字
锁和条件的关键之处:
*锁可以用来保护代码片段,任何时候只能有一个线程执行被保护的代码
*锁可以管理试图进入被保护代码段的线程。
*锁可以拥有一个或多个相关的条件对象。
*每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。换句话说:public synchronized void method() { //method body } //等价于 public void method() { this.intrinsicLock.lock(); try { //method body } finally {this.intrinsicLock.unlock();} }
例如,可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个现实的锁。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notiyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait
或notifyAll
等价于:intrinsicCondition.await()
和intrinsicCondition.signalAll()
用synchronized实现Bank类:class Bank { private double[] accounts; public synchronized void transfer(int from, int to, int amount) throws InterruptedException { while(accounts[from] < amount) { wait(); } accounts[from] -= amount; accounts[to] += amount; notifyAll(); // notify all threads waitting on the condition } public synchronized double getTotalBalance() { ... } }
可以看到,使用synchronized关键字来编写代码要简洁的多。当然,要理解这一代码,你必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些视图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的或者任何其他的同步静态方法。
内部锁和条件存在一些局限。包括:
*不能中断一个正在试图获得锁的线程。
*试图获得锁是不能设定超时
*每个锁仅有单一的条件,可能是不够的。
在代码中应该使用哪一种?Lock和Condition对象还是同步方法?下面是一些建议:
*最好既不使用Lock/Condition也不实用synchronized
关键字。在许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
*如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写代码的数量,减少出错几率。
*如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition
*void notifyAll()
解除哪些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出异常
*void notify()
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者,该方法抛出异常。
*void wait()
导致线程进入等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,抛出异常。
*void wait()
*void wait(long millis)
*void wait(long millis, int nanos)
导致线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象的持有者,抛出异常。 -
同步阻塞
正如刚刚讨论的,每一个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:synchronized(obj)//this is the syntax for a synchronized block { critical section }
于是它获得obj的锁。
有时会发现“特殊的”锁,例如:public class Bank { private double[] accounts; private Object lock = new Object(); ... public void transfer(int from, int to, int amount) { synchronized(lock)//an ad-hoc lock accounts[from] -= amount; accounts[to] += amoutn; } }
在此,lock对象被创建仅仅是用来使用每个Java对象持有的锁。
有时程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定。例如,考虑Vector类,一个列表,它的方法是同步的。现在,假定在Vector中存储银行卡余额。这里有一个transfer方法的原始实现:
public void transfer(Vector accounts, int from, int to, int amount)// Error
{
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
System.out.println(…);
}
Vector类的get和set方法是同步的,但是,这对于我们并没有什么帮助。在第一次对get的调用已经完成之后,一个线程完全可能在transfer方法中剥夺运行权。于是,另一个线程可能在相同的存储位置存入不同的值。但是,我们可以截获这个锁:public void transfer(Vector<Double> accounts, int from, int to, int amount) { synchronized (accounts) { accounts.setCfron, accounts.get(from) - amount): accounts.set(to, accounts.get(to) + amount); } Systen.out.print1n(...); }
-
Volatile域
有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大。
*多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
*编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变,然而,内存中的值可以被另一个线程改变。
如果你使用锁来保护可以被多个线程访问的代码,那么可以不用考虑这种问题。编译器被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令。
如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。
volatile关键字为实例域的同步提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发更新的。
例如,假定一个对象有一个布尔标记done,它的值被一个线程设置确被另一个线程查询,如同我们讨论过的那样,你可以使用锁:private boolean done; public synchronized boolean isDone() {return done;} public synchronized void setDone() {done = true};
或许使用内部锁不是一个好主意。如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。如果注意到这个方面,一个线程可以为这一变量使用独立的Lock。但是,这也会带来许多麻烦。
在这种情况下,将域声明为volatile是合理的:private volatile boolean done; public boolean isDone() {return done}; public viod setDone() {done = true};
volatile不适合复合操作
-
final变量
还有一种情况可以安全访问一个共享域,即这个域名为final时:final Map<String, Double> accounts = new HashMap<>();
其他线程会在构造函数完成构造之后才能看到这个accounts变量。
如果不使用