美文网首页我自己看的,面试题
Java 多线程-Android面试准备2019-2-10

Java 多线程-Android面试准备2019-2-10

作者: LLorenzo | 来源:发表于2019-02-10 20:53 被阅读132次

    主线程

    当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的。

    线程的创建和启动

    继承Thread类创建线程类

    通过继承Thread类来创建并启动多线程的步骤如下:1.定义Thread类的子类,并重写该类的run()方法2.创建线程对象实例3.调用线程对象的start()方法来启动该线程。

    使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量(相当于不同对象之间的实例变量,自然不能共享)。使用这种方式来创建线程时,如果需要访问当前线程,直接使用this即可获得。

    实现Runnable接口创建线程类

    通过实现Runnable接口来创建并启动多线程的步骤如下:1.定义Runnable接口的实现类,并重写该接口的run()方法2.创建Runnable实现类的实例,并以此实例作为target来创建Thread对象(new  Thread(target)),该Thread对象才是真正的线程对象3.调用线程对象的start()方法来启动该线程。

    Runnable接口中只包含一个抽象方法,从Java8开始Runnable接口使用了@FunctionalInterface修饰。也就是说,Runnable接口是函数式接口,可使用Lambda表达式创建Runnable对象。接下来介绍的Callable接口也是函数式接口。

    采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量(即target实例中的实例变量)。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量,所以非常适合多个相同线程来处理同一份资源的情况。但是使用这种方式来创建线程时,如果需要访问当前线程,必须使用Thread.currentThread()方法。

    使用Callable和Future创建线程

    从Java 5开始,Java提供了Callable接口,该接口像是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,但它比run()方法更强大:1.call()方法可以有返回值。2.call()方法可以声明抛出异常。因此完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。问题是Callable接口不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。而且call()方法还有一个返回值,call()方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取call()方法的返回值呢?

    Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个Future Task实现类,该实现类实现了Future接口,并实现了Runnable接口,可以作为Thread类的target。

    在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

    1.boolean cancel(boolean mayInterruptlfRunning):试图取消该Future里关联的Callable任务。

    2.V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。

    3.V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出Timeout Exception异常。

    4.boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。

    5.boolean isDone():如果Callable任务已完成,则返回tue。

    Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同,而且Callable接口是函数式接口,因此可使用Lambda表达式创建Callable对象。

    创建并启动有返回值的线程的步骤如下:1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象。2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。3.使用FutureTask对象作为Thread对象的target创建并启动新线程。4.调用Future Task对象的get()方法来获得子线程执行结束后的返回值。

    使用Callable和Future创建线程示例 运行结果

    上面程序中使用Lambda表达式直接创建了Callable对象,这样就无须先创建Callable实现类再创建Callable对象了,然后将该实例包装成一个FutureTask对象。实现Callable接口与实现Runnable接口并没有太大的差别,只是Callable的call()方法允许声明抛出异常,而且允许带返回值。程序最后调用FutureTask对象的get()方法来返回call()方法的返回值,该方法将导致主线程被阻塞,直到call()方法结束并返回为止。运行上面程序,将看到主线程和call()方法所代表的线程交替执行的情形,程序最后还会输出call()方法的返回值。

    线程的生命周期

    新建和就绪状态

    当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

    当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。至于该线程何时开始运行,以及是否在start()方法后立刻运行其线程,均不确定,而是取决于JVM里线程调度器的调度。

    如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体,run()方法将会立即被执行,在它返回之前其他线程无法并发执行,即完全失去了多线程的意义。所以不能直接调用run()方法,否则就变成单线程程序了。

    运行和阻塞状态

    当发生如下情况时,线程将会进入阻塞状态:1.线程调用sleep()方法主动放弃所占用的处理器资源。2.线程调用了一个阻塞式IO方法,在该方法返回之前该线程被阻塞。3.线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识,后面将有更深入的介绍。4.线程在等待某个通知。5.程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法。

    针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:1.调用sleep()方法的线程经过了指定时间。2.线程调用的阻塞式IO方法已经返回。3.线程成功地获得了试图取得的同步监视器。4.线程正在等待某个通知时,其他线程发出了一个通知。5.处于挂起状态的线程被调用了resume()恢复方法。

    线程状态转换图

    线程死亡

    线程会以如下三种方式结束,结束后就处于死亡状态:1.run()或call()方法执行完成,线程正常结束。2.线程抛出一个未捕获的Exception或Error。3.直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。

    当主线程结束时,其他子线程不受任何影响,并不会随之结束。

    为了测试某线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时该方法将返回true;当线程处于新建或死亡两种状态时该方法将返回false。

    不要试图对一个已经死亡的线程调用start()方法使它重新启动,将会引发IllegalThreadState-Exception异常,死亡就是死亡,该线程将不可再次作为线程执行。

    线程控制

    join线程

    Thread提供了让一个线程等待另一个线程完成的方法:join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞直到被join()方法加入的join线程执行完为止。

    join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

    上面程序中一共有3个线程,主方法开始时就启动了名为"新线程"的子线程,该子线程将会和main线程并发执行。当主线程的循环变量i等于8时,启动了名为"被Join的线程"的线程,该线程不会和main线程并发执行,main线程必须等该线程执行结束后才可以向下执行。在名为"被Join的线程"的线程执行时,实际上只有2个子线程并发执行,而主线程处于等待状态。

    join线程示例 运行结果

    join()方法常用有如下两种重载形式:1.join():等待被join()的线程执行完成。2.join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程还没有执行结束,则不再等待。

    后台线程

    有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为"后台线程(Daemon Thread)",又称为"守护线程"。JVM的垃圾回收线程就是典型的后台线程。

    后台线程有个特征:如果所有的前台线程都死亡(即整个虚拟机中只剩下后台线程时),后台线程会自动死亡。

    调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。Thread类还提供了一个isDaemon()方法,用于判断指定线程是否为后台线程。

    前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

    前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。

    线程睡眠

    如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。sleep()方法形式:static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。

    当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

    线程让步

    yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

    实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

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

    改变线程优先级

    每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。

    Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1-10之间,也可以使用Thread类的如下三个静态常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)。

    线程同步

    线程安全问题

    关于线程安全问题有一个经典的问题:银行取钱的问题,使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题(这里忽略查询账号与密码是否匹配的步骤)。

    银行取钱示例

    有可能出现问题:账户余额只有1000时取出了1600,而且账户余额出现了负值。

    同步代码块

    之所以出现线程安全问题是因为run()方法的方法体不具有同步安全性,即程序中有两个并发线程在修改Account对象;而且系统恰好在某个线程判断余额足够后取出钞票和修改余额的间隔期间(即17行)执行线程切换,切换给另一个修改Account对象的线程,所以就出现了问题。

    为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下synchronized(obj){//此处的代码就是同步代码块},上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

    任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

    虽然Java程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,应该考虑使用账户作为同步监视器,把程序修改成如下形式。

    synchronized示例

    上面程序使用synchronized将run()方法里的方法体修改成同步代码块,该同步代码块的同步监视器是account对象,这样的做法符合"加锁-修改-释放锁"的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(即临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

    同步方法

    与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非statIc方法)而言,无须显式指定同步监视器,  同步方法的同步监视器是this,也就是调用该方法的对象。

    假如此时有一个类A,类A中有methodOne()和methodTwo()都是同步方法。a是A的实例,当在main方法(主线程)中执行a.methodOne()时(a此时是同步监视器),a对象就被锁住,若在此方法还未结束之前在子线程中执行a.methodTwo(),子线程就会被阻塞。但若methodTwo()不是同步方法时,则可以正常执行。

    通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:1.该类的对象可以被多个线程安全地访问。2.每个线程调用该对象的任意方法之后都将得到正确结果。3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

    前面介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。例如上面的Account就是一个可变类,它的balance成员变量可以被改变,当两个线程同时修改Account对象的balance成员变量的值时,程序就出现了异常。

    下面将Account类对balance的访问设置成线程安全的,那么只要把修改balance的方法变成同步方法即可。

    同步方法示例

    上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰该方法,把该方法变成同步方法,该同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作,这样也可以保证多个线程并发取钱的线程安全。因为Account类中已经提供了draw()方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该线程类的run()方法只要调用Account对象的draw()方法即可执行取钱操作。

    synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器和成员变量等。

    上面的DrawThread类无须自己实现取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于已经使用synchronized关键字修饰了draw()方法,同步方法的同步监视器是this,而this总代表调用该方法的对象,在上面示例中,调用draw()方法的对象是account,因此多个线程并发修改同一份account之前,必须先对account对象加锁。这也符合了"加锁-修改-释放锁"的逻辑。

    在Account里定义draw()方法,而不是直接在run()方法中实现取钱逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式:Domain Driven Design(领域驱动设计DDD),这种方式认为每个类都应该是完备的领域对象,例如Account代表用户账户,应该提供用户账户的相关方法;通过draw()方法来执行取钱操作(实际上还应该提供transfer()等方法来完成转账等操作),而不是直接将setBalance()方法暴露出来任人操作,这样才可以更好地保证Account对象的完整性和一致性。

    可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:1.不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步。2.如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。JDK所提供的StringBuilder、StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBuilder来保证较好的性能;当需要保证多线程安全时,就应该使用StringBuffer。

    释放同步监视器的锁定

    任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:1.当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。2.当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

    在如下所示的情况下,线程不会释放同步监视器:1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用suspend()和resume()方法来控制线程。

    同步锁

    从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

    Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

    Java8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock。ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁。使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

    同步锁示例

    上面程序定义了一个ReentrantLock对象,程序中实现draw()方法时,进入方法开始执行后立即请求对ReentrantLock对象进行加锁,当执行完draw()方法的取钱逻辑之后,程序使用finally块来确保释放锁。

    使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合"加锁-修改-释放锁"的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一个线程能进入临界区。

    同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

    虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long, TimeUnit)方法。

    ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

    死锁

    当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下。

    线程通信

    传统的线程通信

    假设系统中有两个线程分别代表存款者和取钱者,二者不断地重复动作,每当存款者将钱存入后取钱者立即取出,不允许存款者连续两次存钱也不允许取钱者连续两次取钱(读者写者)。

    为了实现这种功能,可以借助于Object类提供的wait()、notify()和notifyAll()三个方法,这三个方法并不属于Thread类而是属于Object类。但这三个方法必须由同步监视器对象来调用,这可分成以下两种情况:1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

    关于这三个方法的解释如下:1.wait()导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。wait()方法有两种形式:无时间参数(一直等待直到其他线程通知)的和有时间参数的(等待后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。2.notify()唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。3.notifyAll()唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

    程序中可以通过一个旗标来标识账户中是否已有存款

    ,当旗标为false时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为true,并调用notify()或notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让该线程等待。

    当旗标为true时,表明账户中已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调用notify()或notifyAll()方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为false就调用wait()方法让该线程等待。

    本程序为Account类提供draw()和deposit()两个方法,分别对应该账户的取钱和存款等操作,因为这两个方法可能需要并发修改Account类的balance成员变量的值,所以这两个方法都使用synchronized修饰成同步方法。除此之外,这两个方法还使用了wait()、 notifyAll()来控制线程的协作。

    Account类 DrawTest类

    使用Condition控制线程通信

    如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

    Condition将同步监视器方法(wait()、notify()和notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。

    Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法:1.await():类似于隐式同步监视器上的wait()方法,导致当前线程等待直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。2.signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。3.signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

    下面程序中Account使用Lock对象来控制同步,并使用Condition对象来控制线程的协调运行。

    Account类

    不难发现这个程序与上个程序的逻辑基本相似,只是现在显式地使用Lock对象来充当同步监视器,则需要使用Condition对象来暂停、唤醒指定线程。

    使用阻塞队列控制线程通信

    Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

    BlockingQueue提供如下两个支持阻塞的方法:1.put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满则阻塞该线程。2.take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空则阻塞该线程。

    BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三组:1.在队列尾部插入元素。包括add(E e)、offer(E e)和put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。2.在队列头部删除并返回删除的元素。包括remove()、poll()和take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。3.在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

    BlockingQueue包含的方法之间的对应关系如下表所示。

    BlockingQueue包含的方法之间的对应关系 BlockingQueue与其实现类之间的类图

    图中以黑色方框框出的都是Java7新增的阻塞队列。从图中可以看到,BlockingQueue包含如下5个实现类:1.ArrayBlockingQueue:基于数组实现的BlockingQueue队列。2.LinkedBlockingQueue:基于链表实现的BlockingQueue队列。3.PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。4.SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。5.DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

    下面以ArrayBlockingQueue为例介绍阻塞队列的功能和用法。

    ArrayBlockingQueue示例 ArrayBlockingQueue示例

    线程组和未处理的异常

    Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。

    一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。

    Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组:1.Thread(ThreadGroup group, Runnable target)2.Thread(ThreadGroup group, Runnable target, String name)3.Thread(ThreadGroup group, String name)。

    同时还提供了一个getThreadGroup()方法来返回该线程所属的线程组,getThreadGroup()方法的返回值是ThreadGroup对象。ThreadGroup类提供了如下两个简单的构造器来创建实例:1.ThreadGroup(String name)2.ThreadGroup(ThreadGroup parent, String name):以指定的名字和指定的父线程组创建一个新线程组。

    名字可通过调用ThreadGroup的getName()方法来获取,但不允许改变线程组的名字。

    ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程:1.int activeCount():返回此线程组中活动线程的数目。2.interrupt():中断此线程组中的所有线程。3.isDaemon():判断该线程组是否是后台线程组。4.setDaemon(boolean daemon):把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁。5.setMaxPriority(int pri):设置线程组的最高优先级。

    ThreadGroup内还定义了一个很有用的方法:void uncaughtException(Thread t, Throwable e),该方法可以处理该线程组内的任意线程所抛出的未处理异常。

    从Java5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t, Throwable e)方法来处理该异常。Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e),该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。Thread类提供了如下两个方法来设置异常处理器:1.static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器。2.setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器。

    ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时,JVM会首先査找该异常对应的异常处理器(setUncaughtExceptionHandler()方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则JVM将会调用该线程所属的线程组对象的uncaughtException()方法来处理该异常。线程组处理异常的默认流程如下:1.如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常。2.如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),那么就调用该异常处理器来处理该异常。3.如果该异常对象是ThreadDeath的对象,则不做任何处理;否则将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。

    下面程序为主线程设置了异常处理器,当主线程运行抛出未处理异常时,该异常处理器将会起作用。

    异常处理器示例

    运行该程序,会看到如下输出:Thread[main,5,main] 线程出现了异常:java.lang.ArithmeticException: / by zero,从上面程序的执行结果来看,虽然程序中粗体字代码指定了异常处理器对未捕获的异常进行处理而且该异常处理器也确实起作用了,但程序依然不会正常结束。这说明异常处理器与通过catch捕获异常是不同的——当使用catch捕获异常时,异常不会向上传播给上一级调用者;但使用异常处理器对异常进行处理之后,异常依然会传播给上一级调用者。

    线程池

    系统启动一个新线程的成本是比较高的,所以使用线程池可以提高性能。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当执行结束后该线程并不会死亡而是再次返回线程池中成为空闲状态。

    Java8改进的线程池

    从Java5开始Java内建支持线程池。Java5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池:

    1.newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。

    2.newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。

    3.newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1。

    4.newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolsize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。

    5.newSingleThreadScheduledExecutor:创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。

    6.ExecutorService newWorkStealing Pool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。

    7.ExecutorService newWorkStealingPool:该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。

    上面7个方法中的前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;而中间两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务;最后两个方法则是Java8新增的,这两个方法可充分利用多CPU并行的能力。这两个方法生成的work stealing池,都相当于后台线程池,如果所有的前台线程都死亡了,work stealing池中的线程会自动死亡。

    ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程,就立即执行线程任务),程序只要将一个Runnable对象或Callable对象(代表线程任务)提交给该线程池,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法:

    1.Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中Future对象代表Runnable任务的返回值,但run()方法没有返回值,所以Future对象将在run()方法执行结束后返回null。但可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。

    2.<T> Future<T> submit(Runnable task, T result):将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中result显式指定线程执行结束后的返回值,所以Future对象将在run()方法执行结束后返回result。

    3.<T> Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里call()方法的返回值。

    ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池,它提供了如下4个方法:

    1.ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):指定callable任务将在delay延迟后执行。

    2.ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):指定command任务将在delay延迟后执行。

    3.ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定频率重复执行。也就是说,在initialDelay后开始执行,依次在initialDelay+period、initialDelay+2* period…处重复执行,依此类推。

    4.ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务在任一次执行时遇到异常,就会取消后续执行;否则,只能通过程序来显式取消或终止该任务。

    用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列。调用shutdown()方法后的线程池不再接收新任务,但会将以前所有己提交任务执行完成。当线程池中的所有任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

    使用线程池来执行线程任务的步骤如下:

    1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。

    2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。

    3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例。

    4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

    线程池示例

    Java8增强的ForkJoinPool

    Java7提供了ForkJoinPool来支持将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,是一种特殊的线程池。ForkJoinPool提供了如下两个常用的构造器:

    1.ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool。

    2.ForkJoinPool():以Runtime.availableProcessors方法的返回值作为parallelism参数来创建ForkJoinPool。

    Java8进一步扩展了ForkJoinPool的功能,Java8为ForkJoinPool增加了通用池功能。ForkJoinPool类通过如下两个静态方法提供通用池功能:

    1.ForkJoinPool commonPool():该方法返回一个通用池,通用池的运行状态不会受shutdown()或shutdownNow()方法的影响。当然,如果程序直接执行System.exit(0);来终止虚拟机,通用池以及通用池中正在执行的任务都会被自动终止。

    2.int getCommonPoolParallelism():该方法返回通用池的并行级别。

    创建了ForkJoinPool实例之后,就可调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务了。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction合RecursiveTask。其中RecursiveTask代表有返回值的任务,而RecursiveAction代表没有返回值的任务。

    线程池工具类的类图

    下面以执行没有返回值的"大任务"(简单地打印0-300的数值)为例,程序将一个"大任务"拆

    分成多个"小任务",并将任务交给ForkJoinPool来执行。

    ForkJoinPool示例

    分解后的任务分别调用fork()方法开始并行执行。ForkJoinPool启动了CPU核数个线程来执行这个打印任务。

    上面定义的任务是一个没有返回值的打印任务,如果大任务是有返回值的任务,则可以让任务继承RecursiveTask<T>,其中泛型参数T就代表了该任务的返回值类型。下面程序示范了使用RecursiveTask对一个长度为100的数组的元素值进行累加。

    ForkJoinPool示例

    线程相关类

    Java为线程安全提供了一些工具类。

    Thread loca类

    通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以很简洁地隔离多线程程序的竞争资源。ThreadLocal类,是Thread Local Variable(线程局部变量)的意思,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。

    线程局部变量其实非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。

    ThreadLocal类只提供了如下三个方法:

    1.T get():返回此线程局部变量中当前线程副本中的值。

    2.void remove():删除此线程局部变量中当前线程的值。

    3.void set(T value):设置此线程局部变量中当前线程副本中的值。

    下面程序将向读者证明ThreadLocal的作用。

    主线程问户名有值

    上面Account类中的三行粗体字代码分别完成了创建

    u1l账户的i值:0

    ThreadLocal对象、从ThreadLocal中取出线程局部变量、修改线

    mu账户的i值:1

    程局部变量的操作。由于程序中的账户名是一个ThreadLocal变

    m1户的值:3

    因为账户名又是一

    nul账户的i值:4

    量,所以虽然程序中只有一个Account对象,但两个子线程将会

    mull账户的i值

    产生两个账户名(主线程也持有一个账户名的副本)。两个线程

    线程甲账户的i值

    在加后看

    线程甲账户的i值

    名己经有值

    进行循环时都会在i=6时将账户名改为与线程名相同,这样就

    u11账户的1值:0

    hu11账户的i值:1

    可以看到两个线程拥有两个账户名的情形,如图16.18所示。

    u11账户的主值:

    hu11账户的i值

    从上面程序可以看出,实际上账户名有三个副本,主线程

    hu账户的i值:4

    mu11账户的i值:

    一个,另外启动的两个线程各一个,它们的值互不干扰,每个

    线程乙账户的i值:6

    线程完全拥有自己的Thread Local变量,这就是ThreadLocal的

    图1618线程局部变量互不干扰的情形用途。

    ThreadLocal和其他所有的同步机制一样,都是为了解决多

    线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安

    全访问的。该变量是多个线程共享的,所以要使用这种同步机制,需要很细致地分析在什么时候对变量

    进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情况下,系统并没有将这

    份资源复制多份,只是采用了安全机制来控制对这份资源的访问而己

    ThreadLocal从另一个角度来解决多线程的并发访问, ThreadLoca将需要并发访问的资源复制多份,

    每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。

    Thread Local提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进

    Thread Local,或者把该对象与线程相关的状态使用ThreadLoca保存

    ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相

    同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据

    共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

    通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制:如果

    仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal

    >1692包装线程不安全的集合

    前面介绍Java集合时所讲的ArrayList、LinkedList、Hash Set、Tree Set、 HashMap、TreeMap等都

    是线程不安全的,也就是说,当多个并发线程向这些集合中存、取元素时,就可能会破坏这些集合的

    数据完整性。

    如果程序中有多个线程可能访问以上这些集合,就可以使用Collections提供的类方法把这些集合

    包装成线程安全的集合。Collections提供了如下几个静态方法。

    <T> Collection<T> synchronized Collection( Collection<T>c):返回指定collection对应的线程安

    全的collection

    statIc< t> Lists<T> synchronizedList(List<T>list):返回指定List对象对应的线程安全的List对象。

    statIc<K,v>Map<Kv> synchronizedMap(Map<K,v>m):返回指定Map对象对应的线程安全的

    Map对象。

    statIc<T>set<T> synchronizedSet(set<T>s):返回指定Set对象对应的线程安全的Set对象。

    statIc<k,v> SortedMap<K,v> synchronizedSortedMap( SortedMap<KV>m):返回指定SortedMap

    对象对应的线程安全的SortedMap对象。

    statIc<T> SortedSet< T> synchronizedSortedSet( SortedSet<T>s):返回指定SortedSet对象对应的

    线程安全的SortedSet对象。

    例如需要在多线程中使用线程安全的HashMap对象,则可以采用如下代码:

    //使用Collections的synchroni zedMap方法将一个普通的HashMap包装成线程安全的类

    HashMap m- Collections. synchroni zedMap(new HashMap())i

    ,注

    如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装,如上程序

    所示——当HashMap对象创建后立即被包装成线程安全的HashMap对象

    >16.9.3线程安全的集合类

    实际上从Java5开始,在java util.concurrent包下提供了大量支持高效并发访问的集合接口和实现

    类,如图16.19所示。

    KV>

    图16.19线程安全的集合类

    从图16.19所示的类图可以看出,这些线程安全的集合类可分为如下两类。

    >以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap, ConcurrentSkip ListSet、

    Concurrent LinkedQueue N Concurrent LinkedDeque

    >以CopyOn Write开头的集合类,如CopyOn Write ArrayList、CopyOn Write Array Set

    其中以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访

    问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用

    了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。

    当多个线程共享访问一个公共集合时, ConcurrentLinkedQueue是一个恰当的选择。

    ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多

    个线程访问ConcurrentLinkedQueue集合时无须等待。

    在默认情况下, Concurrent Map支持16个线程并发写入,当有超过16个线程并发向该Map

    中写入数据时,可能有一些线程需要等待。实际上,程序通过设置concurrency Level构造参数(默认

    值为16)来支持更多的并发写入线程

    与前面介绍的HashMap和普通集合不同的是,因为ConcurrentLinkedQueue和Concurrent HashMap

    支持多线程并发访问,所以当使用迭代器来遍历集合元素时,该迭代器可能不能反映出创建迭代器之后

    所做的修改,但程序不会抛出任何异常。

    Java8扩展了ConcurrentHash Map的功能,Java8为该类新增了30多个新方法,这些方法可借助于

    Stream和Lambda表达式支持执行聚集操作。Concurrent Hash Map新增的方法大致可分为如下三类。

    forEach 91(for Each, forEachKey, forEach Value, for Each Entry)

    search y(search, searchKeys, search Values, search Entries)

    reduce 9U(reduce, reduce ToDouble, reduce ToLong, reduceKeys, reduce Values)

    除此之外, ConcurrentHash Map还新增了mapping CountO、new KeySet等方法,增强后的

    ConcurrentHash Map更适合作为缓存实现类使用。

    注:

    使用java. util包下的 Collection作为集合对象时,如果该集合对象创建迭代器后集合

    元素发生改变,则会引发ConcurrentModification Exception异常

    由于CopyOn Write ArraySet的底层封装了CopyOn Write Array List,因此它的实现机制完全类似于

    CopyOn Write Array List集合。

    对于CopyOn Write Array List集合,正如它的名字所暗示的,它采用复制底层数组的方式来实现写操作。

    当线程对CopyOn Write ArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻

    塞。当线程对CopyOn Write Array List集合执行写入操作时(包括调用addO、removeD、seto等方法),

    该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOn Write ArrayList

    集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

    需要指出的是,由于CopyOn WriteArray List执行写入操作时需要频繁地复制数组,性能比较差,但

    由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快、很安全。由

    此可见, CopyOn Write^ Array List适合用在读取操作远远大于写入操作的场景中,例如缓存等。

    1610本章小结

    本章主要介绍了Java的多线程编程支持;简要介绍了线程的基本概念,并讲解了线程和进程之间

    的区别与联系。本章详细讲解了如何创建、启动多线程,并对比了两种创建多线程方式之间的优势和劣

    势,也详细介绍了线程的生命周期。本章通过示例程序示范了控制线程的几个方法,还详细讲解了线程

    同步的意义和必要性,并介绍了两种不同的线程同步方法。另外也介绍了三种实现线程通信的方式。

    本章还介绍了JDK5新增的Callable和Future,使用Callable可以以第三种方式来创建线程,而Future

    则代表线程执行结束后的返回值,使用Callable和Future增强了Java的线程功能。最后本章介绍了池

    的相关类,这些也是Java多线程编程中的必需技能。

    >本章练习

    1.写2个线程,其中一个线程打印1~52,另一个线程打印A~Z,打印顺序应该是12A34B56C

    5152Z。该习题需要利用多线程通信的知识。

    2.假设车库有3个车位(可以用boolean(数组来表示车库)可以停车,写一个程序模拟多个用户

    开车离开、停车入库的效果。注意:车位有车时不能停车。

    相关文章

      网友评论

        本文标题:Java 多线程-Android面试准备2019-2-10

        本文链接:https://www.haomeiwen.com/subject/vawteqtx.html