在JDK5以前,Java多线程以及其性能一直是个软肋,只有synchronized、Thread.sleep()、Object.wait/notify这样有限的方法,而synchronized的效率还特别地低,开销比较大。java
在JDK5以后,相对于前面版本有了重大改进,不只在Java语法上有了不少改进,包括:泛型、装箱、for循环、变参等,在多线程上也有了完全提升,其引进了并发编程大师Doug Lea的java.util.concurrent包(后面简称J.U.C),支持了现代CPU的CAS原语,不只在性能上有了很大提高,在自由度上也有了更多的选择,此时 J.U.C的效率在高并发环境下的效率远优于synchronized。程序员
在JDK6(Mustang 野马)中,对synchronized的内在机制作了大量显著的优化,加入了CAS的概念以及偏向锁、轻量级锁,使得synchronized的效率与J.U.C不相上下,而且官方说后面该关键字还有继续优化的空间,因此在现 在JDK7时代,synchronized已经成为通常状况下的首选,在某些特殊场景:可中断的锁、条件锁、等待得到锁一段时间若是失败则中止,J.U.C是适用的,因此对于 多线程研究来讲,了解其原理以及各自的适用场景是必要的。编程
线程是依附于进程的,进程是分配资源的最小单位,一个进程能够生成多个线程,这些线程拥有共享的进程资源。就每一个线程而言,只有不多的独有资源,如:控制线程运行的线程控制块,保留局部变量和少数参数的栈空间等。线程有就绪、阻塞和运行三种状态,并能够在这之间切换。也正由于多个线程会共享进程资源,因此当它们对同一个共享变量/对象进行操做的时候,线程的冲突和不一致性就产生了。缓存
多线程并发环境下,本质上要解决地是这两个问题:安全
- 线程之间如何通讯;
- 线程之间如何同步;
归纳起来讲就是:线程之间如何正确地通讯。虽说的是在Java层面如何保证,但会涉及到 Java虚拟机、Java内存模型,以及Java这样的高级语言最终是要映射到CPU来执行(关键缘由:现在的CPU有缓存、而且是多核的),虽然有些难懂,但对于深入把握多线程是相当重要的,因此须要多花一些时间。服务器
当多个线程对同一个共享变量/对象进行操做,即便是最简单的操做,如:i++,在处理上实际也涉及到读取、自增、赋值这三个操做,也就是说 这中间存在时间差,致使多个线程没有按照如程序编写者所设想的去顺序执行,出现错位,从而致使最终结果与预期不一致。网络
Java中的多线程同步是经过锁的概念来体现。锁不是一个对象、不是一个具体的东西,而是一种机制的名称。锁机制须要保证以下两种特性:多线程
- 互斥性:即在同一时间只容许一个线程持有某个对象锁,经过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操做)进行访问。互斥性咱们也每每称为操做的原子性;
- 可见性:必须确保在锁被释放以前,对共享变量所作的修改,对于随后得到该锁的另外一个线程是可见的(即在得到锁时应得到最新共享变量的值),不然另外一个线程多是在本地缓存的某个副本上继续操做从而引发不一致;
挂起(Suspend):当线程被挂起的时候,其会失去CPU的使用时间,直到被其余线程(用户线程或调度线程)唤醒。架构
休眠(Sleep):一样是会失去CPU的使用时间,可是在过了指定的休眠时间以后,它会自动激活,无需唤醒(整个唤醒表面看是自动的,但实际上也得有守护线程去唤醒,只是不需编程者手动干预)。并发
阻塞(Block):在线程执行时,所须要的资源不能获得,则线程被挂起,直到知足可操做的条件。
非阻塞(Block):在线程执行时,所须要的资源不能获得,则线程不是被挂起等待,而是继续执行其他事情,待条件知足了以后,收到了通知(一样是守护线程去作)再执行。
挂起和休眠是独立的操做系统的概念,而阻塞与非阻塞则是在资源不能获得时的两种处理方式,不限于操做系统,当资源申请不到时,要么挂起线程等待、要么继续执行其余操做,资源被知足后再通知该线程从新请求。显然非阻塞的效率要高于阻塞,相应的实现的复杂度也要高一些。
在Java中显式的挂起以前是经过Thread的suspend方法来体现,如今此概念已经消失,缘由是suspend/resume方法已经被废弃,它们容易产生死锁,在suspend方法的注释里有这么一段话:当suspend的线程持有某个对象锁,而resume它的线程又正好须要使用此锁的时候,死锁就产生了。
因此,如今的JDK版本中,挂起是JVM的系统行为,程序员无需干涉。休眠的过程当中也不会释放锁,但它必定会在某个时间后被唤醒,因此不会死锁。如今咱们所说的挂起,每每并不是指编写者的程序里主动挂起,而是由操做系统的线程调度器去控制。
因此,咱们经常说的“线程在申请锁失败后会被挂起、而后等待调度”这样有必定歧义,由于这里的“挂起”是操做系统级别的挂起,实际上是在申请资源失败时的阻塞,和Java中的线程的挂起(可能已经得到锁,也可能没有锁,总之和锁无关)不是一个概念,很容易混淆,因此在后文中说的挂起,通常指的是操做系统的操做,而不是Thread中的suspend()。
相应地有必要提下java.lang.Object的wait/notify,这两个方法一样是等待/通知,但它们的前提是已经得到了锁,且在wait(等待)期间会释放锁。在wait方法的注释里明确提到:线程要调用wait方法,必须先得到该对象的锁,在调用wait以后,当前线程释放该对象锁并进入休眠(这里究竟是进入休眠仍是挂起?文档没有细说,从该方法能指定等待时间来看,更多是休眠,没有指定等待时间的,则多是挂起,无论如何,在休眠/挂起以前,JVM都会从当前线程中把该对象锁释放掉),只有如下几种状况下会被唤醒:其余线程调用了该对象的notify或notifyAll、当前线程被中断、调用wait时指定的时间已到。
这是两个操做系统的概念,但理解它们对咱们理解Java的线程机制有着必定帮助。
有一些系统级的调用,好比:清除时钟、建立进程等这些系统指令,若是这些底层系统级指令可以被应用程序任意访问的话,那么后果是危险的,系统随时可能崩溃,因此 CPU将所执行的指令设置为多个特权级别,在硬件执行每条指令时都会校验指令的特权,好比:Intel x86架构的CPU将特权分为0-3四个特权级,0级的权限最高,3权限最低。
而操做系统根据这系统调用的安全性分为两种:内核态和用户态。内核态执行的指令的特权是0,用户态执行的指令的特权是3。
- 当一个任务(进程)执行系统调用而进入内核指令执行时,进程处于内核运行态(或简称为内核态);
- 当任务(进程)执行本身的代码时,进程就处于用户态;
明白了内核态和用户态的概念以后,那么在这两种状态之间切换会形成什么样的效率影响?
在执行系统级调用时,须要将变量传递进去、可能要拷贝、计数、保存一些上下文信息,而后内核态执行完成以后须要再将参数传递到用户进程中去,这个切换的代价相对来讲是比较大的,因此应该是 尽可能避免频繁地在内核态和用户态之间切换。
那操做系统的这两种形态和咱们的线程主题有什么关系呢?这里是关键。Java并无本身的线程模型,而是使用了操做系统的原生线程!
若是要实现本身的线程模型,那么有些问题就特别复杂,难以解决,好比:如何处理阻塞、如何在多CPU之间合理地分配线程、如何锁定,包括建立、销毁线程这些,都须要Java本身来作,在JDK1.2以前Java曾经使用过本身实现的线程模型,后来放弃了,转向使用操做系统的线程模型,所以建立、销毁、调度、阻塞等这些事都交由操做系统来作,而 线程方面的事在操做系统来讲属于系统级的调用,须要在内核态完成,因此若是频繁地执行线程挂起、调度,就会频繁形成在内核态和用户态之间切换,影响效率(固然,操做系统的线程操做是不容许外界(包括Java虚拟机)直接访问的,而是开放了叫“轻量级进程”的接口供外界使用,其与内核线程在Window和Linux上是一对一的关系,这里很少叙述)。
前面说JDK5以前的synchronized效率低下,是 由于在阻塞时线程就会被挂起、而后等待从新调度,而线程操做属于内核态,这频繁的挂起、调度使得操做系统频繁处于内核态和用户态的转换,形成频繁的变量传递、上下文保存等,从而性能较低。
尽管面临不少挑战,多线程有一些优势使得它一直被使用。这些优势是:
- 资源利用率更好;
- 程序设计在某些状况下更简单;
- 程序响应更快速;
CPU可以在等待IO的时候作一些其余的事情。这个不必定就是磁盘IO。它也能够是网络的IO,或者用户输入。一般状况下,网络和磁盘的IO比CPU和内存的IO慢的多。
在单线程应用程序中,若是你想编写程序手动处理多个IO的读取和处理的顺序,你必须记录每一个文件读取和处理的状态。相反,你能够启动两个线程,每一个线程处理一个文件的读取和处理操做。线程会在等待磁盘读取文件的过程当中被阻塞。在等待的时候,其余的线程可以使用CPU去处理已经读取完的文件。其结果就是,磁盘老是在繁忙地读取不一样的文件到内存中。这会带来磁盘和CPU利用率的提高。并且每一个线程只须要记录一个文件,所以这种方式也很容易编程实现。
将一个单线程应用程序变成多线程应用程序的另外一个常见的目的是 实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,而后再返回去监听。
若是一个请求须要占用大量的时间来处理,在这段时间内新的客户端就没法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。
另外一种设计是,监听线程把请求传递给工做者线程池(worker thread pool),而后马上返回去监听。而工做者线程则可以处理这个请求并发送一个回复给客户端。
使用多线程每每能够 得到更大的吞吐率和更短的响应时间,可是,使用多线程不必定就比单线程程序跑的快,这取决于咱们程序设计者的能力以及应用场景的不一样。不要为了多线程而多线程,而应考虑具体的应用场景和开发实力,使用多线程就是但愿可以得到更快的处理速度和利用闲置的处理能力,若是没带来任何好处还带来了复杂性和一些定时炸弹,那就傻逼了?只有在使用多线程给咱们带来的好处远大于咱们付出的代价时,才考虑使用多线程。有时候可能引入多线程带来的性能提高抵不过多线程而引入的开销,一个没有通过良好并发设计得程序也可能比使用单线程还更慢。
多线程程序在访问共享可变数据的时候每每须要咱们很当心的处理,不然就会出现难以发现的BUG,通常地,多线程程序每每比单线程程序设计会更加复杂(尽管有些单线程处理程序可能比多线程程序要复杂),并且错误很难重现(由于线程调度的无序性,某些bug的出现依赖于某种特定的线程执行时序)。
当CPU从执行一个线程切换到执行另一个线程的时候,须要先存储当前线程的本地的数据,程序指针等,而后载入另外一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为 “上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,而后切换到另一个上下文中执行另一个线程。
上下文切换并不廉价。若是没有必要,应该减小上下文切换的发生。
线程在运行的时候须要从计算机里面获得一些资源。除了CPU,线程还须要一些内存来维持它本地的堆栈。它也须要占用操做系统中一些资源来管理线程。咱们能够尝试编写一个程序,让它建立100个线程,这些线程什么事情都不作,只是在等待,而后看看这个程序在运行的时候占用了多少内存。
编写线程运行时执行的代码有两种方式:一种是建立Thread子类的一个实例并重写run方法,第二种是建立类的时候实现Runnable接口。
建立Thread子类的一个实例并重写run方法,run方法会在调用start()方法以后被执行。例子以下:
public class MyThread extends Thread { public void run(){ System.out.println("MyThread running"); } } 复制代码
能够用以下方式建立并运行上述Thread子类:
MyThread myThread = new MyThread(); myTread.start(); 复制代码
一旦线程启动后start方法就会当即返回,而不会等待到run方法执行完毕才返回。就好像run方法是在另一个cpu上执行同样。当run方法执行后,将会打印出字符串MyThread running。
第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法能够被线程调用。下面给出例子:
public class MyRunnable implements Runnable { public void run(){ System.out.println("MyRunnable running"); } } 复制代码
为了使线程可以执行run()方法,须要在Thread类的构造函数中传入 MyRunnable的实例对象。示例以下:
Thread thread = new Thread(new MyRunnable()); thread.start(); 复制代码
当线程运行时,它将会调用实现了Runnable接口的run方法。上例中将会打印出”MyRunnable running”。
对于这两种方式哪一种好并无一个肯定的答案,它们都能知足要求。就我的意见,更倾向于实现Runnable接口这种方法。由于线程池能够有效的管理实现了Runnable接口的线程,若是线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而若是线程是经过实现Thread子类实现的,这将会复杂一些。
有时咱们要同时融合实现Runnable接口和Thread子类两种方式。例如,实现了Thread子类的实例能够执行多个实现了Runnable接口的线程。一个典型的应用就是线程池。
建立并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,以下所示:
Thread newThread = new Thread(MyRunnable()); newThread.run(); //should be start(); 复制代码
起初你并不会感受到有什么不妥,由于run()方法的确如你所愿的被调用了。可是,事实上,run()方法并不是是由刚建立的新线程所执行的,而是被建立新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让建立的新线程执行run()方法,必须调用新线程的start()方法。
当建立一个线程的时候,能够给线程起一个名字。它有助于咱们区分不一样的线程。例如:若是有多个线程写入System.out,咱们就可以经过线程名容易的找出是哪一个线程正在输出。例子以下:
MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable, "New Thread"); thread.start(); System.out.println(thread.getName()); 复制代码
须要注意的是,由于MyRunnable并不是Thread的子类,因此MyRunnable类并无getName()方法。能够经过如下方式获得当前线程的引用:
Thread.currentThread(); 复制代码
所以,经过以下代码能够获得当前线程的名字:
String threadName = Thread.currentThread().getName(); 复制代码
首先输出执行main()方法线程名字。这个线程JVM分配的。而后开启10个线程,命名为1~10。每一个线程输出本身的名字后就退出。
public class ThreadExample { public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); for(int i=0; i<10; i++){ new Thread("" + i){ public void run(){ System.out.println("Thread: " + getName() + "running"); } }.start(); } } } 复制代码
须要注意的是,尽管启动线程的顺序是有序的,可是执行的顺序并不是是有序的。也就是说,1号线程并不必定是第一个将本身名字输出到控制台的线程。这是由于线程是并行执行而非顺序的。JVM和操做系统一块儿决定了线程的执行顺序,他和线程的启动顺序并不是必定是一致的。
Main线程是个非守护线程,不能设置成守护线程
这是由于,Main线程是由Java虚拟机在启动的时候建立的。main方法开始执行的时候,主线程已经建立好并在运行了。对于运行中的线程,调用Thread.setDaemon()会抛出异常Exception in thread "main" java.lang.IllegalThreadStateException。
Main线程结束,其余线程同样能够正常运行
主线程,只是个普通的非守护线程,用来启动应用程序,不能设置成守护线程;除此以外,它跟其余非守护线程没有什么不一样。主线程执行结束,其余线程同样能够正常执行。
这样实际上是很合理的,按照操做系统的理论,进程是资源分配的基本单位,线程是CPU调度的基本单位。对于CPU来讲,其实并不存在java的主线程和子线程之分,都只是个普通的线程。进程的资源是线程共享的,只要进程还在,线程就能够正常执行,换句话说线程是强依赖于进程的。也就是说:
线程其实并不存在互相依赖的关系,一个线程的死亡从理论上来讲,不会对其余线程有什么影响。
Main线程结束,其余线程也能够马上结束,当且仅当这些子线程都是守护线程
Java虚拟机(至关于进程)退出的时机是:虚拟机中全部存活的线程都是守护线程。只要还有存活的非守护线程虚拟机就不会退出,而是等待非守护线程执行完毕;反之,若是虚拟机中的线程都是守护线程,那么无论这些线程的死活java虚拟机都会退出。
并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不一样的任务。前者是逻辑上的同时发生(simultaneous),然后者是物理上的同时发生。
并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不必定要同一时刻发生。
并行(parallelism)是指同时发生的两个并发事件,具备并发的含义,而并发则不必定并行。
来个比喻:并发和并行的区别就是一我的同时吃三个馒头和三我的同时吃三个馒头。
上图反映了一个包含8个操做的任务在一个有两核心的CPU中建立四个线程运行的状况。假设每一个核心有两个线程,那么每一个CPU中两个线程会交替并发,两个CPU之间的操做会并行运算。单就一个CPU而言两个线程能够解决线程阻塞形成的不流畅问题,其自己运行效率并无提升,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。
做者:猿码道 连接:https://juejin.im/post/5a701c246fb9a01cb8100489 来源:掘金 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。