[关闭]
@king 2015-10-10T02:33:50.000000Z 字数 24317 阅读 3767

多线程

Java



多线程


线程概述

当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程


线程和进程

进程(Process):处于运行过程中的程序,具有一定的独立功能。当一个程序进入内存运行时,即变成一个进程。进程是系统进行资源分配和高度的一个独立单位。

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己独有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
  • 动态性:进程与程序的区别在于程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中是不具备的
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响

并发性(concurrency):在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
并行性(parallel):在同一时刻,有多条指令在多个处理器上同时执行

大部分操作系统都支持多进程并发进行。对一个CPU而言,它在某个时间点只能运行一个进程。
多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。
线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。必须确保线程不会妨碍同一进程里的其他线程。
线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
一个线程可以创建和撤消另一个线程,同一个进程中的多个线程可以并发执行。
对多线程实现调度和管理以及资源分配由进程本身负责完成。


多线程的优势

线程在程序中是独立的、并发的执行流。线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。

当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多线程实现并发的性能要高得多。

Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程

多线程用途:浏览器同时下载多个图片,Web服务器同时响应多个用户请求,Java虚拟机本身在后台提供了一个超级线程来进行垃圾回收,GUID也需要启动单独的线程从主机环境收集用户界面事件……


线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流


继承Thread类创建线程类

继承Thread类创建线程类步骤如下:
<1>定义Thread类的子类,并重写该类的run()方法,该方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
<2>创建Thread子类的实例,即创建了线程对象
<3>调用线程对象的start()方法来启动该线程

主线程的线程执行体是由main()方法确定的

  1. public class FirstThread extends Thread{
  2. private int i;
  3. //重写run()方法
  4. public void run(){
  5. for(; i < 100 ; i++){
  6. //当线程类继承Thread类时,直接使用this即可获取当前线程
  7. //Thread对象的getName()返回当前线程的名字
  8. System.out.println(getName() + " 正在执行 " + i);
  9. }
  10. }
  11. public static void main(String[] args) {
  12. for(int i = 0; i < 100; i++){
  13. //调用Thread的currentThread()方法获取当前线程
  14. System.out.println(Thread.currentThread().getName() + " 正在执行 " + 1);
  15. if( i == 20){
  16. //创建并启动第一个线程
  17. new FirstThread().start();
  18. //创建并启动第二个线程
  19. new FirstThread().start();
  20. }
  21. }
  22. }
  23. }

默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为Thread-0、Thread-1……等。
由执行结果可知,两个线程输出的i变量不连续——注意:i变量是FirstThread的实例属性,而不是局部变量(方法体中定义的叫局部变量),但因为程序中每次创建线程对象时都需要创建一个FirstThread对象,所以Thread-0和Thread-1不能共享该实例属性。


实现Runnable接口创建线程类

实现Runnable接口创建线程类步骤如下:
<1>定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
<2>创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

  1. //创建Runnable实现类的对象
  2. SecondThread st = new SecondThread();
  3. //以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
  4. new Thread(st);
  5. //也可以在创建Thread对象时为该Thread对象指定一个名字
  6. new Thread (st, "新线程1");

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体,而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法

<3>调用线程对象的start()方法来启动该线程

  1. public class SecondThread implements Runnable {
  2. private int i;
  3. @Override
  4. public void run() {
  5. for(; i < 100; i++){
  6. //当线程类实现Runnable接口时,如果想获取当前线程,只能用Thread.currentThread()方法
  7. System.out.println(Thread.currentThread().getName() + " 线程执行 " + i);
  8. }
  9. }
  10. public static void main(String[] args) {
  11. for(int i = 0; i < 100; i++){
  12. System.out.println(Thread.currentThread().getName() + " 线程执行 " + i);
  13. if (i == 20){
  14. SecondThread st = new SecondThread();
  15. new Thread(st, "新线程1").start();
  16. new Thread(st, "新线程2").start();
  17. }
  18. }
  19. }
  20. }

运行程序,则输出可知,两个子线程的i变量是连续的,也就是采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例属性。


使用Callable和Future创建线程

从Java 5 开始,Java提供了Callable接口,该接口提供一个call()方法可以作为线程执行体,功能比run()方法更加大

  • call()方法可以有返回值
  • call()方法可以声明抛出异常

Callable接口不是Runnable接口的子接口,不能直接作为Thread的target。

Java 5 提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,它实现了Future接口,并实现了Runnable接口
Future接口里定义了几个公共方法来控制它关联的Callable任务

  • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future关联的Callable任务。
  • V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
  • V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值,如果等待给定的时间,就招聘TimeoutException异常。
  • boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。
  • boolean isDone():如果任务已完成,则返回 true。

创建并启动有返回值的线程的步骤如下:
<1>创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。
<2>创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
<3>使用FutureTask对象作为Thread对象的target创建并启动新线程
<4>调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

  1. package King.exercise;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.FutureTask;
  4. public class ThirdThread implements Callable<Integer> {
  5. //实现call()方法,作为线程执行体
  6. public Integer call(){
  7. int i = 0;
  8. for(; i < 100; i++){
  9. System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
  10. }
  11. return i;
  12. }
  13. public static void main(String[] args) {
  14. //创建Callable对象
  15. ThirdThread rt = new ThirdThread();
  16. //使用FutureTask来包装Callable对象
  17. FutureTask<Integer> task = new FutureTask<Integer>(rt);
  18. for(int i = 0; i < 100; i++){
  19. System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
  20. if(i == 20){
  21. //实质还是以Callable对象来创建并启动线程
  22. new Thread(task, "有返回值的线程").start();
  23. }
  24. }
  25. try{
  26. //获取线程返回值
  27. System.out.println("子线程的返回值:" + task.get());
  28. }
  29. catch (Exception ex){
  30. ex.printStackTrace();
  31. }
  32. }
  33. }

创建线程的三种方式对比

实现Runnable、Callable接口的方式基本相同,因为可以归为一种方式,这种方式与继承Thread方式之间的主要差别如下。
采用实现Runnable、Callable接口的方式创建多线程:

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类
  • 这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想
  • 劣势是编程稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法修改。

采用继承Thread类的方式创建多线程:

  • 劣势是,不能再继承其他父类
  • 编写简单。如果要访问当前线程,只须用this

一般推荐采用实现接口方式


线程的生命周期

当线程被创建并启动后,并不是一启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。CPU会在多线程之间切换,线程状态也会多次在运行、阻塞之间切换


新建和就绪状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态牺牲,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,牌这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于何时运行取决于JVM里线程调度器的调度。如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠1毫秒,在这1毫秒内CPU不会空闲,它会去执行另一个牌就绪状态的线程。
启动线程应该用start(),不能使用run()方法,否则会被当成一个普通的方法。


运行和阻塞状态

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。线程在运行过程中会被中断,使其他线程获得执行的机会,细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个进程时,系统会优先考虑进程 的优先级。
在某些小型设备如手机可能采用协作式调度策略,需要线程调用自己的sleep()或yield()方法放弃所战胜的资源。

发生如下情况时,线程进入阻塞状态

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起,这个方法容易导致死锁,应尽量避免使用。

当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态。


线程死亡

线程会以如下3种方式结束,结束后就处于死亡状态:

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受上线程的影响。
为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法。当线程牌新建、死亡2种状态时,返回false,否则返回true。


控制线程

join线程

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

  1. package King.exercise;
  2. public class JoinThread extends Thread {
  3. //构造器
  4. public JoinThread(String name){
  5. super(name);
  6. }
  7. //重写run()方法
  8. public void run(){
  9. for(int i = 0; i < 100; i++){
  10. System.out.println(getName() + " " + i);
  11. }
  12. }
  13. public static void main(String[] args) throws Exception {
  14. //启动子线程
  15. new JoinThread("新线程").start();
  16. for(int i = 0; i < 100; i++){
  17. if(i==20){
  18. JoinThread jt = new JoinThread("被Join的线程");
  19. jt.start();
  20. //main线程调用了jt线程的join()方法,main线程必须等jt线程执行结束后才会向下执行
  21. jt.join();
  22. }
  23. System.out.println(Thread.currentThread().getName() + " " + i);
  24. }
  25. }
  26. }

