黑马程序员成都中心编著【黑马程序员】Java核心技术点之多线程一、为什么使用多线程1.并发与并行我们知道,在单核机器上,“多进程”并不是真正的多个进程在同时执行,而是通过CPU时间分片,操作系统快速在进程间切换而模拟出来的多进程。我们通常把这种情况成为并发,也就是多个进程的运行行为是“一并发生”的,但不是同时执行的,因为CPU核数的限制(PC和通用寄存器只有一套,严格来说在同一时刻只能存在一个进程的上下文)。现在,我们使用的计算机基本上都搭载了多核CPU,这时,我们能真正的实现多个进程并行执行,这种情况叫做并行,因为多个进程是真正“一并执行”的(具体多少个进程可以并行执行取决于CPU核数)。综合以上,我们知道,并发是一个比并行更加宽泛的概念。也就是说,在单核情况下,并发只是并发;而在多核的情况下,并发就变为了并行。下文中我们将统一用并发来指代这一概念。2.阻塞与非阻塞UNIX系统内核提供了一个名为read的函数,用来读取文件的内容:typedefssize_tint;typedefsize_tunsigned;ssize_tread(intfd,void*buf,size_tn);这个函数从描述符为fd的当前文件位置复制至多n个字节到内存缓冲区buf。若执行成功则返回读取到的字节数;若失败则返回-1。read系统调用默认会阻塞,也就是说系统会一直等待这个函数执行完毕直到它产生一个返回值。然而我们知道,磁盘通常是一种慢速I/O设备,这意味着我们用read函数读取磁盘文件内容时,往往需要比较长的时间(相对于访问内存或者计算一些数值来说)。那么阻塞的时候我们当然不想让系统傻等着,我们想在这黑马程序员成都中心编著期间做点儿别的事情,等着磁盘准备好了通知我们一下,我们再来读取文件内容。实际上,操作系统正是这样做的。当阻塞在read这类系统调用中的时候,操作系统通常都会让该进程暂时休眠,调度一个别的进程来执行,以免干等着浪费时间,等到磁盘准备好了可以让我们来进行I/O了,它会发送一个中断信号通知操作系统,这时候操作系统重新调度原来的进程来继续执行read函数。这就是通过多进程实现的并发。3.多进程vs多线程进程就是一个执行中的程序实例,而线程可以看作一个进程的最小执行单元。线程与进程间的一个显著区别在于每个进程都有一整套变量,而同一个进程间的多个线程共享该进程的数据。多进程实现的并发通常在进程创建以及数据共享等方面的开销要比多线程更大,线程的实现通常更加轻量,相应的开销也就更小,因此在一般客户端开发场景下,我们更加倾向于使用多线程来实现并发。然而,有时候,多线程共享数据的便捷容易可能会成为一个让我们头疼的问题,我们在后文中会具体提到常见的问题及相应的解决方案。在上面的read函数的例子中,如果我们使用多线程,可以使用一个主线程去进行I/O的工作,再用一个或几个工作线程去执行一些轻量计算任务,这样当主线程阻塞时,线程调度程序会调度我们的工作线程来执行计算任务,从而更加充分的利用CPU时间片。而且,在多核机器上,我们的多个线程可以并行执行在多个核上,进一步提升效率。二、如何使用多线程1.线程执行模型每个进程刚被创建时都只含有一个线程,这个线程通常被称作主线程(mainthread)。而后随着进程的执行,若遇到创建新线程的代码,就会创建出新线程,而后随着新线程被启动,多个线程就会并发地运行。某时刻,主线程阻塞在一个慢速系统调用中(比如前面提到黑马程序员成都中心编著的read函数),这时线程调度程序会让主线程暂时休眠,调度另一个线程来作为当前运行的线程。每个线程也有自己的一套变量,但相比于进程来说要少得多,因此线程切换的开销更小。2.创建一个新线程(1)通过实现Runnable接口在Java中,有两种方法可以创建一个新线程。第一种方法是定义一个实现Runnable接口的类并实例化,然后将这个对象传入Thread的构造器来创建一个新线程,如以下代码所示:classMyRunnableimplementsRunnable{...publicvoidrun(){//这里是新线程需要执行的任务}}Runnabler=newMyRunnable();Threadt=newThread(r);(2)通过继承Thread类第二种创建一个新线程的方法是直接定义一个Thread的子类并实例化,从而创建一个新线程。比如以下代码:classMyThreadextendsThread{publicvoidrun(){//这里是线程要执行的任务}}黑马程序员成都中心编著创建了一个线程对象后,我们直接对其调用start方法即可启动这个线程:t.start();(3)两种方式的比较既然有两种方式可以创建线程,那么我们该使用哪一种呢?首先,直接继承Thread类的方法看起来更加方便,但它存在一个局限性:由于Java中不允许多继承,我们自定义的类继承了Thread后便不能再继承其他类,这在有些场景下会很不方便;实现Runnable接口的那个方法虽然稍微繁琐些,但是它的优点在于自定义的类可以继承其他的类。3.线程的属性(1)线程的状态线程在它的生命周期中可能处于以下几种状态之一:New(新生):线程对象刚刚被创建出来;Runnable(可运行):在线程对象上调用start方法后,相应线程便会进入Runnable状态,若被线程调度程序调度,这个线程便会成为当前运行(Running)的线程;Blocked(被阻塞):若一段代码被线程A”上锁“,此时线程B尝试执行这段代码,线程B就会进入Blocked状态;Waiting(等待):当线程等待另一个线程通知线程调度器一个条件时,它本身就会进入Waiting状态;TimeWaiting(计时等待):计时等待与等待的区别是,线程只等待一定的时间,若超时则不再等待;Terminated(被终止):线程的run方法执行完毕或者由于一个未捕获的异常导致run方法意外终止会进入Terminated状态。黑马程序员成都中心编著后文中若不加特殊说明的话,我们会用阻塞状态统一指代Blocked、Waiting、TimeWaiting。(2)线程的优先级在Java中,每个线程都有一个优先级,默认情况下,线程会继承它的父线程的优先级。可以用setPriority方法来改变线程的优先级。Java中定义了三个描述线程优先级的常量:MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY。每当线程调度器要调度一个新的线程时,它会首先选择优先级较高的线程。然而线程优先级是高度依赖与操作系统的,在有些系统的Java虚拟机中,甚至会忽略线程的优先级。因此我们不应该将程序逻辑的正确性依赖于优先级。线程优先级相关的API如下:voidsetPriority(intnewPriority)//设置线程的优先级,可以使用系统提供的三个优先级常量staticvoidyield()//使当前线程处于让步状态,这样当存在其他优先级大于等于本线程的线程时,线程调度程序会调用那个线程4.Thread类Thread实现了Runnable接口,关于这个类的以下实例域需要我们了解:privatevolatilecharname[];//当前线程的名字,可在构造器中指定privateintpriority;//当前线程优先级privateRunnabletarget;//当前要执行的任务privatelongtid;//当前线程的IDThread类的常用方法除了我们之前提到的用于启动线程的start外还有:sleep方法,这是一个静态方法,作用是让当前线程进入休眠状态(但线程不会释放已获取的锁),这个休眠状态其实就是我们上面提到过的TimeWaiting状态,从休眠状态“苏黑马程序员成都中心编著醒”后,线程会进入到Runnable状态。sleep方法有两个重载版本,声明分别如下:publicstaticnativevoidsleep(longmillis)throwsInterruptedException;//让当前线程休眠millis指定的毫秒数publicstaticnativevoidsleep(longmillis,intnanos)throwsInterruptedException;//在毫秒数的基础上还指定了纳秒数,控制粒度更加精细join方法,这是一个实例方法,在当前线程中对一个线程对象调用join方法会导致当前线程停止运行,等那个线程运行完毕后再接着运行当前线程。也就是说,把当前线程还没执行的部分“接到”另一个线程后面去,另一个线程运行完毕后,当前线程再接着运行。join方法有以下重载版本:publicfinalsynchronizedvoidjoin()throwsInterruptedExceptionpublicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException;publicfinalsynchronizedvoidjoin(longmillis,intnanos)throwsInterruptedException;无参数的join表示当前线程一直等到另一个线程运行完毕,这种情况下当前线程会处于Wating状态;带参数的表示当前线程只等待指定的时间,这种情况下当前线程会处于TimeWaiting状态。当前线程通过调用join方法进入TimeWaiting或Waiting状态后,会释放已经获取的锁。实际上,join方法内部调用了Object类的实例方法wait,关于这个方法我们下面会具体介绍。yield方法,这是一个静态方法,作用是让当前线程“让步”,目的是为了让优先级不低于当前线程的线程有机会运行,这个方法不会释放锁。interrupt方法,这是一个实例方法。每个线程都有一个中断状态标识,这个方法的作用黑马程序员成都中心编著就是将相应线程的中断状态标记为true,这样相应的线程调用isInterrupted方法就会返回true。通过使用这个方法,能够终止那些通过调用可中断方法进入阻塞状态的线程。常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而终止当前线程。以下这幅图很好的诠释了随着各种方法的调用,线程在不同的状态之间的切换(图片来源:):5.wait方法与notify/notifyAll方法(1)wait方法wait方法是Object类中定义的实例方法。在指定对象上调用wait方法能够让当前线程进入阻塞状态(前提时当前线程持有该对象的内部锁(monitor)),此时当前线程会释放已经获取的那个对象的内部锁,这样一来其他线程就可以获取这个对象的内部锁了。当其他线程获取了这个对象的内部锁,进行了一些操作后可以调用notify方法来唤醒正在等待该对象的线程。(2)notify/notifyAll方法notify/notifyAll方法也是Object类中定义的实例方法。它俩的作用是唤醒正在等待相应对象的线程,区别在于前者唤醒一个等待该对象的线程,而后者唤醒所有等待该对象的线程。这么说比较抽象,下面我们来举一个具体的例子来说明以下wait和notify/notifyAll的用法。请看以下代码(转自Java并发编程:线程间协作的两种方式):1publicclassTest{2privateintqueueSize=10;黑马程序员成都中心编著3privatePriorityQueueIntegerqueue=newPriorityQueueInteger(queueSize);45publicstaticvoidmain(String[]args){6Testtest=newTest();7Producerproducer=test.n