线程总结
创建、启动线程
extends Thread:通过继承获取当前线程this(常省略)
implements Runnable|Callable:通过实现接口获取当前线程Thread.currenThread()
线程的声明周期
1) 新建new;
2) 就绪start(),但并未立即执行,让当前线程(主线程)睡眠1ms:Thread.sleep(1);
3) 运行:执行run()方法中的线程执行体;
4) 阻塞
5) 死亡
控制线程
join():由当前使用线程的程序调用线程对象.join(),调用join()方法的线程将先执行。
setDaemonThread()设置成后台线程。
sleep():暂停,进入阻塞。
yield():暂停,进入就绪。
同步
1) 同步代码块
同步方法:非static的同步监视器为this,即为调用该方法的对象
2) 同步锁(Lock):显式定义同步对象
Lock的实现类:ReentrantLock(可重入锁:一个线程可以对已被加锁的ReentrantLock锁再次加锁。)
ReadWriteLock(读写锁)的实现类:ReentrantReadWriteLock <- Java8新增的StampedLock可代替它。
线程通信
程序通常无法准确地控制线程的轮换执行,但java提供了一些机制来保证线程协调运行。
1) synchronized:wait()、notify()、notifyAll()。
2) Lock对象:await()、signal()、signalAll()。
3) BlockingQueue(阻塞队列)
ThreadGroup
可对多个线程同时控制
线程池
系统启动一个新线程的成本是比较高的,因为它涉及操作系统交互。当系统中需要创建大量生存周期短暂的线程时,更应该考虑用线程池。
线程安全
注意:
this关键字总是指向调用该方法的对象:
(1)在构造器中使用this引用时,this总是引用该构造器正在初始化的对象。对构造器正在初始化的对象的成员变量赋值。
(2)在方法中引用调用该方法的对象。它所代表的对象只能是当前类,只有该方法被调用时,this所代表的对象才被确定下来。谁在调用这个方法,this就代表谁。
super关键字
在子类中访问被覆盖的父类变量:super.a
子类构造器调用父类构造器:super(a),其中a是变量
类若没有提供构造器,使用它的静态方法来获取实例。
所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程有一定的独立功能,是系统进行资源分配和调度的一个独立单位。
进程的三个特征:独立性、动态性、并发性。
并发性(concurrency)和并行性(parallel)不同。并行是指在同一时刻,有多条指令在多个处理器上同时执行;并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。
对于一个CPU而言,它在某个时间点只能执行一个程序,即只能执行一个进程。
进程之间不能共享内存,但线程之间共享内存非常容易。
线程的创建和启动
所有的线程对象都必须是Thread类或其子类的实例。有三种方式创建线程类:
继承Thread类创建线程类
1) 定义Thread类的子类,并重写该类的run()方法,run()方法的方法体就代表了线程需要完成的任务,因此run()方法称为线程执行体;
2) 创建线程对象,即创建Tread子类的实例;
3) 调用线程对象的start()方法来启动该线程。
Java程序开始运行时,至少会创建一个主线程,主线程的执行体是由main方法确定的,main方法的方法体就是主线程的线程执行体。
1 | /* |
输出:
1 | main 0 |
Thread.currentThread()方法:currentThread()是Thread类的静态方法,总是返回当前正在执行的线程对象。(静态方法,可直接由类调用)
getName():是Thread类的实例方法,返回调用该方法的线程名字。(实例方法,需要对象调用)
可用setName(String name)为线程设置名字。默认情况下,主线程名字为main,用户启动的多个线程的名字为Thread-0、Thread-1…。
使用继承Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量。
实现Runnable接口创建线程类
1) 定义Ruannable接口的实现类,并重写该接口的run()方法;
2) 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。(Runnable对象仅仅作为Thread对象的target)
SecondThread st = new SecondThread();
new Thread(st);
或new Thread(st, “新线程1”); //可以在创建Thread对象时为该Thread对象指定一个名字
3) 调用线程对象的start()方法来启动线程。
1 | /* |
多个线程可以共享同一个target,所以多个线程可以共享同一个线程类的实例变量。
通过继承来获得当前线程比较简单,直接用this就可以了;但通过Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法。
使用Callable接口和FutureTask创建线程(实现Callable接口与实现Runnable接口的方式基本相同)
1) 创建Callable接口的实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,允许声明抛出异常,在创建Callable实现类的实例;
Callable接口是函数式接口,可以通过Lambda表达式创建Callable对象。函数式接口 <-> Lambda表达式。若不使用Lambda表达式,要创建实例,需要先实现类实现Callable接口,再创建实现类的对象,较繁琐。
2) 使用FutureTask类包装Callable对象;
3) 使用FutureTask对象最为Thread对象的target创建并启动线程;
4) 调用FutureTask对象的get()方法获得子线程执行结束后的返回值。
1 | /* |
函数式接口是只包含一个抽象方法的接口。使用匿名内部类来实例化函数式接口的对象,有了Lambda表达式,这一方式可以简化。
线程的声明周期
经历的状态:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
新建
new创建线程,JVM为其分配内存,并初始化成员变量。
就绪
线程对象调用了start()方法后,该线程将处于就绪状态。(调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理。)JVM会为其创建方法调用栈和程序计数器。此时该线程还没运行,只与该线程何时开始运行,取决于JVM里线程调度器的调度。
如果希望调用子线程的start()方法后子线程立即开始执行,可以让当前线程(主线程)睡眠1毫秒:Thread.sleep(1);
运行
处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体。
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短),线程在运行时需要被中断,目的是是其他线程获得执行的机会。
所有现代的桌面和操作系统都采用抢占式调度策略,系统会给每个可执行的线程一小段时间来处理任务,当该段时间用完后,系统会剥夺该线程所占用的资源,让其他线程获得执行的机会。系统会考虑线程的优先级。
阻塞
死亡
控制线程
joint线程
由使用线程的程序调用joint()方法,则调用线程(如main线程)将会被阻塞,直到被join()方法加入的join线程执行完为止。
后台线程(Daemon Thread)
setDaemonThread()设置成后台线程。
JVM的垃圾回收线程就是后台线程。
sleep()暂停,进入阻塞。
yield():暂停,线程让步,进入就绪。
改变线程优先级
MAX_PRIORITY(10)、MIN_PRIORITY(1)、NORM_PRIORITY(5)。
线程同步
一个账户多个人银行取钱问题:当两个进程并发修改同一个文件时,容易出现该问题。
同步代码块
Java多线程引入了同步监视器,使用同步监视器的通用方法是同步代码块:
1 | synchronized(obj){ |
obj就是同步监视器:阻止两个线程对同一个共享资源进行并发访问。
线程开始执行同步代码块之前,必须先获得同步监视器的锁定。
任何时候只能有一个线程可以获得同步监视器的锁定。
同步方法
对于synchronized修饰的实例方法(没有static修饰的方法)无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
释放同步监视器的锁定
同步锁(Lock)
Java5提供了功能更强大的同步机制:通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
Lock提供了比synchronized代码块和synchronized方法更广泛的锁定操作。
Lock是控制多个线程对共享资源进行访问的工具,每次只能有一个线程对Lock对象加锁。
Java5提供的两个根接口:Lock、ReadWriteLock,并为Lock提供ReentrantLock(可重入锁)实现类,为ReadWriteLock提供ReentrantReadWrieteLock实现类。Java8新增StampedLock类,大多数场景下可代替ReentrantReadWrieteLock实现类。
可重入即一个线程可以对已被加锁的ReentrantLock再次加锁。
在实现线程安全的控制中,ReentrantLock(可重入锁)对象可以显示地加锁、释放锁。通常代码格式如下:
1 | class X{ |
同步锁与使用同步方法有点类似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。
死锁
两个线程相互等待对方释放同步监视器时就会发生死锁。Java虚拟机没有检测,也没有采取措施来处理死锁情况,因此多线程编程时应该采取措施释放死锁。
一旦死锁发生,所有线程处于阻塞状态,无法运行。程序不会发生任何异常,程序不会向下执行,也不会给出任何提示。
由于Thread类的suspend()方法也很容易导致死锁,因此java推荐不适用这个方法暂停线程的执行。
线程通信
程序通常无法准确地控制线程的轮换执行,但java提供了一些机制来保证线程协调运行。
传统的线程通信
Object类的wait()、notify()、notifyAll()方法,这三个方法必须由同步监视器对象来调用。
例注意点:取钱线程已经执行结束,等待其他线程来取钱,并不是等待其他线程释放同步监视器。不要把程序阻塞和死锁等同!
使用Condition控制线程通信
不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器。
当使用Lock对象来保证线程同步是,Java提供了一个Condition类来保持协调。获取Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。
Condition类的await()、signal()、signalAll()方法。
这种控制线程同步方式与synchronized关键字来控制线程同步(即1)的步骤基本相似。
1 | private final Lock lock = new ReentrantLock(); |
使用阻塞队列(BlockingQueue)控制线程
Java5提供了一个BlockingQueue接口,它是Queue的子接口,但主要作用不是作为容器,而是作为线程同步工具。
BlockingQueue的特征:当生产者试图向BlockingQueue中放入元素时,如队列已满,则该线程被阻塞;当消费者试图从BlockingQueue中取出元素时,如队列为空,则该线程被阻塞。
以ArrayBlockingQueue测试阻塞队列的put()方法的用法:
1 | public class BlockingQueueTest{ |
对于上面程序,队列未满放入元素时,使用put()、add()、offer()方法的效果完全一样。放入两个元素后,如果使用put()方法放入元素会阻塞线程;使用add()方法会引发异常;使用offer()方法会返回false,元素不会放入。
1 | import java.util.concurrent.BlockingQueue; |
1 | import java.util.concurrent.BlockingQueue; |
1 | import java.util.concurrent.ArrayBlockingQueue; |
上面程序,Thread-0、Thread-1、Thread-2线程都必须等到Thread-3执行后才能执行。
线程组和未处理的异常
ThreadGroup表示线程组,它可以对一批线程进行分类管理,对线程组的控制相当于程序直接同时控制这批线程。中途不可改变线程所属线程组,直到该线程死亡。
默认情况下,A线程创建了B线程,在没有指定B线程线程组的情况下,子线程B线程和创建它的父线程A同属一个线程组。
ThreadGroup的void uncaughtException(Thread t, Throwable e)方法,处理线程组内任意线程所抛出的未处理异常。t表示出现异常的线程,e表示该线程抛出的异常。
Thread.UncaughtExceptionHandler是Thread类的一个静态内部借款,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e)方法。
ThreadGroup类实现了Thread.UncaughtExcepitonHandler接口,因此每个线程所属的线程组将会作为默认的异常处理器。
当一个线程抛出未处理异常时,通常的线程组处理异常的流程如下:
1) 若设置了默认异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),调用该异常处理器来处理异常。
2) 若该线程有父线程,调用父线程组的uncaughtExcepiton()方法来处理异常。
3) 如果该异常对象时ThreadDeath的对象,不做任何处理,否则System.err打印错误输出流,并结束该线程。
1 | /* |
1 | /* |
Thread.currentThread()表示当前线程,放在main()方法中就表示主线程。
虽然程序中粗体diamante指定了异常处理器对未捕获的异常进行处理,但程序依然不会结束,这与catch不同:catch捕获的异常不会向上传播给调用者,但使用异常处理器对异常进行处理后会将异常传播给上一级调用者。
线程池
系统启动一个新线程的成本是比较高的,因为它涉及操作系统交互。当系统中需要创建大量生存周期短暂的线程时,更应该考虑用线程池。除此之外,使用线程池还可有效控制系统中并发线程的数量。
Java8改进的线程池
与数据库连接池类似的是,当系统启动时,线程池创建大量空闲的线程,程序将一个Runnable对象会Callable对象传给线程池,线程池会启动给一个线程来执行它们的run()方法和call()方法,当run()方法和call()方法结束后,程序并不会死亡,而是再次返回线程池中成为空闲状态,等待下一次程序将一个Runnable对象会Callable对象传给线程池。
Executors工厂类来产生线程池。
使用线程池来执行线程任务的步骤:
1) 调用Executors类的静态工厂方法创建一个ExecutorService对象(7中创建方式),该对象代表一个线程池;
2) 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
3) 调用ExecutorService对象的submit()方法来提交Runnable实现类或Callable实现类的实例;(7中提交方式)
4) 当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
关于上述第2)步,创建了实现类之后没有直接像之前一样创建线程、执行线程来执行实例对象所代表的任务,而是通过3)4)步的线程池来执行该任务。
System.exit(0); //终止虚拟机
Java8增强的ForkJoinPool
为了充分利用多CPU、多核CPU的优势,java7提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。
RecursiveAction抽象类代表没有返回值的任务,如打印;RecursiveTask代表有返回值的任务,如将多个数相加。
1 | import java.util.concurrent.RecursiveAction; |
1 | import java.util.concurrent.ForkJoinPool; |
输出:
1 | ForkJoinPool-1-worker-2的i值7 |
可见,ForkJoinPool启动了4个线程来执行这个打印任务,这是因为计算机的CPU是四核的。而且可以看出这20个数不是连续打印的,而是进行了分集。
线程相关类
ThreadLocal类
java为线程安全提供了一些工具类,如ThreadLocal
通过使用ThreadLocal
本质:为每个使用该变量的线程都提供一个变量值的副本,使每个线程都可以独立地改变自己的副本,不会和其他线程的副本冲突。
同步机制与ThreadLocal不同:同步机制是为了同步对个线程对相同资源的并发访问,是多个线程之间进行通行的有效方式;而ThreadLocal是为了给多个线程的数据共享,从根本上解决多个线程之间对共享资源(变量)的竞争,也就不需要多个线程进行同步。
通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;
如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
1 | class Account{ |
1 | class MyTest extends Thread{ |
1 | public class ThreadLocalTest{ |
输出:
1 | 初始名 //主线程访问账户名有值 |
可以看出账户名有3个副本,它们的值互不干扰,各自完全拥有自己的ThreadLocal变量,这就是ThreadLocal变量的用户。
包装线程不安全的集合
ArraList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的集合。线程不安全:当多个并发线程向这些集合中存、取元素时,可能会破坏这些集合的数据完整性。
使用Collections提供的类方法(静态方法)把这些集合包装成线程安全的。
如需要在多线程中使用线程安全的HashMap集合:
1 | HashMap m = Collections.synchronizedMap(new HashMap()); //在集合创建后立即包装 |