您的位置:首页 > 其它

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

2017-06-13 00:00 423 查看

RoadMap



1. 线程实现

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

1.1 内核线程实现

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

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(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(默认值)和

-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
2TRHEAD_PRIORITY_LOWEST
3TRHEAD_PRIORITY_BELOW_NORMAL
4TRHEAD_PRIORITY_BELOW_NORMAL
5(Thread.NORMAL_PRIORITY)TRHEAD_PRIORITY_NORMAL
6TRHEAD_PRIORITY_ABOVE_NORMAL
7TRHEAD_PRIORITY_ABOVE_NORMAL
8TRHEAD_PRIORITY_ABOVE_HIGHEST
9TRHEAD_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类来实现线程本地存储的功能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: