您的位置:首页 > 编程语言 > Java开发

Java基础—多线程和多进程

2015-06-04 19:11 393 查看
通常操作系统支持同时运行多个任务(程序),每个运行中的程序就是一个进程。而这个内存中运行的程序包含多个顺序执行流,而每一个执行流就是一个线程。

进程是系统进行资源分配和调度的一个独立单位。包含如下特征:独立性:进程是系统中独立存在的实体,拥有独立的资源,每个进程都拥有自己的私有地址空间。没有经过进程本身的允许不能访问其他进程的地址空间。动态性:进程与程序的区别在于,程序是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和状态。并发性①:多个进程可以在单个处理器上并发执行,且不会受到影响。

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程也可以被称作轻量级进程。线程是进程的执行单元。当进程被初始化后,主线程就被创建了。一个线程必须有一个父进程,线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程拥有的全部资源。因为多个线程。线程是独立运行的,它并不知道进程中是否还有其他线程,线程是抢占式的,当前线程随时都可能挂起,以便另一个线程可以运行。一个线程可以创建和销毁另一个线程,同一个进程中的多个线程之间可以并发执行。一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少包含一个线程。

操作系统创建一个进程时,需要分配独立的内存空间和大量的相关资源,而线程则很简单,效率高。线程共享同一个进程虚拟空间,进程代码段、进程的公有数据等。利用这些数据线程可以方便地实现相互间的通讯。Java语言内置多线程的功能支持,而不是单纯作为底层操作系统的调度方法,从而简化了Java的多线程编程。

Java使用Thread类代表线程,所有的线程对象必须是Thread类或其子类的实例。两种实现方法:继承Thread类并实现run方法,实现runnable接口,并用new Thread(实现runnable接口的类实例,‘新线程1).start();当使用继承Thread类的方法创建线程类,多条线程之间无法共享线程类的实例变量。而runnable则可以,因为创建的Runnable对象只是线程target,而多条线程可以共享一个target,所以多条线程共享同一个线程类的实例属性。

线程的生命周期:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)五种状态。当线程处于新建和死亡状态时,isAlive()返回false。线程启动以后不可能独占CPU,所以CPU会在多个线程之间切换,于是线程状态也会多次在运行、阻塞间切换。新建:程序new了一个线程之后,即新建状态,此时Java虚拟机仅仅为其分配了内存,并初始化了其成员变量。就绪:当调用了start后,即就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,此时线程仅仅是可以运行状态,何时运行取决于JVM里线程的调度。当开始执行run方法体时,线程处于运行状态,如果计算机只有一个CPU,在任何时刻只有一条线程处于运行状态,当然在一个多处理器的机器上,将会有多个线程并行执行。但当线程数大于处理器数是,依然会有多条线程在一个CPU上轮换的现象。对于抢占式策略的系统,系统会为每一个可执行的线程一小段时间处理任务,当时间用完,系统就会剥夺其所占资源,让其他线程获得执行机会,在选择下一个线程时,系统会考虑线程的优先级。协作式的需线程主动调用sleep\yield来放弃资源给下一个线程执行。

线程进入阻塞状态:1.线程调用sleep方法。2.线程调用了一个阻塞式IO方法,在该方法返回前,线程被阻塞。3.线程视图获得一个同步监视器,但该同步监视器被其他线程所持有。4.线程等待某个通知notify。5.程序调用了线程suspend方法将该线程挂起,不过这个方法容易导致死锁,所以尽量避免使用这种方法。处于阻塞状态的线程只能进入就绪状态。

线程进入就绪状态,等待系统重新调度它:1.调用sleep方法的线程经过了指定时间。2.线程调用的阻塞式IO方法已经返回。3.线程成功地获得了试图取得同步监视器。4.线程正在等待某个通知时,其他线程发出了一个通知。5.处于关起状态的线程被调用了resume恢复方法。

线程死亡:1.线程在run方法执行完成,正常结束。2.现成派出一个未捕获的Exception或Error。3.直接调用该线程的stop()方法结束该线程------容易死锁,不推荐使用。死亡后的线程无法调用start方法重新启动,处于启动状态后的线程也无法再次调用start,否则都会产生异常。

控制线程:join线程:一个线程等待另一个线程完成的方法。即当在某个程序执行流中调用其他线程的join()方法,调用线程将被阻塞,直到被join方法加入的join线程完成为止。Join通常是由使用线程的程序调用,用于将大问题划分成许多小问题,每个小问题分配一个线程,当所有的小问题得到处理后,再调用主线程来进一步操作。join方法的三种重载行为join()\join(long millis)\join(long millis,int nanos)控制线程(守护线程、精灵线程):在后台运行并为其他线程提供服务。JVM的垃圾回收线程即是典型的后台线程。如果所有的前台线程都死亡了,后台线程会自动死亡(有一定的时间)。调用Thread对象setDamon(true)方法可将指定线程设置成后台线程,需保证在start前。当整个虚拟机中只剩下后台线程中,程序就没有运行的必要了,所以虚拟机也就退出了。可用isDaemon()判断,用于判断指定线程是否为后台线程。线程睡眠:sleep(long
millis)\sleep(long millis, int nanos)。线程让步yield:也是一个让当前正在执行的线程暂停的静态方法,但它不会阻塞该进程,它只是将该线程转入就绪状态。让系统的线程调度器重新调整一次,完全有可能刚被暂停,又被系统重新调度出来执行。

线程的同步:使用同步监视器的通用方法就是同步代码块,语法如下:synchronized(obj){...//此处代码即是同步代码块}obj即是同步监视器,含义是:线程开始执行同步代码块前,必须先获得对同步监视器的锁定。通常我们推荐使用可能被并发访问的共享资源充当同步监视器。与同步代码块对应的,Java还支持用synchronized来修饰某个方法,此时无需指定同步监视器,即this,也就是当前的对象对象。通过使用同步方法可以非常方便地将某类变成线程安全的类。特征如下:该类的对象可以被多个线程安全的访问,每个线程调用该对象的任意方法后都将得到正确的结果。每个线程调用该对象的任意方法后,该对象的依然保持合理状态。不可变类必然是线程安全的,因为它的对象的状态不可改变。对于可变的类来说,只需将对象的属性方法修改成同步方法即可。可变类的线程安全是以效率为代价的,为了减轻这种负面影响,采用如下策略:不要对县城安全类的所有方法都同步,只对那些会改变竞争资源(共享资源)的方法进行同步。如果可变类有两种运行环境:单线程环境和多线程环境,则应该提供两种版本,以便保持性能。

释放同步监视器的锁定:1.当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。2.当线程在同步代码块、同步方法中遇到break、return终止了继续执行时。3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时将会释放同步监视器。4.当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停、并释放同步监视器。线程不会释放同步监视器:1,当线程执行同步代码块或同步方法时,程序调用Thread.sleep()/yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。2.线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器,我们一般不建议使用suspend及resume方法来控制线程。

Lock:Java在1.5以后提供了比synchronized方法和代码块更加广泛的锁定操作,Lock灵活且现实定义锁对象。并且可以支持多个相关的Condition对象。某些锁允许对共享资源并发访问,如ReadWriteLock(读写锁)。在实现线程安全的控制中,通常喜欢使用ReentranLock(可重入锁,即可以对已经加锁的再次加锁,通过一个计数器来标示嵌套)使用该Lock对象可以显示加锁、释放锁,一般在finally中释放锁。tryLock获取超时失效琐,试图获取可中断琐lockInterruptibly()方法。Lock对象保证同步时(因为不是隐式的,所以无法使用wait、notify等),Java提供了一个Condition类来保持协调,他可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。Condition提供了如下三个方法:await()、signal()、signalAll来实现相应的功能

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也不会对此采取措施处理。

Object类提供wait()、notify()、notifyAll()三个方法,这三个方法必须由同步监视器对象来调用,可分为如下两种情况:1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以在同步方法中可直接使用这三个方法,2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号咯的喜爱那个,所以必须使用该对象调用这三个方法。wait():导致当前线程等待,并释放对该同步监视器的锁定,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。notify:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则选择唤醒其中一个线程,且是随意的,只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。notifyALL:唤醒所有。

管道:前面的两种线程通信方式,也是线程之间协调运行的控制策略。如果想要在线程之间做更多的信息交互,则可以使用管道流进行通信。它主要用如下三种形式PipedInputStream/PipedOutStream,PipedReader\PipedWriter以及PipeSinkChannel/Pipe.SourceChannel,分别代表管道字节流、管道字符流和新IO管道Channel。使用管道实现多线程通信可按如下步骤进行:1.使用new操作符分别创建管道输入流和输出流。2.使用管道输入流和输出流的connect方法把两个输入流和输出流连接起来。3.将管道输入流、管道输出流分别传入两个线程。4.两个线程可以分别依赖于各自的管道输入流、管道输出流进行通信。通常没有不要使用管道流,因为两个线程共享同属于一个进程的共享数据。这才是信息交换的最好方式。

线程组:可以对线程进行分类管理,默认情况下子线程和创建他的父线程拥有相同的线程组,线程在加入指定线程组后不允许改变线程组直至死亡。Thread提供了如下几个方法来创建一个线程组:Thread(ThreadGroup group, Runnable target):以target的run方法作为线程执行体创建新线程属于group组,Thread(ThreadGroup group, Runnable target, String name):线程名为name。Thread(ThreadGroup
group, Runnable target),创建新线程,名为name,属于group线程组。Thread的两个构造函数:ThreadGroup(String name)以指定线程组名字来创建新线程组。ThreadGroup(ThreadGroup parent, String name)已制定的名字,指定的父线程组创建一个新线程组。int activeCount返回此线程组中活动线程的数目。interrupt()中断此线程组中的所有线程。isDaemon():判断该线程组是否是后台线程组。setDaemon(boolean
daemon),设置后台线程组,当后台线程组的最后一个线程被销毁,则后台线程组将自动销毁。setMaxPriority(int pri)设置线程组的最高优先级。

JDK1.5后,Java加强了线程的异常处理,如果线程执行过程中异常,JVM会在线程借宿前自动查找是否有对象的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,将会调用uncaughtException (Thread t, Throwable e)方法来处理。Thread类提供了两个方法来设置异常处理器:1.staticsetDefaultUncaughtExceptionHandler为所有的线程类的所有线程实例设置默认的异常处理器。setUncaughtExceptionHandler,为指定线程实例设置异常处理器。ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的鲜橙族将会作为默认的异常处理器,所以当一个线程抛出异常时,JVM首先查找该异常对应的异常处理,否则继续调用线程所属线程组对象的uncaughtException方法来处理该异常,线程组异常处理流程:1.如果有父线程组,则调用父线程组uncaughtException方法2.否则如果该线程实例所属的线程类有默认的异常处理器那么就调用该异常处理器处理。3.否则将调用栈信息打印到System,err错误输出流,并结束该线程。

Callable和Future:JDK1.5后Java提供了一个Callable接口。它是以call()方法作为线程的执行体,但可以有返回值,call()方法也可以声明抛出异常,而Futer接口则代表Cllable接口里的返回值,并为Futer接口提供了一个FuterTask实现类,此类实现了Futer和Runnable接口,可以作为Thread类的target。future中定义了如下公共方法来控制它关联的Callable任务,boolean cancel(boolean mayInterruptlfRunning)试图取消该Future里关联的Callable任务。V
get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束时才会得到返回值。V get(long timeout, TimeUnit unit)返回Callable任务里call()方法的返回值,该方法让新恒旭最多阻塞指定时间,如果Callable依然没返回只,则会抛出TimeOutException异常。Boolean isCancelled()如果在Callable任务正常完成前被取消,则返回true。isDone():如果Callable任务已完成,则返回true。创建并启动有返回值的线程步骤如下:1.创建Callable接口的实现类,并实现Call()方法,该方法将作为线程的执行体,且有返回值。(Callable接口里的泛型参数类型与call方法返回值类型需相同)2.创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FuterTask对象封装了该Callable对象call()方法的返回值。3.使用FutureTask对象作为Thread对象的target创建、并启动新线程。4.塔吊用FuttureTask对象的方法来获得子线程执行结束后的返回值。

线程池:系统启动一个新线程的成本是比较高的,因为他涉及与操作系统交互,因此使用线程池可以很好地提高性能,尤其是当前程序中需要创建大量生存期很短暂的线程时,更应考虑线程池。线程池与数据库连接池类似,线程池在系统启动时即创建大量的空闲线程,程序将一个Runnable对象传给线程池,线程池启动一条线程执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。使用线程池可以将系统中的并发线程数目控制在线程池规定的数目,从而控制系统性能急剧下降,甚至导致JVM崩溃的问题。从JDK1.5后Java内建支持线程池,提供一个Executors工厂类产生连接池。提供如下静态方法newCachedThreadPool()创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。newFixedThreadPool(int
nThreads):创建一个可重用、具有固定线程数的线程池。newSingleThreadExecutor():创建一个只有单线程的线程池。newScheduledThreadPool(int corePoolSize)创建具有指定线程数的线程池,他可以在指定延迟后执行现场任务,参数制定了池中所保存的线程数,即使线程是空闲的也被保存在线程池内。newSingleThreadScheduledExecutor()创建只有一条线程的线程池,它可以指定延迟后执行线程任务。前三个方法返回一个ExecutorService线程池对象,它可以执行Runnable或Cllable对象所代表的线程,而后两个方法返回一个ScheduledExecutorService线程池,他是ExecutorService子类,他可以指定延迟后执行线程任务。线程池用完后可以使用shutdown()关闭,此时线程池中的所有任务执行完后,池中所有线程都会死亡;另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。使用线程池来执行线程任务的步骤如下:①调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。3.调用ExecutorService对象的submit方法来提交Runnable或Callable实例。4.当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池。

ThreadLocal<T>类可以简化多线程编程时的并发访问,是线程局部变量的意思,它为每个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突,从线程角度看,就好像每个线程都完全拥有该变量。提供了 T get():返回次线程局部变量中当前线程副本中的值。void remove()删除此线程局部变量中当前线程的值。void set()设置此线程局部变量中当前线程副本中的值。ThradLocal并不能代替同步机制,两者面向的问题领域不同,同步机制是为了同步多个线程对相同资源的并发访问,是多线程通信的有效方式,而ThreadLocal则是隔离多个线程的数据共享,从根本上避免了多个线程之间的共享资源,也就不存在同步问题了。

Collections:前面提到ArrayList\linkedLIst\HashSet\TreeSet\HashMap等都是线程不安全的,也就是有可能当多个线程向这些集合中放入一个元素时,可能破坏数据的完整性,Collections提供了如下静态方法,把他们包装秤线程安全的集合。<T> Collection <T> synchronizedCollection(Collection <T> c),static <T>list<T>synchronizedList(List<T> list);返回指定List对应的线程安全的List对象。Map、SortedMap、Set、SortedSet类似。形式如下:HashMap
m = Collections.synchronizedMap(new HashMap())。

线程安全的集合类:java.util.concurrent包提供了ConcurrentHashMap\ConcurrentLinkedQueue两个支持并发访问的集合,读取操作时不必锁定。多个线程共同访问ConcurrentLinkedQueue不允许使用null,实现了多线程的高效访问,无需等待。ConcurrentHashMap支持16条线程并发写入,超过则可能有一些线程需等待。可以设置concurrenyLevel构造方法来设置等多的并发线程。和前面使用迭代器访问遍历集合元素差别是,该迭代器可能不能反应出创建迭代器后所做的修改,但程序不会抛出任何异常(ConcurrentModificationException)

①并发性和并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。现代的操作系统都支持多进程的并发,一般分为共用式的多任务操作策略和抢占式的多任务策略(CPU在某个时间节点上只能执行一个进程,CPU不断在这些进程之间轮换看似多个进程同时执行)

②线程可通过setName\getName设置和获取线程的名字,Thread.currentThread是一个返回当前正在执行的线程对象的静态方法。

③runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run方法仅作为线程的执行体,而实际的线程对象依然是Thread类实例,只是该Thread线程负责执行其target的run方法。在Java中hread类不可以把任意对象的任意方法作为线程的执行体,c#可以。

④两种实现线程方式的比较,采用Runnable接口方式的多线程:线程类只是实现了Runnale接口,还可以继承其他类。可以多个线程共享一个target对象,所以非常适合多个相同线程共同处理一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想,缺点是获得当前线程仅能使Thread,currentThread()方法,采用继承Thread类的方式:已经继承了Thread类,不能再继承其他父类。获取当前线程仅需使用this即可。尽量采用Runnable实现多线程。

⑤启动线程用start方法,是因为此时系统会把run方法当成线程的执行体来处理,而如果调用run方法。则会立即按照普通对象执行此方法,在该方法返回前其他线程无法并发执行。如果希望在主线程中让子线程立马执行,只需Sleep(1)即可。

⑥sleep和yield区别:sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同或更高的线程执行机会。sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield不会讲线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源被执行。sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时要么捕捉该异常,要么显示声明抛出该异常。而yield方法则没有声明抛出任何异常。sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

⑦线程优先级:每个线程都有默认的优先级,并且与创建它的父线程具有相同的优先级。main线程具有普通优先级,一般可通过set/getPriority方法来设置和返回指定线程的优先级。MAX_PRIPORITY=10,MIN_PRIPORITY=1,NORM_PRIPORITY=5

⑧synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。领域驱动设计DDD
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: