多线程——线程创建和启动
进程
当一个程序进入内存中运行时,即变成一个进程,其具有一定的独立功能,时系统进行资源分配和调度一个独立单位。
线程
也被称为轻量级进程,线程是进程的执行单元,如同进程在系统中的地位一样,线程在进程中是独立的、并发的执行流。一个进程可以有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、程序计数器和自己的局部变量,但不拥有系统资源。因为多个线程共享父进程的全部资源。线程也可以创建和撤销另一个线程,同时线程之间的执行时抢占式的,也就是说当前运行的线程可能被挂起,以便另一个线程执行。
系统创建一个进程时,需为其分配独立的内存空间和相关的资源,但是创建一个线程则简单得多,因此用线程实现来实现并发比多进程实现并发性能要高得多。
线程创建和启动
1. 继承Thread类创建线程类
继承Thread类,并在子类中重写public void run ()方法,这个方法就是线程执行体。创建了Thread子类实例就创建了线程对象。通过调用对象的start()方法启动线程。
通过调用Thread静态方法currentThread()可以获得当前线程对象。线程对象调用getName()可以获得线程名称。
public class MyThread extends Thread{
@Override
public void run() {
/*
线程主体
*/
}
public static void main(String[] args) {
System.out.println("Hello World!");
Thread myThread = new MyThread();
myThread.start();
}
}
2. 实现Runnable接口创建线程类
定义Runnable接口实现类,并重写run()方法,这个方法同样是线程的执行体。创建Runnable实例,作为Thread的target来创建Thread对象,此Thread对象即为创建的线程。
public class SecondThread implements Runnable {
@Override
public void run() {
/*
线程主体
*/
}
public static void main(String[] args) {
Runnable runnable = new SecondThread();
// runnable作为线程target
Thread secondThread = new Thread(runable);
secondThread.start();
}
}
3.使用Callable和Future创建线程(可以获得线程返回结果,可以抛出异常)
创建Callable接口的实现类,并实现call()方法,该call()方法作为线程执行体,且该call()方法有返回值,再创建Callable接口的实例。可以直接使用Lamda表达式创建Callable对象。
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的taget创建并启动新线程。
调用FutrueTask对象的get()方法来获得子线程执行结束后的返回值。
public class ThirdThread extends Thread{
public static void main(String[] args) {
// 创建Callable对象,使用Lambda表达式创建callable对象
// 泛型指定的是call()方法的返回值
// 使用FutureTask来包装callable对象
// 函数式接口和Lambda表达式可以隐式转换
FutureTask<Integer> task = new FutureTask<>( ()->{// 通过函数式接口实现Callable
int i = 0;
return i;
} );
new Thread(task, "有返回值的线程").start();
}
}
对比:Runnable和Callable创建的多线程类可以继承其他类,同时这两种方法可以实现多个线程对象共享一个target,从而可以让多个线程处理一个事物,实现cpu、代码、数据分开。直接继承Thread类创建的对象,编码较简单,但是无法继承其他类。
线程的生命周期
1.新建
使用new关键字创建了线程之后,线程即处于新建状态。和其他java对象一样,仅由jvm为其分配内存,而不表现出任何的动态的特征。
2.就绪
当线程创建之后,调用了start()方法,该线程即处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,但线程并没有开始运行,只是表示线程可以运行了。而何时开始运行取决于JVM里的线程调度器。(但是绝不可以直接调用run方法运行线程执行体,这种调用方法,系统不会将其作为线程来处理,只会视为普通方法调用)。
3.运行和阻塞状态
当线程开始执行run方法的线程执行体,则处于运行状态。但是当线程数量多于处理器数量时,会存在多个线程在一个CPU上轮换的现象,当线程处于挂起没有cpu处理时,即进入阻塞状态。阻塞的情况有:
1)在线程调用sleep()方法主动放弃所占用的处理器资源会阻塞;2)调用了一个阻塞式的IO方法,并在该方法返回前线程被阻塞;3)线程试图获得一个正被其他线程持有的同步监视器(在线程同步博文中讲到)会阻塞;4)线程在等待某个通知(notify,在线程通信博客中提及);5)程序调用suspend()方法将该线程挂起,但这个方法容易导致死锁,应该尽量避免。
针对上面对应的阻塞,当出现如下情况时,线程重新进入就绪状态:
1)调用sleep()过了指定时间;2)阻塞式IO方法已经返回;3)线程成功地获取了同步监视器;4)线程得到了其他线程发起的通知;5)处于挂起状态的线程被调用了resume()方法恢复。
4.线程死亡
以下几种情况会让线程结束:1)当线程run()或call()方法执行完成;2)线程抛出一个未捕获的Exception、Error;3)直接调用该线程的stop()方法,线程结束即处于死亡状态。
多线程——线程控制
java的线程支持提供了一些便捷的工具方法,通过这些方法可以很好地控制线程执行。
join线程
在某个线程中调用其他线程的join()方法,调用线程会阻塞等待被调用join()的线程执行完。通常这种用法用于在某个父线程中,开启多个子线程去完成多个子任务,父线程等待子线程任务都完成,再来进行下一步的处理。
join()方法的三种重载形式:
1)join():等待被join的线程执行完;
2)join(long millis):等待millis毫秒后,等待线程还没执行完,则不继续等待。
3)join(long millis, long nanos):等待线程执行时间最长为millis毫秒nanos微秒。
后台线程
有一种线程是在后台运行,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”,JVM的垃圾回收线程就是典型的后台线程。
特征:如果前台线程都死亡,后台线程会自动死亡。前台线程子线程默认是后台线程,后台线程子线程默认是后台线程。
setDaemon():可以通过调用Thread对象的setDaemon(true)使得线程为后台线程。需要在启动之前设置否则会引发IllegalThreadStateException异常。
isDaemon():Thread对象的方法可以判断该线程是否是后台线程。
线程睡眠sleep
Thread类的静态方法,声明抛出InterruptedException,可以让线程暂停一段时间并进入阻塞状态。其有两种重载方法:
static void sleep(long milis):暂停线程millis毫秒,并进入阻塞状态。
static void sleep(long millis, long nanos):暂停线程millis毫秒,nanos微秒,并进入阻塞状态。
线程让步yield
yield()也是Thread类的静态方法,它的作用是将线程暂停,并进入就绪状态,这一点是跟睡眠sleep不同的。所以,线程调度器也有可能在线程yield让步后,又立马将其调出来重新执行。
改变线程优先级
Thread类提供了setPriority(int newPriority),getPriority()方法来设置和返回指定线程的优先级,setPriority的参数可以是一个范围1~10之间的整数,也可以是三个静态常量MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)。
多线程——线程同步
线程之间会公用一些资源,当多个线程访问同一个数据时,很容易造成意想不到的问题。为了解决这个问题,java的多线程提供了几种方法。
同步代码块
synchronized(obj)
{
...
// 同步代码块
}
synchronized括号内obj就是代码监视器,线程开始执行同步代码块之前,需要先获得对同步监视器的锁定。同步代码执行完成后,线程会释放对该同步监视器的锁定。
java允许使用任何对象作为同步监视器,但是出于同步监视器的目的,因此通常可以使用可能被并发访问的共享资源充当同步资源器。
同步方法
sychronized还可以用来修饰某个方法,这个方法就称为同步方法,对于这个方法,无需显示指定同步监视器,同步监视器就是this,也即调用该方法的对象。synchronized关键词不能用来修饰构造方法和成员变量。
public synchronized void function() {
// 方法内容
}
释放同步监视器的锁定
线程在如下几种情况会释放对同步监视器的锁定:
1.同步方法、同步代码块执行结束,当前线程即释放同步监视器。
2.当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
3.当前线程在同步代码块出现了未处理的Error或Exception,导致了该代码块、该同步方法异常结束,当前线程会释放同步监视器。
4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
线程在如下情况下不会释放同步监视器:
1.线程执行同步代码块或方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,但当前线程不会释放同步监视器。
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法,将该线程挂起,但是该线程不会释放同步监视器。
同步锁(Lock)
从java5开始,java提供了一个功能更强大的线程同步机制,通过显示定义同步锁对象来实现同步,而不是系统隐式使用同步监视器。java5中为同步锁提供了两个根接口,Lock、ReadWriteLock,ReentrantLock(可重入锁)是对Lock的实现类,ReentrantReadWriteLock式ReadWirteLock的实现类。同步锁比较常用的方法如下:
class X {
// 定义同步锁
private final Lock lock = new ReentrantLock();
// 方法中使用同步锁
public void funtion() {
// 加锁
lock.lock();
try {
// 方法内容
}
catch(...) {
}
finally {
// 解锁
lock.unlock();
}
}
}
死锁
当两个线程都在等待对方释放同步监视器时,就会造成死锁,程序无法继续执行。因此线程间要避免造成死锁。
多线程——线程通信
在线程运行时,程序无法准确控制线程,但是java提供了一些机制来保证线程协调运行。
传统的线程通信
Object类提供了wait()、notify()和notifyAll()三个方法,这个并不属于Thread类,必须由同步监视器对象来调用,分为两种情况:
1.synchronized修饰的同步方法,由于同步监视器是该方法所属类的实例this,故可以直接在方法中调用wait、notify、notifyAll方法。
2.synchronized同步代码块,监视器是括号中的对象,故需由此对象调用。
- wait():导致该线程等待并释放同步监视器,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。wait方法有三种重载方法,不带参数(等待直到notify或notifyAll唤醒)、带毫秒参数、带毫秒和微秒参数。
- notify():当线程调用同步监视器的notify方法,会随机唤醒一个wait中的线程,但是只有在当前线程释放同步监视器后,唤醒的线程才有机会执行。
- notifyAll():唤醒同步监视器上所有wait等待的线程,但同样当前线程并不会释放同步监视器。
使用condition来控制线程通讯
当线程使用Lock对象来保证同步时,由于没有隐式的同步监视器,故不能使用传统的方法。Java提供了Condition可以让那些已经得到Lock对象的线程无法继续执行并释放Lock对象。同时,Condition也可以唤醒其他处于等待的线程。
要获取绑定在Lock对象上的Condition对象,可以直接调用该Lock对象的newCondition()方法即可。
// 获取Lock对象
Lock lock = new ReentrantLock();
// 获取与lock绑定的Condition对象
Condition codition = lock.newCondition();
/**Condition对象的方法与功能**/
// 停止当前线程直到其他线程调用该Condition对象的signal或signalAll的方法,还有很多变体提供丰富的功能
await();
// 唤醒已停止的一个线程
signal();
// 唤醒所有停止的线程
signalAll();
使用阻塞队列
BlockingQueue是Queue的子接口,但是并不是用作容器,而是用作线程同步的工具。其特征是:当生产者线程试图想BlockingQueue中放入元素时,如果该队列已经满了,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列为空,则该线程被阻塞。
阻塞队列有几套方法配合使用:
队尾插入元素 | add(e) 可抛异常、可阻塞 | offer(e) 可抛异常、可阻塞 | put(e) 可抛异常、可阻塞 | offer(e,time,unit) 可抛异常、可阻塞、可指定超时时长 |
队头删除元素 | remove() 可抛异常、可阻塞 | poll() 可抛异常、可阻塞 | take() 可抛异常、可阻塞 | poll(time,unit) 可抛异常、可阻塞、可指定超时时长 |
获取、不删除元素 | element() 可抛异常 | peek() 可抛异常 | 无 | 无 |
多线程——线程组和未处理的异常
java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,如果没有显示制定线程所属的线程组,则该线程归属于默认线程组。同时,在默认情况下,子线程与创建它的线程归属于同一线程组,一旦一个线程加入某个线程组后,该线程一直属于这个线程组,运行中不能改变,直至线程死亡。
为线程设置线程组
Thread类提供了几个构造器来设置新创建的线程属于哪个线程组:
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, String name)
创建线程组对象
同时提供了getThreadGroup()方法获取线程所属的线程组。线程组类提供了两个简单的构造方法来创建实例:
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),该方法可以处理该线程组内的任意线程所抛出的未处理异常。
从java5开始,加强了线程的异常处理,在线程跑出异常时,jvm会自动查找是否有对应的Thread.UncaughtExceptionHandler对象来处理这个异常,此时会调用uncaughtException(Thread t, Throwable e)方法来处理该异常。UncaughtExceptionHandler是Thread类内部的静态接口,只有一个方法void uncaughtException(Thread t, Throwable e)。Thread类提供了两个方法来设置这个接口:
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):通过静态方法为Thread类的所有实例设置默认异常处理器。
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):对象调用方法指定异常处理器。
线程组实现了Thread.UncaughtExceptionHandler接口,故线程组可作为组内的默认异常处理器。当线程抛出未处理异常,处理器调用流程如下:
1.如果该线程通过setUncaughtExceptionHandler制定了处理器,则通过这个处理。
2.否则调用线程组的线程处理器。
3.否则调用线程类的默认处理器。
4.将异常跟踪栈的信息打印到system.sys的错误输出流。如果异常对象是ThreadDeath则不打印。
注意:异常处理器与catch不同之处在于,uncaughtExceptionHandler处理完后还是会将异常抛出到调用上级。
多线程——线程池
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互,在需要创建大量生存期很短的线程时,更应该考虑启动新线程的性能成本。因此,在这种情况下应该使用线程池,线程池在系统启动时就创建了大量空闲线程,程序将一个Runnable或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run或call方法,当线程执行完后,并不会死亡,而是再次返回线程池中,成为空闲状态,等待执行下一个Runnable对象的run或call方法。
Java8线程池
java8提供了内建线程池,新增了一个Executors工厂类来产生线程池对象,该工厂的几个静态方法可以提供相应的线程池:
public static ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程会缓存在线程池。
public static ExecutorService newFixedThreadPool(int nThreads):创建指定数量线程的线程池。
public static ExecutorService newSingleThreadExecutor():创建一个线程的线程池。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):指定线程数量,同时在指定延迟后执行线程任务,其返回的对象是ScheduledExecutorService是ExecutorService线程池类的子类。
public static ScheduledExecutorService newSingleThreadScheduledExecutor():产生可延迟执行的单线程线程池。
public static ExecutorService newWorkStealingPool(int parallelism):创建持有足够的线程线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。生成的线程池是后台线程池,当所有前台线程都死亡了,线程池中线程会自动死亡。
public static ExecutorService newWorkSteallingPool():上一个方法的简化方法,如果当前机器有n个cpu,则目标并行级别被设置为n。生成的线程池也是后台线程池。
在线程池对象ExecutorService中,提供了三个方法来提交Runnable和Callable任务。
- Future<?> submit(Runnable task):提交Runnable,由于Runnable是没有返回值的,所以Future获取的返回值是null。但是可以调用Future对象的isDone()、isCancelled()方法来获得Runnable对象的执行状态。
- <T> Future<T> submit(Runnable task, T result):提交runnable任务,同时显示地将result作为线程执行完后的返回结果。
- <T> Future<T> submit(Callable<T> task):提交Callable任务给线程池。
在用完了线程池后,调用shutdown()之后不再接受任务,同时将正在执行的任务和已经提交并在等待执行的任务完成,就销毁线程。执行shudonwNow()则会立即停止所有线程的活动任务,同时暂停处理正在等待的任务,返回等待执行的任务列表。
Java8增强的ForkJoinPool
现在的处理器大多是多核心的,如果将一个任务拆分成多个小任务,并分发到这些核心上并行执行,最后合并结果,可以充分利用cpu的计算能力。ForkJoinPool就提供了这样的功能,其提供了两个构造器:
ForkJoinPool(int parallelism):提供parallelism个并行线程的线程池。
ForkJoinPool():提供Runtime.availableProcessors()方法的返回值作为并行线程池数量。
有了ForkJoinPool实例后,可以调用方法submit(ForkJoinTask task)或invoke(ForkJoinTask task)来执行指定任务。其中ForkJoinTask代表可以并行、合并的任务。ForkJoinTask作为抽象类有两个抽象子类RecursiveTask(有返回值)和RecursiveAction(无返回值)。
class RecursiveActionObject extends RecursiveAction {
// 重写comput方法
@Override
protected void compute() {
// 声明第一个分解对象
RecursiveActionObject object1 = new RecursiveActionObject();
// 声明第二个分解对象
RecursiveActionObject object2 = new RecursiveActionObject();
// 将分解任务并发运行
object1.fork();
object2.fork();
}
}