java并发简介

  有的时候因为数据的同时访问,致使服务器不堪重负,这个时候就须要高并发的介入,这篇文章将会从线程,控制,监视器三个大方面来简单地介绍java的并发。java

<!--原文请参考http://ifeve.com/java-concurrency-constructs/-->编程

线程

线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(好比文件句柄等)也能访问同一个进程所建立的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。数组

每一个程序都至少拥有一个线程-即做为Java虚拟机(JVM)启动参数运行在主类main方法的线程。在Java虚拟机初始化过程当中也可能启动其余的后台线程。这种线程的数目和种类因JVM的实现而异。然而全部用户级线程都是显式被构造并在主线程或者是其余用户线程中被启动。安全

这里对Thread类中的主要方法和属性以及一些使用注意事项做出总结。这些内容会在这本书(《Java Concurrency Constructs》)上进行进一步的讨论阐述。Java语言规范以及已发布的API文档中都会有更详细权威的描述。服务器

构造方法

Thread类中不一样的构造方法接受以下参数的不一样组合:多线程

  • 一个Runnable对象,这种状况下,Thread.start方法将会调用对应Runnable对象的run方法。若是没有提供Runnable对象,那么就会当即获得一个Thread.run的默认实现。
  • 一个做为线程标识名的String字符串,该标识在跟踪和调试过程当中会很是有用,除此别无它用。
  • 线程组(ThreadGroup),用来放置新建立的线程,若是提供的ThreadGroup不容许被访问,那么就会抛出一个SecurityException 。

Thread类自己就已经实现了Runnable接口,所以,除了提供一个用于执行的Runnable对象做为构造参数的办法以外,也能够建立一个Thread的子类,经过重写其run方法来达到一样的效果。然而,比较好的实践方法倒是分开定义一个Runnable对象并用来做为构造方法的参数。将代码分散在不一样的类中使得开发人员无需纠结于Runnable和Thread对象中使用的同步方法或同步块之间的内部交互。更广泛的是,这种分隔使得对操做的自己与其运行的上下文有着独立的控制。更好的是,同一个Runnable对象能够同时用来初始化其余的线程,也能够用于构造一些轻量化的执行框架(Executors)。另外须要提到的是经过继承Thread类实现线程的方式有一个缺点:使得该类没法再继承其余的类。并发

Thread对象拥有一个守护(daemon)标识属性,这个属性没法在构造方法中被赋值,可是能够在线程启动以前设置该属性(经过setDaemon方法)。当程序中全部的非守护线程都已经终止,调用setDaemon方法可能会致使虚拟机粗暴的终止线程并退出。isDaemon方法可以返回该属性的值。守护状态的做用很是有限,即便是后台线程在程序退出的时候也常常须要作一些清理工做。(daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,好比打印机队列管理,它老是在系统中运行。)app

启动线程

调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。框架

当一个线程正常地运行结束或者抛出某种未检测的异常(好比,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。当线程终止以后,是不能被从新启动的。在同一个Thread上调用屡次start方法会抛出InvalidThreadStateException异常。dom

若是线程已经启动可是尚未终止,那么调用isAlive方法就会返回true.即便线程因为某些缘由处于阻塞(Blocked)状态该方法依然返回true。若是线程已经被取消(cancelled),那么调用其isAlive在何时返回false就因各Java虚拟机的实现而异了。没有方法能够得知一个处于非活动状态的线程是否已经被启动过了(译者注:即线程在开始运行前和结束运行后都会返回false,你没法得知处于false的线程具体的状态)。另外一点,虽然一个线程可以得知同一个线程组的其余线程的标识,可是却没法得知本身是由哪一个线程调用启动的。

优先级

Java虚拟机为了实现跨平台(不一样的硬件平台和各类操做系统)的特性,Java语言在线程调度与调度公平性上未做出任何的承诺,甚至都不会严格保证线程会被执行。可是Java线程却支持优先级的方法,这些方法会影响线程的调度:

每一个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)
默认状况下,新建立的线程都拥有和建立它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).
线程的当前优先级能够经过getPriority方法得到。
线程的优先级能够经过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。

当可运行的线程数超过了可用的CPU数目的时候,线程调度器更偏向于去执行那些拥有更高优先级的线程。具体的策略因平台而异。好比有些Java虚拟机实现老是选择当前优先级最高的线程执行。有些虚拟机实现将Java中的十个优先级映射到系统所支持的更小范围的优先级上,所以,拥有不一样优先级的线程可能最终被同等对待。还有些虚拟机会使用老化策略(随着时间的增加,线程的优先级逐渐升高)动态调整线程优先级,另外一些虚拟机实现的调度策略会确保低优先级的线程最终仍是可以有机会运行。设置线程优先级能够影响在同一台机器上运行的程序之间的调度结果,可是这不是必须的。

线程优先级对语义和正确性没有任何的影响。特别是,优先级管理不能用来代替锁机制。优先级仅仅是用来代表哪些线程是重要紧急的,当存在不少线程在激励进行CPU资源竞争的状况下,线程的优先级标识将会显得很是有用。好比,在ParticleApplet中将particle animation线程的优先级设置的比建立它们的applet线程低,在某些系统上可以提升对鼠标点击的响应,并且不会对其余功能形成影响。可是即便setPriority方法被定义为空实现,程序在设计上也应该保证可以正确执行(尽管可能会没有响应)。

下面这个表格列出不一样类型任务在线程优先级设定上的一般约定。在不少并发应用中,在任一指定的时间点上,只有相对较少的线程处于可执行的状态(另外的线程可能因为各类缘由处于阻塞状态),在这种状况下,没有什么理由须要去管理线程的优先级。另外一些状况下,在线程优先级上的调整可能会对并发系统的调优起到一些做用。

范围  用途
10      Crisis management(应急处理)
7-9    Interactive, event-driven(交互相关,事件驱动)
4-6    IO-bound(IO限制类)
2-3    Background computation(后台计算)
1        Run only if nothing else can(仅在没有任何线程运行时运行的)

控制方法

只有不多几个方法能够用于跨线程交流:

  • 每一个线程都有一个相关的Boolean类型的中断标识。在线程t上调用t.interrupt会将该线程的中断标识设为true,除非线程t正处于Object.wait,Thread.sleep,或者Thread.join,这些状况下interrupt调用会致使t上的这些操做抛出InterruptedException异常,可是t的中断标识会被设为false。
  • 任何一个线程的中断状态均可以经过调用isInterrupted方法来获得。若是线程已经经过interrupt方法被中断,这个方法将会返回true。
  • 可是若是调用了Thread.interrupted方法且中断标识尚未被重置,或者是线程处于wait,sleep,join过程当中,调用isInterrupted方法将会抛出InterruptedException异常。调用t.join()方法将会暂停执行调用线程,直到线程t执行完毕:当t.isAlive()方法返回false的时候调用t.join()将会直接返回(return)。另外一个带参数毫秒(millisecond)的join方法在被调用时,若是线程没可以在指定的时间内完成,调用线程将从新获得控制权。由于isAlive方法的实现原理,因此在一个尚未启动的线程上调用join方法是没有任何意义的。一样的,试图在一个尚未建立的线程上调用join方法也是不明智的。

起初,Thread类还支持一些另一些控制方法:suspend,resume,stop以及destroy。这几个方法已经被声明过时。其中destroy方法历来没有被实现,估计之后也不会。而经过使用等待/唤醒机制增长suspend和resume方法在安全性和可靠性的效果有所欠缺,将在3.2章节进行具体讨论。而stop方法所带来的问题也将在3.1.2.3进行探讨。

 

静态方法

Thread类中的部分方法被设计为只适用于当前正在运行的线程(即调用Thread方法的线程)。为强调这点,这些方法都被声明为静态的。

  • Thread.currentThread方法会返回当前线程的引用,获得这个引用能够用来调用其余的非静态方法,好比Thread.currentThread().getPriority()会返回调用线程的优先级。
  • Thread.interrupted方法会清除当前线程的中断状态并返回前一个状态。(一个线程的中断状态是不容许被其余线程清除的)
  • Thread.sleep(long msecs)方法会使得当前线程暂停执行至少msecs毫秒。

Thread.yield方法纯粹只是建议Java虚拟机对其余已经处于就绪状态的线程(若是有的话)调度执行,而不是当前线程。最终Java虚拟机如何去实现这种行为就彻底看其喜爱了。

尽管缺少保障,但在不支持分时间片/可抢占式的线程调度方式的单CPU的Java虚拟机实现上,yield方法依然可以起到切实的做用。在这种状况下,线程只在被阻塞的状况下(好比等待IO,或是调用了sleep等)才会进行从新调度。在这些系统上,那些执行非阻塞的耗时的计算任务的线程就会占用CPU很长的时间,最终致使应用的响应能力下降。若是一个非阻塞的耗时计算线程会致使时间处理线程或者其余交互线程超出可容忍的限度的话,就能够在其中插入yield操做(或者是sleep),使得具备较低线程优先级的线程也能够执行。为了不没必要要的影响,你能够只在偶然间调用yield方法,好比,能够在一个循环中插入以下代码:if (Math.random() < 0.01) Thread.yield();

在支持可抢占式调度的Java虚拟机实现上,线程调度器忽略yield操做多是最完美的策略,特别是在多核处理器上。