join()方法有3种重载形式:

  • void join():等待该线程终止。
  • void join(long millis):等待该线程终止的时间最长为 millis 毫秒。
  • void join(long millis, int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

后台线程

在后台运行,为其他线程提供服务的线程普查称为“后台线程”(Daemon Thread),又称“守护线程”、“精灵线程”,JVM的垃圾回收线程就是典型的后台线程
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDeamon(true)方法可将指定线程设置成后台线程
isDaemon()方法用于判断指定线程是否为后台线程。
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到作出响应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,否则会引发IllegalThreadStateException异常。


线程睡眠:sleep

sleep()是Thread类的静态方法。让当前正在执行的线程暂停一段时间,并进入阻塞状态。
线程在睡眠时间段内不会获得执行的机会,即使系统中没有其他可执行的线程。


线程让步:yield

yield()方法也是Thread类的静态方法。它不会阻塞该线程,只是让线程转入就绪状态。
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

sleep()和yield()方法的区别:

  • sleep()应运暂停当前优先级后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同或更高的线程执行机会。
  • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield()强制当前线程进入就绪状态。
  • sleep()方法声明抛出了InterruptedException异常。而yield()方法没有声明抛出任何异常
  • sleep()方法比yield()有更好的可逢凶化吉性。通常不建议后者来控制并发线程的执行

改变线程优先级

每个线程的默认优先级都与创建它的父线程的优先级相同。main线程默认具有普通优先级。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。优先级参数为1~10的整数。详见API
不同操作系统上的优先级并不相同,不能很好地和Java的10个优先级对应,应尽量使用三个静态常量 来设置优先级,以保证可移植性。


线程同步


线程安全问题

银行取钱问题。不懂的自己百度


同步代码块

Java使用同步监视器来解决线程安全问题,使用同步监视器的通用方法就是同步代码块

  1. synchronized(obj){ //obj就是同步监视器
  2. ...
  3. // 代码
  4. }

线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只有一个线程可以获得对同步监视器的锁定。同步代码块执行完后,该线程会释放对该同步监视器的锁定。
Java允许使用任何对象作为同步监视器,通常推荐使用可能被并发访问的共享资源充当同步监视器。
任何线程在修改指定资源前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。这种方式保证任一时刻只有一个线程可以进入修改共享资源的代码区(也称临界区),从而保证线程安全性。


同步方法

使用 synchronized 关键字来修饰某个方法,则该方法称为同步方法。同步方法无须显式指定监视器,同步方法的同步监视器是this,也就是该对象本身。
使用同步方法可以非常方便地实现线程安全的类。
面向对象里有一种流行的设计方式:Domain Driven Design(领域驱动设计,DDD),这种方式认为每个类都应该是一个完备的领域对象。
可变类的线程安全是以降低程序的运行效率作为代价的,为减少线程安全所带来的负面影响,程序可以采用如下策略:

  • 不要对线程安全类的所有方法都进行同步。只对那些会改变竞争资源(也就是共享资源)的方法进行同步
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本。线程不安全和线程安全版本。

释放同步监视器的锁定

程序无法显式释放对同步监视器的锁定。当方法执行结束或中断(break、return、异常)或程序执行了同步监视器对象的wait()方法时,会释放对同步监视器的锁定
Thread.sleep()、Thread.yield()方法,线程的suspend()方法,都不会释放同步监视器。


同步锁(Lock)

Java 5 开始提供更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象来充当。Lock提供了更广泛的锁定操作允许更灵活的结构,可以具有差别很大的属性,并支持多个相关的Condition对象
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
Lock、ReadWriteLock是Java 5 提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantReadWriteLock实现类
在实现线程安全的控制中,比较常用 的是ReentrantLock(可重入锁),使用该Lock对象可以显式地加锁、释放锁。

  1. class X{
  2. // 定义锁对象
  3. private final ReentrantLock lock = new ReentrantLock();
  4. // ...
  5. // 定义需要保证线程安全的方法
  6. public void m(){
  7. // 加锁
  8. lock.lock();
  9. try{
  10. //需要保证线程安全的代码
  11. }
  12. //使用finally块来保证释放锁
  13. finally{
  14. lock.unlock();
  15. }
  16. }
  17. }

使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象(加了同步锁的类的对象)作为同步监视器。
当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
Lock还提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁。所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。


死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测也没有采取措施来处理死锁情况,所有线程处于阻塞状态,无法继续。
系统中出现多个同步监视器的情况下很容易出现死锁。


线程通信


传统的线程通信

由同步监视器对象调用Object类提供的如下三个方法:

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。
  • notify():唤醒在此同步监视器上等待的单个线程,如有多个则任意选择一个。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。

使用Condition控制线程通信

如果程序不使用synchronized而是改用Lock对象来保证同步,则系统中不存在隐匿的同步监视器,也就不能使用wait()、notify()、notiryAll()方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他牌等待的线程。
Condition将同步监视方法(wait()、notify()、notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
调用Lock对象的newCondition()方法来获取特定Lock实例的Condition实例。Condition类提供了如下3个方法:

  • await():类似wait(),直接其他线程调用该Condition的signal()或signalAll()方法来唤醒该线程。该await()方法有更多谈何,如long awaitNanos(longnanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等。
  • signal():唤醒在此Lock对象上等待的单个线程。若有多个线程则任意。
  • signalAll():类似notifyAll()

使用阻塞队伍(BlockingQueue)控制线程通信

Java 5 提供了一个BlockingQueue接口,虽然它也是Queue的子接口,但它主要用途并不是作为容器,而是作为线程同步的工具。
BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue提供如下两个支持阻塞的方法:

  • put(E e):深度把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
  • take():深度从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

由于BlockingQueue继承了Queue接口,也可以使用Queue中的方法,归纳起来分为三组:

  • 在队列尾部插入元素,包括add(E e)、offer(E e)和put(E e)。当队列已满时,这3个方法分别抛出异常、返回false、阻塞队列。
  • 在队列头部删除并返回删除的元素。包括remove()、poll()和take()方法。若队列已空,这3个方法分别抛出异常、返回false、阻塞队列。
  • 在队列头部取出但不删除元素,包括element()和peek()方法,若队列已空,这2个方法分别抛出异常、返回false。

BlockingQueue包含的方法之间的对应关系

队尾插入元素 add(e) offer(e) put(e)
队头删除元素 remove() poll() take()
获取、不删除元素 element() peek()

BlockingQueue包含如下5个实现类

  • ArrayBlockingQueue :基于数组实现的BlockingQueue队列
  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列
  • PriorityBlockingQueue:它并不是标准的阻塞队列,与前面介绍的PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
  • SynchronousQueue:同步队列,对该队列的存、取操作必须交替进行
  • DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序。
  1. // 定义一个长度为2的阻塞队列
  2. BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
  3. bq.put("Java"); // 与bq.add("Java")、bq.offer("Java")相同
  4. bq.put("Java");
  5. bq.put("Java"); //阻塞线程
  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. import javax.xml.stream.events.StartDocument;
  4. class Producer extends Thread{
  5. private BlockingQueue<String> bq;
  6. public Producer(BlockingQueue<String> bq) {
  7. this.bq = bq;
  8. }
  9. public void run(){
  10. String[] strArr = new String[]{
  11. "java", "Struts", "Spring"
  12. };
  13. for(int i = 0; i < 999999999; i++){
  14. System.out.println(getName() + "生产者准备生产集合元素");
  15. try{
  16. Thread.sleep(200);
  17. //尝试放入元素,如果队列已满,则线程被阻塞
  18. bq.put(strArr[i % 3]);
  19. }
  20. catch (Exception ex){
  21. ex.printStackTrace();
  22. }
  23. System.out.println(getName() + "生产完成:" + bq);
  24. }
  25. }
  26. }
  27. class Consumer extends Thread{
  28. private BlockingQueue<String> bq;
  29. public Consumer(BlockingQueue<String> bq){
  30. this.bq = bq;
  31. }
  32. public void run(){
  33. while(true){
  34. System.out.println(getName() + "消费者准备消费集合元素" );
  35. try{
  36. Thread.sleep(200);
  37. //深度取出元素,如果队列已空,则线程被阻塞
  38. bq.take();
  39. }
  40. catch (Exception e){
  41. e.printStackTrace();
  42. }
  43. System.out.println(getName() + "消费完成" + bq);
  44. }
  45. }
  46. }
  47. public class BlockingQueueTest {
  48. public static void main(String[] args) {
  49. //创建一个容量为1的BlockingQueue
  50. BlockingQueue<String> bq = new ArrayBlockingQueue<String>(1);
  51. //启动3个生产者线程
  52. new Producer(bq).start();
  53. new Producer(bq).start();
  54. new Producer(bq).start();
  55. //启动1个消费者线程
  56. new Consumer(bq).start();
  57. }
  58. }

线程组和未处理的异常

Java使用ThreadGroup来表示线程组,可以对一批线程进行分类管理。默认情况下,子线程和创建它的父线程处于同一个线程组内。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
Thread类提供了3个构造器来设置新创建的线程属于哪个线程组:

  • Thread(ThreadGroup group, Runnable target)
  • Thread(ThreadGroup group, Runnable target, String name)
  • Thread(ThreadGroup group, String name)

ThreadGroup 提供了2个简单的构造器来创建实例

  • ThreadGroup(String name)
  • ThreadGroup(ThreadGroup parent, String name)

ThreadGroup提供了如下几个常用 的方法业操作整个线程组里的所有线程

  • int activeCount():返回线程组中活动线程的数目
  • interrupt():中断此线程组中所有线程
  • isDaemon():判断该线程组是否后台线程组
  • setDaemon(boolean daemon):把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁
  • setMaxPriority(int pri):设置线程组的最高优先级

ThreadGroup内还定义了一个很有用的方法:void uncaughtException(Thread t, Throwable e),处理该线程组内的任意线程所抛出的未处理异常。从Java 5 开始,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会自动调用uncaughtException()方法来处理异常。
Thread类提供了如下两个方法来设置异常处理器

  • static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器
  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器。

ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理对象。当一个线程抛出未处理异常时,JVM会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler()方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM将会调用该线程所属的线程对象的uncaughtException()方法来处理该异常。默认流程如下:

  • 如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常
  • 如果该线程实例所属的线程类有默认的异常处理器,那么就调用该异常处理器来处理异常
  • 如果该异常对象是ThreadDeath的对象,则不做任何处理,否则将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程

线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。这种情况下使用线程池可以很好地提高性能。
线程池在系统启动时即创建大量的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态。
线程池的最大线程参数可以控制系统中并发线程数。


Java 5 实现的线程池

Java 5 新建了一个Executors工厂类来产生线程池。包含如下几个静态工厂方法来创建线程池

  • newCachedThreadPool():创建一个具有缓存功能的线程池
  • newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池
  • newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThreadpool()时传入参数为1
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务
  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务

前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;而后2个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。
ExecutorService代表尽快执行线程的线程池,程序只要将一个Runnable对象或Callable对象提供给该线程池,就会尽快执行该任务。ExecutorService提供了如下3个方法:

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

ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。其方法详见API。

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

使用线程池来执行线程任务的步骤如下:
<1> 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
<2>创建Runnable实现类或Callable实现烊的实例,作为线程执行任务
<3>调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例
<4>当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池,该方法试图停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

  1. // 创建一个具有固定线程数的线程池
  2. ExecutorService pool = Executors.newFixedThreadPool(5);
  3. // 向线程池中提交两个线程
  4. pool.submit(new MyThread()); //MyThread为自定义的Runnable实现类
  5. pool.submit(new MyThread());
  6. pool.shutdown();

Java 7 新增的ForkJoinPool
为充分利用多CPU、多核CPU的性能优势,可以考虑把一个任务拆分成多个小任务,把多个小任务放到多个处理器核心上并行执行,当多个小任务执行完成之后,再将这些执行结果合并起来即可。
Java 7 提供了ForkJoinPool来支持上述过程。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。ForkJoinPool提供了如下两个常用构造器

  • ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool

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

下面以执行没有返回值的“大任务”(简单打印0~300的数值)为例,程序将一个大任务拆成多个小任务,并将任务交给ForkJoinPool来执行

  1. package King.exercise;
  2. import java.util.concurrent.ForkJoinPool;
  3. import java.util.concurrent.RecursiveAction;
  4. import java.util.concurrent.TimeUnit;
  5. //继承RecursiveAction来实现可分解的任务
  6. class PrintTask extends RecursiveAction{
  7. //每个小任务最多只打印50个数
  8. private static final int THRESHOLD = 50;
  9. private int start;
  10. private int end;
  11. //打印从start到end的任务
  12. public PrintTask(int start, int end){
  13. this.start = start;
  14. this.end = end;
  15. }
  16. @Override
  17. protected void compute(){
  18. //当end与start之间的差小于THRESHOLD时,开始打印
  19. if(end - start < THRESHOLD){
  20. for (int i = start ; i < end; i++){
  21. System.out.println(Thread.currentThread().getName() + "的i值" + i);
  22. }
  23. }
  24. else{
  25. //将end与start之间的差大于THRESHOLD时,将大任务分解成小任务
  26. int middle = (start + end)/2;
  27. PrintTask left = new PrintTask(start, middle);
  28. PrintTask right = new PrintTask(middle, end);
  29. //并行执行两个小任务
  30. left.fork();
  31. right.fork();
  32. }
  33. }
  34. }
  35. public class ForkJoinPoolTest {
  36. public static void main(String[] args) throws Exception {
  37. ForkJoinPool pool = new ForkJoinPool();
  38. //提交可分解的PrintTask任务
  39. pool.submit(new PrintTask(0, 300));
  40. pool.awaitTermination(2, TimeUnit.SECONDS);
  41. pool.shutdown();
  42. }
  43. }

下面程序示范了使用RecursiveTask对一个长度为100的数组的元素值进行累加

  1. package King.exercise;
  2. import java.util.Random;
  3. import java.util.concurrent.ForkJoinPool;
  4. import java.util.concurrent.Future;
  5. import java.util.concurrent.RecursiveTask;
  6. //继承RecursiveTask来实现可分解任务
  7. class CalTask extends RecursiveTask<Integer>{
  8. //每个小任务最多只累加20个数
  9. private static final int THRESHOLD = 20;
  10. private int[] arr;
  11. private int start;
  12. private int end;
  13. //累加从start到end的数组元素
  14. public CalTask(int[] arr, int start, int end){
  15. this.arr = arr;
  16. this.start = start;
  17. this.end = end;
  18. }
  19. @Override
  20. protected Integer compute(){
  21. int sum = 0;
  22. //当end 与start之间的差小于THRESHOLD时,开始实际累加
  23. if(end - start < THRESHOLD){
  24. for(int i = start ; i < end; i++){
  25. sum += arr[i];
  26. }
  27. return sum;
  28. }
  29. else{
  30. //当end与start之间的差大于THRESHOLD,即要打印的数超过20个时,将大任务分解成两个“小任务”
  31. int middle = (start + end)/2;
  32. CalTask left = new CalTask(arr, start, middle);
  33. CalTask right = new CalTask(arr, middle, end);
  34. //并行执行两个小任务
  35. left.fork();
  36. right.fork();
  37. //把两个小任务的结果合并起来
  38. return left.join() + right.join();
  39. }
  40. }
  41. }
  42. public class Sum {
  43. public static void main(String[] args) throws Exception {
  44. int[] arr = new int[100];
  45. Random rand = new Random();
  46. int total = 0;
  47. //初始化100个数字元素
  48. for(int i = 0, len = arr.length; i < len; i++){
  49. int tmp = rand.nextInt(20);
  50. //对数组元素赋值,并将数组元素的值添加到total总和中
  51. total += (arr[i] = tmp);
  52. }
  53. System.out.println(total);
  54. ForkJoinPool pool = new ForkJoinPool();
  55. //提交可分解的CalTask任务
  56. Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
  57. System.out.println(future.get());
  58. pool.shutdown();
  59. }
  60. }

线程相关类

Java为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。除此之外,Java 5还新增了大量的线程安全类。


ThreadLocal类

ThreadLocal是Thread Local Variable (线程局部变量)的意思。它为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。
它只提供如下3个public 方法

  • T get():返回此线程局部变量中当前线程副本中的值
  • void remove():删除此线程局部变量中当前线程的值
  • void set(T value):设置此线程局部变量中当前线程副本中的值
  1. package King.exercise;
  2. class Account{
  3. //定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量,每个线程都会保留该变量的一个副本
  4. private ThreadLocal<String> name = new ThreadLocal<String>();
  5. //定义一个初始化name属性的构造器
  6. public Account(String str){
  7. this.name.set(str);
  8. System.out.println("---" + this.name.get());
  9. }
  10. //name的setter和getter方法
  11. public String getName(){
  12. return name.get();
  13. }
  14. public void setName(String str){
  15. this.name.set(str);
  16. }
  17. }
  18. class MyTest extends Thread{
  19. private Account account;
  20. public MyTest(Account account, String name){
  21. super(name);
  22. this.account = account;
  23. }
  24. public void run(){
  25. for(int i = 0; i < 10; i++){
  26. if( i == 6){
  27. //将账户名替换成当前线程名
  28. account.setName(getName());
  29. }
  30. System.out.println(account.getName() + "账户的i值" + i);
  31. }
  32. }
  33. }
  34. public class ThreadLocalTest {
  35. public static void main(String[] args) {
  36. Account at = new Account("初始名");
  37. //虽然两个线程共享同一个账户 ,即只有一个账户名,但由于账户名是ThreadLocal类型的,所以每个线程都完全拥有各自的账户名副本,因此在i=6之后,将看到两个线程访问同一个账户时出现不同的账户名
  38. new MyTest(at, "线程甲").start();
  39. new MyTest(at, "线程乙").start();
  40. }
  41. }

输出:

---初始名
null账户的i值0
null账户的i值1
null账户的i值2
null账户的i值3
null账户的i值4
null账户的i值0
null账户的i值5
null账户的i值1
线程甲账户的i值6
null账户的i值2
线程甲账户的i值7
null账户的i值3
线程甲账户的i值8
null账户的i值4
线程甲账户的i值9
null账户的i值5
线程乙账户的i值6
线程乙账户的i值7

ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突。普通同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所以要使用这种同步机制,需要很细致夫什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情况下,系统并没有将这份资源复制多份,只是采用安全机制来控制对这份资源的访问。
ThreadLocal从另一角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,从而没有必要对该变量进行同步了。
ThreadLocal提供了线程安全的共享对象,在编写多线程袋里 ,可以把不安全的整个变量封装晕ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。
ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争。
如果多个线程之间需要共享资源,就使用同步机制;如果仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal。


包装线程不安全的集合

ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap都是线程不安全的。如果程序中有多个线程可能访问以上这些集合,那么我们可以使用Collections提供的静态方法把这些集合包装成线程安全的集合。详见API。
应该在创建集合对象后立即包装。


线程安全的集合类

从Java 5 开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口的实现类。
主要分为如下两类:

  • 以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet

以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好性能。
当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择,它不允许使用null元素。多个线程访问ConcurrentLinkedQueue集合时无须等待。
默认情况下,ConcurrentHashMap支持16个线程并发写入。超过16个则需等待。也可以通过设置构造参数来支持更多的并发写入线程
当用迭代器来遍历集合元素时,该迭代器可能不能反映出创建迭代器之后所做的修改,但程序不会抛出任何异常。
当我们使用java.util急下的Collection作为集合对象时,如果该集合对象创建迭代器后集合元素发生改变,则会引发ConcurrentModificationException异常

由于CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于后者。
CopyOnWriteArrayList集合采用复制底层数组的方式来实现写操作
当线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对它执行写入操作时(包括调用add()、remove()、set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。
同样由于频繁复制数组,性能较差,但由于读操作与写操作不是操作同一数组,而且读操作也不需要加锁,因此读操作就很快、很安全。由此可铜陵,CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,例如缓存。
P768


原子动作

不能分割交织的操作乘称作原子动作,这些动作一旦发生,便不能在中途停止,要么完全发生,要么根本不发生,直至动作结束。虽然大部分Java语句都不是原子动作,但是也有一些动作可以认定为是原子性的:

  • 1.引用类型变量值的读和写。注意这儿是引用值的读写,而不是所引用对象内容的读和写。
  • 2.除了long和double之外的简单类型的读和写。
  • 3.所有声明为volatile的变量的读和写,包括long和double类型以及引用类型

final字段的访问是不需要互斥的。因为一旦初始化完毕,这些字段只能进行读操作,因此可被不同线程之间安全共享。


用匿名内部类开启多线程

  1. public class Deadlock {
  2. static class Friend {
  3. private final String name;
  4. public Friend(String name) {
  5. this.name = name;
  6. }
  7. public String getName() {
  8. return this.name;
  9. }
  10. public synchronized void bow(Friend bower) {
  11. System.out.format("%s: %s has bowed to me!%n",
  12. this.name, bower.getName());
  13. bower.bowBack(this);
  14. }
  15. public synchronized void bowBack(Friend bower) {
  16. System.out.format("%s: %s has bowed back to me!%n",
  17. this.name, bower.getName());
  18. }
  19. }
  20. public static void main(String[] args) {
  21. final Friend a = new Friend("A");
  22. final Friend b = new Friend("B");
  23. // 居然可以这样
  24. new Thread(new Runnable() {
  25. public void run() { a.bow(b); }
  26. }).start();
  27. new Thread(new Runnable() {
  28. public void run() { b.bow(a); }
  29. }).start();
  30. }
  31. }

同步技术

解决多线程按照特定顺序访问共享数据的技术称作同步。同步技术最常见的编程范式是同步保护块。这种编程范式在操作前先检测某种条件是否成立,如成立则继续操作;如不成立则有两种选择,一种是简单的循环检测,直至此条件条件成立:

  1. public void guardedOperation(){
  2. while(!condition_expression){
  3. System.out.println("Not ready yet, I have to wait again!");
  4. }
  5. }

这种方法非常消耗CPU资源,任何情况下都不应该使用这种方法。

另种更好的方式是条件不成立时调用Object.wait方法挂起当前线程,使它一直等待,直至另一个线程发出激活事件。当然该事件不一定是当前线程希望等待的事件。


创建常量对象Immutable Object

下面的准则列出了通常创建常量对象的策略,当然不是创建所有“常量”对象都要符合下面的准则。

  • 1.不要提供setter方法,这些方法会修改字段以及由字段引用的对象。
  • 2.将字段声明为final和private访问类型。
  • 3.不允许子类覆盖方法,最简单的办法是将类定义为final。更复杂的方法是将构造函数定义为私有,并使用factory方法生成实例。
  • 4.如果实例字段中包括可变对象的引用,则不允许这些对象被改变,也就是不要提供修该可变对象的方法。
  • 5.不要将可变对象的引用共享;不要将可变对象的引用外部序列化;传递给构造函数的可变对象应复制备份赋给字段;不要返回原始对象,而应返回对象的复制。

但是同样也应该注意到,Immutable Object也会带来副作用,就是前面所说的小对象创建与销毁所带来的开销。Java的所有对象都是在内存堆而不是运行栈中分配,因此对象的创建、销毁及回收的开销都是比较大的。如果垃圾收集不是太及时,就会造成一定时间内类似于内存泄漏的现象,这也是Swing应用程序内存开销比较大的一个重要原因。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注