线程
线程
目 录
1简介
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程
适用范围
典型的应用
1.服务器中的文件管理或通信控制
2.前后台处理
3.异步处理
特点
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
3)可并发执行。
4)共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
2概念
多线程
一般来说,我们把正在计算机中执行的程序叫做"进程"(Process),而不将其称为程序(Program)。所谓"线程"(Thread),是"进程"中某个单一顺序的控制流。新兴的操作系统,如Mac,Windows NT,Windows 95等,大多采用多线程的概念,把线程视为基本执行单位。线程也是Java中的相当重要的组成部分之一。
甚至最简单的Applet也是由多个线程来完成的。在Java中,任何一个Applet的paint()和update()方法都是由AWT(Abstract Window Toolkit)绘图与事件处理线程调用的,而Applet 主要的里程碑方法——init(),start(),stop()和destory() ——是由执行该Applet的应用调用的。
单线程的概念没有什么新的地方,真正有趣的是在一个程序中同时使用多个线程来完成不同的任务。某些地方用轻量进程(Lightweig ht Process)来代替线程,线程与真正进程的相似性在于它们都是单一顺序控制流。然而线程被认为轻量是由于它运行于整个程序的上下文内,能使用整个程序共有的资源和程序环境。
线程与进程的比较
另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
发生进程切换与发生线程切换时相比较,进程切换时涉及到有关资源指针的保存以及地址空间的变化等问题;线程切换时,由于同进程内的线程共享资源和地址 空间,将不涉及资源信息的保存和地址变化问题,从而减少了操作系统的开销时间。而且,进程的调度与切换都是由操作系统内核完成,而线程则既可由操作系统内 核完成,也可由用户程序进行。图1 多线程与进程之间的关系
线程和进程的区别在于,子进程和父进程有相同的代码段,不同的数据段,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。线程的运行中需要使用计算机的内存资源和CPU。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。
线程与进程的区别可以归纳为以下4点:
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
3守护线程
守护线程是特殊的线程,一般用于在后台为其他线程提供服务.
Java中,isDaemon():判断一个线程是否为守护线程.
Java中,setDaemon():设置一个线程为守护线程.
C# 守护线程
类1:守护线程类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/** * 本线程设置了一个超时时间 * 该线程开始运行后,经过指定超时时间, * 该线程会抛出一个未检查异常通知调用该线程的程序超时 * 在超时结束前可以调用该类的cancel方法取消计时 * @author solonote */ public class TimeoutThread extends Thread{ /** * 计时器超时时间 */ private long timeout; /** * 计时是否被取消 */ private boolean isCanceled = false ; /** * 当计时器超时时抛出的异常 */ private TimeoutException timeoutException; /** * 构造器 * @param timeout 指定超时的时间 */ public TimeoutThread( long timeout,TimeoutException timeoutErr) { super (); this .timeout = timeout; this .timeoutException = timeoutErr; //设置本线程为守护线程 this .setDaemon( true ); } /** * 取消计时 */ public synchronized void cancel() { isCanceled = true ; } /** * 启动超时计时器 */ public void run() { try { Thread.sleep(timeout); if (!isCanceled) throw timeoutException; } catch (InterruptedException e) { e.printStackTrace(); } } } |
4线程的同步
线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源、什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些原则问题需要考虑,是否有竞争资源被同时改动的问题?对于同步,在具体的Java代码中需要完成以下两个操作:把竞争访问的资源标识为private;同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。当然这不是唯一控制并发安全的途径。synchronized关键字使用说明synchronized只能标记非抽象的方法,不能标识成员变量。为了演示同步方法的使用,构建了一个信用卡账户,起初信用额为100w,然后模拟透支、存款等多个操作。显然银行账户User对象是个竞争资源,而多个并发操作的是账户方法oper(int x),当然应该在此方法上加上同步,并将账户的余额设为私有变量,禁止直接访问。
5工作原理
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程不拥有系统资源,只有运行必须的一些数据结构;它与父进程的其它线程共享该进程所拥有的全部资源。线程可以创建和撤消线程,从而实现程序的并发执行。一般,线程具有就绪、阻塞和运行三种基本状态。
在多中央处理器的系统里,不同线程可以同时在不同的中央处理器上运行,甚至当它们属于同一个进程时也是如此。大多数支持多处理器的操作系统都提供编程接口来让进程可以控制自己的线程与各处理器之间的关联度(affinity)。
有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。
进程可以支持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。
6线程属性
线程体
所有的操作都发生在线程体中,在Java中线程体是从Thread类继承的run()方法,或实现Runnable接口的类中的run()方法。当线程产生并初始化后,实时系统调用它的run()方法。run()方法内的代码实现所产生线程的行为,它是线程的主要部分。
线程状态
附图表示了线程在它的生命周期内的任何时刻所能处的状态以及引起状态改变的方法。这图并不是完整的有限状态图,但基本概括了线程中比较感兴趣和普遍的方面。以下讨论有关线程生命周期以此为据。
●新线程态(New Thread)
产生一个Thread对象就生成一个新线程。当线程处于"新线程"状态时,仅仅是一个空线程对象,它还没有分配到系统资源。因此只能启动或终止它。任何其他操作都会引发异常。例如,一个线程调用了new方法之后,并在调用start方法之前的处于新线程状态,可以调用start和stop方法。
●可运行态(Runnable)
start()方法产生运行线程所必须的资源,调度线程执行,并且调用线程的run()方法。在这时
线程处于可运行态。该状态不称为运行态是因为这时的线程并不总是一直占用处理机。特别是对于只有一个处理机的PC而言,任何时刻只能有一个处于可运行态的线程占用处理 机。Java通过调度来实现多线程对处理机的共享。注意,如果线程处于Runnable状态,它也有可能不在运行,这是因为还有优先级和调度问题。
当以下事件发生时,线程进入非运行态。
①suspend()方法被调用;
②sleep()方法被调用;
③线程使用wait()来等待条件变量;
④线程处于I/O请求的等待。
●死亡态(Dead)
当run()方法返回,或别的线程调用stop()方法,线程进入死亡态。通常Applet使用它的stop()方法来终止它产生的所有线程。
线程的本操作:
派生:线程在进程内派生出来,它即可由进程派生,也可由线程派生。
阻塞(Block):如果一个线程在执行过程中需要等待某个事件发生,则被阻塞。
图2 线程的状态与操作
线程的另一个执行特性是同步。线程中所使用的同步控制机制与进程中所使用的同步控制机制相同。
线程优先级
虽然我们说线程是并发运行的。然而事实常常并非如此。正如前面谈到的,当系统中只有一个CPU时,以某种顺序在单CPU情况下执行多线程被称为调度(scheduling)。Java采用的是一种简单、固定的调度法,即固定优先级调度。这种算法是根据处于可运行态线程的相对优先级来实行调度。当线程产生时,它继承原线程的优先级。在需要时可对优先级进行修改。在任何时刻,如果有多条线程等待运行,系统选择优先级最高的可运行线程运行。只有当它停止、自动放弃、或由于某种原因成为非运行态低优先级的线程才能运行。如果两个线程具有相同的优先级,它们将被交替地运行。 Java实时系统的线程调度算法还是强制性的,在任何时刻,如果一个比其他线程优先级都高的线程的状态变为可运行态,实时系统将选择该线程来运行。一个应用程序可以通过使用线程中的方法setPriority(int),来设置线程的优先级大小。
线程中的join()可以用来邀请其他线程先执行(示例代码如下):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package org.thread.test; public class Join01 implements Runnable { public static void main(String[] args) { for ( int i = 0 ; i < 20 ; i++) { if (i == 5 ) { Join01 j = new Join01(); Thread t = new Thread(j); t.setName( "被邀请先执行的线程." ); t.start(); try { // 邀请这个线程,先执行 t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println( "没被邀请的线程。" + (i + 1 )); } } public void run() { for ( int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName() + (i + 1 )); } } } |
yield()告诉系统"把自己的CPU时间让掉,让其他线程或者自己运行",示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package org.thread.test; public class Yield01 { public static void main(String[] args) { YieldFirst yf = new YieldFirst(); YieldSecond ys = new YieldSecond(); YieldThird yt = new YieldThird(); yf.start(); ys.start(); yt.start(); } } class YieldFirst extends Thread { @Override public void run() { for ( int i = 0 ; i < 10 ; i++) { System.out.println( "第一个线程第" + (i + 1 ) + "次运行." ); // 让当前线程暂停 yield(); } } } class YieldSecond extends Thread { @Override public void run() { for ( int i = 0 ; i < 10 ; i++) { System.out.println( "第二个线程第" + (i + 1 ) + "次运行." ); // 让当前线程暂停 yield(); } } } class YieldThird extends Thread { @Override public void run() { for ( int i = 0 ; i < 10 ; i++) { System.out.println( "第三个线程第" + (i + 1 ) + "次运行." ); // 让当前线程暂停 yield(); } } } |
幽灵线程
任何一个Java线程都能成为幽灵线程。它是作为运行于同一个进程内的对象和线程的服务提供者。例如,HotJava浏览器有一个称为" 后台图片阅读器"的幽灵线程,它为需要图片的对象和线程从文件系统或网络读入图片。 幽灵线程是应用中典型的独立线程。它为同一应用中的其他对象和线程提供服务。幽灵线程的run()方法一般都是无限循环,等待服务请求。
线程组
每个Java线程都是某个线程组的成员。线程组提供一种机制,使得多个线程集于一个对象内,能对它们实行整体操作。譬如,你能用一个方法调用来启动或挂起组内的所有线程。Java线程组由ThreadGroup类实现。
7多线程
对于多线程的好处这就不多说了。但是,它同样也带来了某些新的麻烦。只要在设计程序时特别小心留意,克服这些麻烦并不算太困难。在生成线程时必须将线程放在指定的线程组,也可以放在缺省的线程组中,缺省的就是生成该线程的线程所在的线程组。一旦一个线程加入了某个线程组,不能被移出这个组。
同步线程
许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态。这就需要同步机制。在Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。它由高层的结构隐式实现,来保证操作的对应。(然而,我们注意到Java虚拟机提供单独的monito renter和monitorexit指令来实现lock和unlo
ck操作。) synchronized语句计算一个对象引用,试图对该对象完成锁操作,并且在完成锁操作前停止处理。当锁操作完成synchronized语句体得到执行。当语句体执行完毕(无论正常或异常),解锁操作自动完成。作为面向对象的语言,synchronized经常与方法连用。一种比较好的办法是,如果某个变量由一个线程赋值并由别的线程引用或赋值,那么所有对该变量的访问都必须在某个synchromized语句或synchronized方法内。
现在假设一种情况:线程1与线程2都要访问某个数据区,并且要求线程1的访问先于线程2,则这时仅用synchronized是不能解决问题的。这在Unix或Windows NT中可用Simaphore来实现。而Java并不提供。在Java中提供的是wait()和notify()机制。使用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
synchronized method_1 ( /*……*/ ) { // called by thread 1. // access data area available= true ; notify(); } synchronized method_2( /*……*/ ){ //called by thread 2. while (!available) try { wait(); // wait for notify(). } catch (Interrupted Exception e){ } // access data area } |
其中available是类成员变量,置初值为false。
如果在method-2中检查available为假,则调用wait()。wait()的作用是使线程2进入非运行态,并且解锁。在这种情况下,method-1可以被线程1调用。当执行notify()后。线程2由非运行态转变为可运行态。当method-1调用返回后。线程2可重新对该对象加锁,加锁成功后执行wait()返回后的指令。这种机制也能适用于其他更复杂的情况。
死锁
如果程序中有几个竞争资源的并发线程,那么保证均衡是很重要的。系统均衡是指每个线程在执行过程中都能充分访问有限的资源。系统中没有饿死和死锁的线程。Java并不提供对死锁的检测机制。对大多数的Java程序员来说防止死锁是一种较好的选择。最简单的防止死锁的方法是对竞争的资源引入序号,如果一个线程需要几个资源,那么它必须先得到小序号的资源,再申请大序号的资源。
8优化
Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。而互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。所以需要进行对线程进行优化,提高效率。
轻量级锁
轻量级锁(Lightweight Locking)是从Java6开始引入的概念,本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。下面将详细介绍JVM如何利用CAS,实现轻量级锁。
Java Object Model中定义,Object Header是一个2字(1 word = 4 byte)长度的存储区域。第一个字长度的区域用来标记同步,GC以及hash code等,官方称之为 mark word。第二个字长度的区域是指向到对象的Class。在2个word中,mark word是轻量级锁实现的关键,其结构见右表。
从表中可以看到,state为lightweight locked的那行即为轻量级锁标记。bitfieds名为指向lock record的指针,这里的lock record,其实是一块分配在线程堆栈上的空间区域。用于CAS前,拷贝object上的mark word。第三项是重量级锁标记。后面的状态单词很有趣,inflated,译为膨胀,在这里意思其实是锁已升级到OS-level。一般我们只关注第二和第三项即可。lock,unlock与mark word之间的联系如右图所示。在图中,提到了拷贝object mark word,由于脱离了原始mark word,官方将它冠以displaced前缀,即displaced mark word(置换标记字)。这个displaced mark word是整个轻量级锁实现的关键,在CAS中的compare就需要用它作为条件。
在拷贝完object mark word之后,JVM做了一步交换指针的操作,即流程中第一个橙色矩形框内容所述。将object mark word里的轻量级锁指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用。lock record里的owner指针指向object mark word的作用是为了在接下里的运行过程中,识别哪个对象被锁住了。
最后一步unlock中,我们发现,JVM同样使用了CAS来验证object mark word在持有锁到释放锁之间,有无被其他线程访问。如果其他线程在持有锁这段时间里,尝试获取过锁,则可能自身被挂起,而mark word的重量级锁指针也会被相应修改。此时,unlock后就需要唤醒被挂起的线程。
偏向锁
Java偏向锁(Biased Locking)是Java 6引入的一项多线程优化。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。它与轻量级锁的区别在于,轻量级锁是通过CAS来避免进入开销较大的互斥操作,而偏向锁是在无竞争场景下完全消除同步,连CAS也不执行(CAS本身仍旧是一种操作系统同步原语,始终要在JVM与OS之间来回,有一定的开销)。所谓的无竞争场景,就是单线程访问带同步的资源或方法。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)。
偏向模式和非偏向模式,在mark word表中,主要体现在thread ID字段是否为空。
挂起持有偏向锁的线程,这步操作类似GC的pause,但不同之处是,它只挂起持有偏向锁的线程(非当前线程)。
在抢占模式的橙色区域说明中有提到,指向当前堆栈中最近的一个lock record(在轻量级锁中,lock record是进入锁前会在stack上创建的一份内存空间)。这里提到的最近的一个lock record,其实就是当前锁所在的stack frame上分配的lock record。整个步骤是从偏向锁恢复到轻量级锁的过程。
偏向锁也会带来额外开销。在JDK6中,偏向锁是默认启用的。它提高了单线程访问同步资源的性能。
但试想一下,如果你的同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。事实上,消除偏向锁的开销还是蛮大的。所以在你非常熟悉自己的代码前提下,大可禁用偏向锁 -XX:-UseBiasedLocking。
9分类
线程有两个基本类型:
10举例
SUN Solaris 2.3
用户级线程在调用核心服务时(如文件读写),需要“捆绑(bound)”在一个LWP上。永久捆绑(一个LWP固定被一个用户级线程占用,该LWP移到LWP池之外)和临时捆绑(从LWP池中临时分配一个未被占用的LWP)。
图3 用户线程、轻权进程和核心线程的关系
1.有关的C库函数
1
2
|
/* 创建用户级线程 */ int thr_create( void * stack_base, size_t stack_size, void * (*start_routine)( void *), void *arg, long flags, thread_t *new_thread_id); |
其中flags包括:THR_BOUND(永久捆绑),THR_NEW_LWP(创建新LWP放入LWP池),若两者同时指定则创建两个新LWP,一个永久捆绑而另一个放入LWP池。
2.有关的系统调用
在当前进程中创建LWP
1
|
int _lwp_create(ucontext_t * contextp, unsigned long flags,lwpid_t * new_lwp_id); |
构造LWP上下文
1
|
void _lwp_makecontext(ucontext_t * ucp, void (*start_routine)( void *), void * arg, void * private , caddr_t stack_base, size_t stack_size); |
注意:没有进行“捆绑”操作的系统调用
Linux
1.创建线程
1
|
int pthread_create(pthread_t * thread , const pthread_attr_t * attr, void * (*start_routine) ( void *), void * arg); |
2.等待线程
1
|
int pthread_join(pthread_t thread , void ** retval); |
Windows NT
NT线程状态
(5) 转换状态:线程在准备执行而其内核堆栈处于外存时,线程进入转换状态;当其内核堆栈调回内存,线程进入就绪状态。
NT线程的有关API
1.创建线程
1
|
HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId); |
2.结束本线程
1
|
VOID WINAPI ExitThread( DWORD dwExitCode); |
3.挂起指定的线程
1
|
DWORD WINAPI SuspendThread( HANDLE hThread ); |
4.恢复指定线程运行
1
|
DWORD WINAPI ResumeThread( HANDLE hThread); |
线程的组成
1)一组代表处理器状态的CPU寄存器中的内容
2)两个栈,一个用于当线程在内核模式下执行的时候,另一个用于线程在用户模式下执行的时候
5)有时候线程也有它们自己的安全环境,如果多线程服务器应用程序要模仿其客户的安全环境,则往往可以利用线程的安全环境。
11Java线程
1)最简单的情况是,Thread/Runnable的run()方法运行完毕,自行终止。
2)对于更复杂的情况,比如有循环,则可以增加终止标记变量和任务终止的检查点。
4)IO阻塞和等锁情况下需要通过特殊方式进行处理。
5)使用Future类的cancel()方法调用。
6)调用线程池执行器的shutdown()和shutdownNow()方法。
7)守护线程会在非守护线程都结束时自动终止。
8)Thread的stop()方法,但已不推荐使用。
概论 |
|
---|
方式 |
|
---|
理论 |
|
---|
元素 |
|
---|
协调 |
|
---|
编程 |
|
---|
APIs |
|
---|
问题 |
|
---|
- 参考资料