线程组

每个线程都是一个线程组中的成员。默认状况下,新建线程和建立它的线程属于同一个线程组。线程组是以树状分布的。当建立一个新的线程组,这个线程组成为当前线程组的子组。getThreadGroup方法会返回当前线程所属的线程组,对应地,ThreadGroup类也有方法能够获得哪些线程目前属于这个线程组,好比enumerate方法。

ThreadGroup类存在的一个目的是支持安全策略来动态的限制对该组的线程操做。好比对不属于同一组的线程调用interrupt是不合法的。这是为避免某些问题(好比,一个applet线程尝试杀掉主屏幕的刷新线程)所采起的措施。ThreadGroup也能够为该组全部线程设置一个最大的线程优先级。

线程组每每不会直接在程序中被使用。在大多数的应用中,若是仅仅是为在程序中跟踪线程对象的分组,那么普通的集合类(好比java.util.Vector)应是更好的选择。

在ThreadGroup类为数很少的几个方法中,uncaughtException方法倒是很是有用的,当线程组中的某个线程因抛出未检测的异常(好比空指针异常NullPointerException)而中断的时候,调用这个方法能够打印出线程的调用栈信息。


同步

对象与锁

每个Object类及其子类的实例都拥有一个锁。其中,标量类型int,float等不是对象类型,可是标量类型能够经过其包装类来做为锁。单独的成员变量是不能被标明为同步的。锁只能用在使用了这些变量的方法上。然而正如在2.2.7.4上描述的,成员变量能够被声明为volatile,这种方式会影响该变量的原子性,可见性以及排序性。

相似的,持有标量变量元素的数组对象拥有锁,可是其中的标量元素却不拥有锁。(也就是说,没有办法将数组成员声明为volatile类型的)。若是锁住了一个数组并不表明其数组成员均可以被原子的锁定。也没有能在一个原子操做中锁住多个对象的方法。

Class实例本质上是个对象。正以下所述,在静态同步方法中用的就是类对象的锁。

同步方法和同步块

使用synchronized关键字,有两种语法结构:同步代码块和同步方法。同步代码块须要提供一个做为锁的对象参数。这就容许了任意方法能够去锁任一一个对象。但在同步代码块中使用的最普通的参数倒是this。

同步代码块被认为比同步方法更加的基础。以下两种声明方式是等同的:

1 synchronized void f() { /* body */ }
2 void f() { synchronized(this) { /* body */ } }

synchronized关键字并非方法签名的一部分。因此当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,另外,构造方法不多是真正同步的(尽管能够在构造方法中使用同步块)。

同步实例方法在其子类和父类中使用一样的锁。可是内部类方法的同步却独立于其外部类, 然而一个非静态的内部类方法能够经过下面这种方式锁住其外部类:

1 synchronized(OuterClass.this) { /* body */ }

等待锁与释放锁

使用synchronized关键字须遵循一套内置的锁等待-释放机制。全部的锁都是块结构的。当进入一个同步方法或同步块的时候必须得到该锁,而退出的时候(即便是异常退出)必须释放这个锁。你不能忘记释放锁。

锁操做是创建在独立的线程上的而不是独立的调用基础上。一个线程可以进入一个同步代码的条件是当前锁未被占用或者是当前线程已经占用了这个锁,不然线程就会阻塞住。(这种可重入锁或是递归锁不一样于POSIX线程)。这就容许一个同步方法能够去直接调用同一个锁管理的另外一个同步方法,而不须要被冻结(注:即不须要再经历释放锁-阻塞-申请锁的过程)。

同步方法或同步块遵循这种锁获取/锁释放的机制有一个前提,那就是全部的同步方法或同步块都是在同一个锁对象上。若是一个同步方法正在执行中,其余的非同步方法也能够在任什么时候候执行。也就是说,同步不等于原子性,可是同步机制能够用来实现原子性。

当一个线程释放锁的时候,另外一个线程可能正等待这个锁(也多是同一个线程,由于这个线程可能须要进入另外一个同步方法)。可是关于哪个线程可以紧接着得到这个锁以及何时,这是没有任何保证的。(也就是,没有任何的公平性保证-见3.4.1.5)另外,没有什么办法可以获得一个给定的锁正被哪一个线程拥有着。

正如2.2.7讨论的,除了锁控制以外,同步也会对底层的内存系统带来反作用。

静态变量/方法

锁住一个对象并不会原子性的保护该对象类或其父类的静态成员变量。而应该经过同步的静态方法或代码块来保证访问一个静态的成员变量。静态同步使用的是静态方法锁声明的类对象所拥有的锁。类C的静态锁能够经过内置的实例方法获取到:
synchronized(C.class) { /* body */ }

