1Java多线程技能
讲到多线程技术,我们就不得不提及“进程”和“线程”的概念,“百度百科”里对“进程”的解释如下:进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时锁发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源匹配和调度的一个独立单位。通俗的来讲其实就是一个正在操作系统中运行的exe程序,如图1-1所示,而线程可以理解成是在进程中独立运行的子任务。比如,QQ.exe运行时就有很多的子任务在同时运行。
图1-1系统进程列表
那么使用多线程有什么优点呢?如果有使用过“多任务操作系统”的经验,比如Windows,那么我们对于它的方便性就会有深入的体会:使用多任务操作系统后,可以最大限度的利用CPU的空闲时间来处理其他任务,比如一边处理正在由打印机打印数据,一边使用Word编辑文档。为了更好的理解多线程的优势,我们可以看一些多线程模型(图1-2)和单线程模型(图1-3),总的来说多线程可以使CPU异步执行,大大的提高CPU的利用率,而单线程只能顺序的执行。
图1-2多任务环境
图1-3单任务环境
讲了线程的概念以及多线程的优点,接下来我们来谈一谈怎么使用多线程。首先是实现多线程,一般实现多线程的方式主要有两种,一种是继承Thread类,另一种是实现Runnable接口,通过Java的源码可以发现,Thread类实现了Runnable接口,同时又考虑到Java不支持多继承,所以通过实现Runnable接口的方式来实现多线程更加符合Java的特性;其次是启动线程,启动线程一般有start()方法和run()方法,start()方法会通知“线程规划器”此线程已经准备就绪,等待系统安排一个时间来调用Thread中run()方法,由于“线程规划器”调用的随机性,所有执行start()的顺序不代表线程启动的顺序,具有异步性;而run()方法却不会交给“线程规划器”而是交给main主线程来调用run()方法,也就是必须等run()方法执行完成后,才能执行后面的代码,具有顺序性;接着是暂停/恢复线程,Java中一般用suspend()方法和resume()方法来暂停/恢复线程,但是suspend和resume的缺点是独占,如果使用不当的话,极易造成公共对象的独占,是的其他线程无法访问公共同步对象,同时suspend和resume容易应为线程的暂停导致数据的不同步;最后是停止线程,一种是直接使用stop()方法,但是这个方法不安全、会抛出ThreadDeath异常、强制让线程停止导致一些请理性工作得不到完成而且会对锁定的对象进行“解锁”导致数据不同步,而且是已被启用作废的,在将来的Java版本中,这个方法将不可用或不支持,还有一种是通过interrupt()方法,给线程注册一个中断标识,然后通过interrupted()和isInterrupted()方法来判断是否有中断标识,然后结合抛异常、break或者return来退出线程,对于interrupted()方法是用来测试当前线程是否已经中断,具有清除中断标识的作用,isInterrupted()方法用来测试是否已经中断,不具有清除中标功能。
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。守护线程时特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁,典型的守护线程就是垃圾回收,主要是为其他线程的运行的运行提供服务,用户可以通过Thread.setDameon()方法将一个线程设置为守护线程。
2 对象及变量的并发访问
前面讲述了多线程的技能,从而我们对线程的创建、启动、暂停、停止等基本操作有了一定的了解,接下来当然就会涉及到线程中的实例变量、局部变量以及方法的访问控制的问题,对于自定义线程中实例变量存在有两种情况,一种是不共享数据的情况(如图2-1),一种是共享数据的情况(如图2-2),对于不共享数据的情况下,线程之间就不会存在“非线程安全”问题,而对于共享数据的情况下就会存在“非线程安全”问题,例如:对于一个i--操作来说,在某些JVM中要分成如下三步:
1) 取得原有i值
2) 计算i--
3) 对i进行赋值
在这3个步骤中,如果有多个线程同时访问,那么就会出现“非线程安全”问题。
图2-1不共享数据
图2-2共享数据
对于“非线程安全”问题常用的处理方法是在方法前加上synchronized关键字,使多个线程以排队的顺序执行某一个方法。当一个线程调用方法前,先判断当前方法有没有上锁,如果上锁了,说明有其他的线程正在调用此方法,必须等到其他线程结束后才可以执行此方法,这样就实现了排队调用方法的目的了。但有一点我们必须要搞清楚,对于非静态方法前加入synchronized关键字,关键字synchronized取得的锁是对象锁,而不是把一段代码或者方法当做锁,如果多个线程访问的是同一个对象那么加上synchronized关键字是可以解决“非线程安全”问题,但是如果是多个对象多个锁,那么线程之间仍然是异步执行的;对于静态方法前加入synchronized关键字,关键字synchronized取得的锁就是当前的*.java文件的Class类,Class锁可以对类的所有对象起作用。
然而使用关键字synchronized声明方法在某些情况下是存在弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长的时间。在这样的情况下可以使用synchronized同步语句块来来解决,只对需要顺序执行的某个或者某些语句进行加锁,从而提高运行效率,常用的同步语句方法有synchronized(this)、synchronized(非this对象)、synchronized(class)。对于synchronized(this)和synchronized(非this对象)来说,监视器对象都是圆括号中的所指定的对象,而synchronized(class)的监视器对象是类,是与在静态方法前加synchronized关键字具有同样的概念。
对于同一个类中多方法加了synchronized关键字或者synchronized语句块的调用遵循以下规则:
1) A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用Object对象中非synchronized类型或者非synchronized代码块的方法。
2) A线程先持有Object对象的Lock锁,B线程如果在这时调用Object对象中synchronized类型或者synchronized代码块的方法则需等待,也就是同步。
总而言之,我们要区分清楚监视器对象是否相同,如果相同那么对于方法的调用就要遵循以上的规则,如果监视器对象不同,那么线程之间的方法调用就是异步的,而且如果线程遇到异常后会自动的释放掉锁持有的锁,防止出现死锁的现象。
在Java5中引入了一种Lock对象,该对象也能实现同步的效果,并且在扩张功能上也更加强大,比如具有嗅探锁定、多路通知等功能。而且使用上比synchronized更加的灵,同时类ReentrantLock具有完全互斥排他的效果,即同一个时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了实例变量的线程安全,但是效率却是非常低下。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率。读写锁表示也有两个锁,一个是读操作相关的锁,也叫共享锁;另一个是写操作相关的锁,也好排它锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
3 线程间通信
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间通信就是成为整体的备用方案之一,可以说,使线程进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对给线程任务处理的过程中进行有效地把控与监督。
通过不断的使用while语句轮询机制来检测某一个条件,来进行线程间的通信,这样会浪费CPU资源,如果轮询的时间间隔很小,更浪费CPU资源;如果轮询的时间间隔很大,有可能会取不到想要得到的数据。所以就需要有一种机制来实现减少CPU的资源浪费。而且还可以实现在多个线程间通信,他就是“wait/notify”机制。等待/通知机制在生活中比比皆是,比如在就餐中就会出现,如图3-1所示。
厨师和服务员之间的交互要在“菜品传递台”上,在这期间会有几个问题:
1) 厨师做完一道菜的时间不确定,所以厨师将菜品放到“菜品传递台”上的时间也不确定
2) ]服务员取到菜的时间取决于厨师,所以服务员就有“等待”(wait)的状态。
3) 服务员如何能取到菜呢?这又取决于厨师,厨师将菜放在“菜品传递台”上,其实就相当于一种通知(notify),这时服务员才可以拿到菜并交给就餐者。
4) 在这个过程中出现了“等待/通知”机制。
图3-1就餐时出现等待通知
方法wait()的作用是使得当前执行代码的线程立刻进行等待,将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,同时会把自己所持有的锁进行释放,直到接到通知或被中断为止。在调用wait()之前,线程必须获得对象的对象级别锁,即只能在同步方法或者同步代码块中调用wait()方法。方法notify()的主要作用是用来通知那些可能等待该对象锁的其他线程,如果哟多个线程等待,则由“线程规划器”随机挑选出一个呈wait状态的线程,并对其发出notify通知,并使其获得该对象的对象锁,同时notify也要在同步方法或者同步代码块中进行调用,即在调用前,线程也必须获得该对象的对象级别锁。有一点需要明白的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获得该对象锁。方法notifyAll()可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待队列退出,进入可运行状态。此时,只有第一个获取到该对象锁的线程才会进行running,如图3-2所示中线程状态切换示意图:
图3-2线程状态切换示意图
1) 新创建一个新的线程对象后,再调用它的start()方法,系统会为此线程分配CPU资源,时期处于Runnable(可运行)状态,这是一个准备运行的阶段,如果线程抢占到CPU资源,此线程就处于Running(运行)状态。
2) Runnable状态和Running状态可以相互切换,因为有可能线程运行一段时间后,有其他高优先级的线程抢占了CPU资源,这时此线程就从Running状态变成Runnable状态。
3) Blocked是阻塞的意思,例如遇到一个IO操作,此时CPU处于空闲状态,可能会转而把CPU时间片分配给其他线程,这时也可以成为“暂停”状态。Blocked结束后,进入Runnable状态,等待系统重新分配资源。
4) run()方法运行结束后进入销毁阶段,这个线程执行完毕。
在前面介绍过通过Lock对象也能实现同步的效果,并且在扩张功能上也更加强大,比如具有嗅探锁定、多路通知等功能。而且使用上比synchronized更加的灵活。关键字synchronized与wait()和notify()/notifyAll()方法结合可以实现通知/等待模式,但是使用notify()/notifyAll()方法进行通知时,被通知的线程却是JVM随机选择的,因为synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所以的线程都注册在它一个对象的身上,而且对于notifyAll()时,需要通知所有的WAITTING线程,没有选择权,会出现相当大的效率问题。但是Lock对象里面可以创建多个Condition(即对象监视器)实例,线程可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
Object类中的wait()方法相当于Condition类中的await()方法。
Object类中的wait(long timeout)方法相当于Condition类中的await(long timeout,TimeUnit unit)方法。
Object类中的notify()方法相当于Condition类中的signal()方法。
Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
4 总结
这本书主要讲述了Java中多线程技术,主要围绕三个方面来进行讲解,第一个方面讲的主要就是一些对线程的基本操作,使我们对线程能有一个初步的认识,知道可以通过继承Thread类或者实现Runnable接口来实现一个自定义的线程类,同时明白这两种方法的区别以及JDK中关于Thread的API的使用;第二个方面主要讲解的是怎么对变量以及方法的访问进行控制,保证不会出现一些“非线程安全”的问题,主要提到的解决方案有synchronized关键字、synchronized代码块、Lock类等来控制线程对实例变量和方法的顺序访问,同时知道方法中的变量不会出现“非线程安全”问题;第三方面讲的是怎么把一个个单独的线程结合成一个整体,使得线程之间可以相互的通信,从而保证CPU具有高效的利用率,在这方面介绍的主要技术就是wait/notifhe lock/condition,并指出lock/condition比wait/notify方法具有更高的效率以及灵活性,因为lock/condition可以注册多个监视器对象,将不同的对象注册到不同的监视器对象上,可以更加方便的对线程进行管理而wait/notify只有一个监视器对象。
网友评论