内存模型与多线程设计-线程与虚拟机

RoadMap

1. 线程实现

    线程是比进程更轻量级的调度执行单位,线程的引入,能够把一个进程的资源分配和执行调度分开,各个线程既能够共享进程资源(内存地址、文件I/O等),又能够独立调度(线程是CPU调度的基本单位),主流的操做系统都提供了线程实现,Java语言则提供了在不一样硬件和操做系统平台下对线程操做的统一处理,实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。java

1.1 内核线程实现

    内核线程(Kernel-Level Thread,KLT)就是直接由操做系统内核支持的线程,这种线程由内核来完成线程切换,内核经过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。web

    程序通常不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是咱们一般意义上所讲的线程,因为每一个轻量级进程都由一个内核线程支持,所以只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型算法

1.2 使用用户线程实现

    从广义上来说,一个线程只要不是内核线程,就能够认为是用户线程(User Thread,UT),所以,从这个定义上来说,轻量级进程也属于用户线程,但轻量级进程的实现始终是创建在内核之上的,许多操做都要进行系统调用,效率会受到限制。 而狭义上的用户线程指的是彻底创建在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的创建、同步、销毁和调度彻底在用户态中完成,不须要内核的帮助。这种进程与用户线程之间1:N的关系称为一对多的线程模型。安全

1.3 使用用户线程加轻量级进程混合实现

    线程除了依赖内核线程实现和彻底由用户程序本身实现以外,还有一种将内核线程与用户线程一块儿使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程仍是彻底创建在用户空间中,所以用户线程的建立、切换、析构等操做依然廉价,而且能够支持大规模的用户线程并发。而操做系统提供支持的轻量级进程则做为用户线程和内核线程之间的桥梁,这样可使用内核提供的线程调度功能及处理器映射,而且用户线程的系统调用要经过轻量级线程来完成,大大下降了整个进程被彻底阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。多线程

    对于Sun JDK来讲,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,由于Windows和Linux系统提供的线程模型就是一对一的。并发

    在Solaris平台中,因为操做系统的线程特性能够同时支持一对一及多对多的线程模型,所以在Solaris版的JDK中也对应提供了两个平台专有的虚拟机参数:工具

-XX:+UseLWPSynchronization(默认值)和oop

-XX:+UseBoundThreads来明确指定虚拟机使用哪一种线程模型。性能

 

2. 线程的调度

    线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling).测试

Java使用的线程调度方式就是抢占式调度。

2.1 调度方式

2.1.1 抢占式(Preemptive Threads-Scheduling)

    抢占式调度的多线程系统的每一个线程由系统来分配执行时间,线程的切换不禁线程自己来决定在在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程致使整个进程阻塞的问题。缺点是实现成本高,程序对线程不可控。

2.1.2 协同式(Cooperative Threads-Scheduling)

    协同式调度的多线程系统的线程的执行时间由线程自己来控制,线程把本身的工做执行完了以后,要主动通知系统切换到另一个线程上。协同式多线程的最大好处是实现简单,并且因为线程要把本身的事情干完后才会进行线程切换,切换操做对线程本身是可知的,因此没有什么线程同步的问题。

2.1.3 建议

    虽然Java线程调度是系统自动完成的,可是咱们仍是能够“建议”系统给某些线程多分配一点执行时间,另外的一些线程则能够少分配一点(设置优先级)

    不过,线程优先级并非太靠谱,缘由是Java的线程是经过映射到系统的原生线程上来实现的,因此线程调度最终仍是取决于操做系统。虽然如今不少操做系统都提供线程优先级的概念,可是并不见得能与Java线程的优先级一一对应,不一样的操做系统应对线程调度的方式也是有差别的。

    Java一共设置了10个优先级状态

Java线程优先级 Windows 线程优先级
1(Thread.MIN_PRIORITY) TRHEAD_PRIORITY_LOWEST
2 TRHEAD_PRIORITY_LOWEST
3 TRHEAD_PRIORITY_BELOW_NORMAL
4 TRHEAD_PRIORITY_BELOW_NORMAL
5(Thread.NORMAL_PRIORITY) TRHEAD_PRIORITY_NORMAL
6 TRHEAD_PRIORITY_ABOVE_NORMAL
7 TRHEAD_PRIORITY_ABOVE_NORMAL
8 TRHEAD_PRIORITY_ABOVE_HIGHEST
9 TRHEAD_PRIORITY_ABOVE_HIGHEST
10(Thread.MAX_PRIORITY) TRHEAD_PRIORITY_ABOVE_CRITICAL

2.2 状态转换

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,

3. 线程安全

3.1 线程安全类型

    Java语言中各类操做共享的数据的安全类型分为如下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

3.1.1 不可变

    不可变(Immutable),是指对象一旦被建立是不容许修改的,若是须要修改或者对象属性等,则会从新生成一份新对象进行修改并返回给调度者,因此不可变对象必定是线程安全的。

    常见的不可变对象如String, replace方法,substring方法。基本类型的封装类.经常使用的还有枚举类型,以及java.lang.Number的部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型.

3.1.2 绝对的线程安全

    绝对的线程安全是指 无论运行时环境如何,调用者都不须要任何额外的同步措施”一般须要付出很大的,甚至有时候是不切实际的代价(Brian Goetz给出的线程安全的定义)。在java语言中很难找到 绝对线程安全的类, 不仅仅是实现成本的问题,对系统运行的性能也有着很大影响。

3.1.3 相对线程安全

    相对的线程安全就是咱们一般意义上所讲的线程安全,它须要保证对这个对象单独的操做是线程安全的,咱们在调用的时候不须要作额外的保障措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性,Java大部分的线程安全的集合容器都是相对线程安全的。

3.1.4 线程兼容

    线程兼容是指对象自己并非线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用,咱们平时说的这个类不是线程安全的, 应该就是对应这个安全级别(线程兼容)

3.1.5 线程对立

    线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码,

3.2 实现线程安全

    紧接着的一个问题就是咱们应该如何实现线程安全,这与代码编写和虚拟机提供的锁机制有着密不可分的联系,但虚拟机提供的同步和锁机制也起到了很是重要的做用。由于这篇是虚拟机系列的博文,会更加偏重虚拟机的线程安全实现方面。

3.2.1 互斥同步(阻塞同步)

    互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。所以,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的    

    在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字通过编译以后,会在同步块的先后分别造成monitorenter和monitorexit这两个字节码指令,这两个字节码都须要一个reference类型的参数来指明要锁定和解锁的对象。若是Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;若是没有明确指定,那就根据synchronized修饰的是实例方法仍是类方法,去取对应的对象实例或Class对象来做为锁对象。

    根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。若是获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

    须要特别注意是,synchronized同步块对同一条线程来讲是可重入的,不会出现本身把本身锁死的问题。另外,同步块在已进入的线程执行完以前,会阻塞后面其余线程的进入。

    除了synchronized以外,咱们还可使用java.util.concurren包中的相关并发工具类进行同步。

3.2.2 非阻塞同步

    互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,互斥同步属于一种悲观的并发策略,不管共享数据是否真的会出现竞争,它都要进行加锁,用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要唤醒等操做。随着硬件指令集的发展咱们能够采起基于冲突检测的乐观并发策略,即就是先进行操做,若是没有其余线程争用共享数据,那操做就成功了;若是共享数据有争用,产生了冲突,那就再采起其余的补偿措施。由于不会把线程挂起因此是(Non-Blocking Synchronization)

    冲突检测,是须要靠硬件指令来完成的,这类指令经常使用的有

    测试并设置(Test-and-Set)
    交换(Swap)
    比较并交换(Compare-and-Swap,CAS)
    加载连接/条件存储(Load-Linked/Store-Conditional,LL/SC)

3.2.2.1 CAS 算法

    CAS的语义是“我认为V的值应该为A,若是是,那么将V的值更新为B,不然不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。

3.2.3 无需同步

    要保证线程安全,并非必定就要进行同步,二者没有因果关系。同步只是保证共享数据争用时的正确性的手段,若是自己不涉及数据共享,天然就不须要同步了,所以Java世界有两类无需同步的状况。

3.2.3.1 可重入代码(Reentrant Code)

    能够在代码执行的任什么时候刻中断它,转而去执行另一段代码(包括递归调用它自己),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来讲,可重入性是更基本的特性,它能够保证线程安全,即全部的可重入的代码都是线程安全的,可是并不是全部的线程安全的代码都是可重入的。


3.2.3.2 线程本地存储(Thread Local Storage)

    若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。

    常见的有  生产者-消费者模式,web中的-request-response 模式。前者是将须要操做同一组数据的线程放到一个队列里面顺序执行,消除资源竞争的状况,后者则是,变量线程独享(Thread-per-Request)。

     Java语言中,若是一个变量要被多线程访问,可使用volatile关键字声明它为“易变的”,若是一个变量要被某个线程独享,能够经过java.lang.ThreadLocal类来实现线程本地存储的功能。

相关文章
相关标签/搜索