每一个类所对应的静态锁和其余的类(包括其父类)没有任何的关系。经过在子类中增长一个静态同步方法来试图保护父类中的静态成员变量是无效的。应使用显式的代码块来代替。

以下这种方式也是一种很差的实践:
synchronized(getClass()) { /* body */ } // Do not use
这种方式,可能锁住的实际中的类,并非须要保护的静态成员变量所对应的类(有多是其子类)

Java虚拟机在类加载和类初始化阶段,内部得到并释放类锁。除非你要去写一个特殊的类加载器或者须要使用多个锁来控制静态初始顺序,这些内部机制不该该干扰普通类对象的同步方法和同步块的使用。Java虚拟机没有什么内部操做能够独立的获取你建立和使用的类对象的锁。然而当你继承java.*的类的时候,你须要特别当心这些类中使用的锁机制。


监视器

正如每一个对象都有一个锁同样,每个对象同时拥有一个由这些方法(wait,notify,notifyAll,Thread,interrupt)管理的一个等待集合。拥有锁和等待集合的实体一般被称为监视器(虽然每种语言定义的细节略有不一样),任何一个对象均可以做为一个监视器。

对象的等待集合是由Java虚拟机来管理的。每一个等待集合上都持有在当前对象上等待但还没有被唤醒或是释放的阻塞线程。

由于与等待集合交互的方法(wait,notify,notifyAll)只在拥有目标对象的锁的状况下才被调用,所以没法在编译阶段验证其正确性,但在运行阶段错误的操做会致使抛出IllegalMonitorStateException异常。

这些方法的操做描述以下:

Wait
调用wait方法会产生以下操做:

  • 若是当前线程已经终止,那么这个方法会当即退出并抛出一个InterruptedException异常。不然当前线程就进入阻塞状态。
  • Java虚拟机将该线程放置在目标对象的等待集合中。
  • 释放目标对象的同步锁,可是除此以外的其余锁依然由该线程持有。即便是在目标对象上屡次嵌套的同步调用,所持有的可重入锁也会完整的释放。这样,后面恢复的时候,当前的锁状态可以彻底地恢复。

Notify
调用Notify会产生以下操做:

  • Java虚拟机从目标对象的等待集合中随意选择一个线程(称为T,前提是等待集合中还存在一个或多个线程)并从等待集合中移出T。当等待集合中存在多个线程时,并无机制保证哪一个线程会被选择到。
  • 线程T必须从新得到目标对象的锁,直到有线程调用notify释放该锁,不然线程会一直阻塞下去。若是其余线程先一步得到了该锁,那么线程T将继续进入阻塞状态。
  • 线程T从以前wait的点开始继续执行。

NotifyAll

notifyAll方法与notify方法的运行机制是同样的,只是这些过程是在对象等待集合中的全部线程上发生(事实上,是同时发生)的。可是由于这些线程都须要得到同一个锁,最终也只能有一个线程继续执行下去。

Interrupt(中断)
若是在一个因wait而中断的线程上调用Thread.interrupt方法,以后的处理机制和notify机制相同,只是在从新获取这个锁以后,该方法将会抛出一个InterruptedException异常而且线程的中断标识将被设为false。若是interrupt操做和一个notify操做在同一时间发生,那么不能保证那个操做先被执行,所以任何一个结果都是可能的。(JLS的将来版本可能会对这些操做结果提供肯定性保证)

Timed Wait(定时等待)
定时版本的wait方法,wait(long mesecs)和wait(long msecs,int nanosecs),参数指定了须要在等待集合中等待的最大时间值。若是在时间限制以内没有被唤醒,它将自动释放,除此以外,其余的操做都和无参数的wait方法同样。并无状态可以代表线程正常唤醒与超时唤醒之间的不一样。须要注意的是,wait(0)与wait(0,0)方法其实都具备特殊的意义,其至关于不限时的wait()方法,这可能与你的直觉相反。

因为线程竞争,调度策略以及定时器粒度等方面的缘由,定时等待方法可能会消耗任意的时间。(注:关于定时器粒度并无任何的保证,目前大多数的Java虚拟机实现当参数设置小于1毫秒的时候,观察的结果基本上在1~20毫秒之间)

Thread.sleep(long msecs)方法使用了定时等待的wait方法,可是使用的并非当前对象的同步锁。它的效果以下描述:if (msecs != 0)  {Object s = new Object();synchronized(s) { s.wait(msecs); }}固然,系统不须要使用这种方式去实现sleep方法。须要注意的,sleep(0)方法的含义是中断线程至少零时间,随便怎么解释都行。

相关文章
相关标签/搜索