第五部分 高效并发

第12章 Java内存模型与线程

12.1 概述

计算机的运算速度和它的存储和通信子系统速度差距太大,大量时间花费在磁盘I/O、网络通信、数据库访问上。
除了充分利用计算机处理器的能力外,一个服务端对多个客户端提供服务实例一个更具体的并发应用场景。衡量一个服务性能好坏,每秒事务处理数(Transactions per second TPS)是重要的指标。
服务端是java最擅长的领域之一,不过如何写好并发应用程序是服务端程序开发难点之一,各种中间件服务器、各类框架都提供处理尽可能多的线程并发细节。

12.2 硬件的效率和一致性

由于计算机的存储时被与处理器的运算速度有几个数量级的差距,所以即如一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同不会内存之中,这样处理器就无序等待缓慢的内存读写了。
它引入了一个新问题:缓存一致性(Cache Coherence),即多个处理器运算任务涉及同一块主内存时,同步回主内存时以谁的缓存数据为准?这里会遵循一些协议,在读写时根据协议进行操作。
“内存模型”:在特定的协议操作下,对特定的内存或高速缓存进行读写访问的过程抽象。
在这里插入图片描述

12.3 Java内存模型

java虚拟机中试图定义一种java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台都能达到一致的内存访问效果。

12.3.1 主内存和工作内存

java内存模型的主要目标是定义程序中各个变量的访问规则,此处的变量指实例字段、静态字段、构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。
java内存模型归乡所有的变量存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都在工作内存中进行,不能直接读写主内存中的变量。
在这里插入图片描述

12.3.2 内存间交互操作

8种操作:
lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)
如果要把一个变量从主内存复制到工作内存,就要顺序的执行read和load操作,如果要把变量从工作内存同步回朱内存,就要顺序的执行store和write操作。
一些规则。

12.3.4 对于volatile型变量的特殊规则

关键字volatile是java虚拟机提供的最轻量级的同步机制,但不容易被正确理解,所以需要处理多线程数据竞争问题的时候一律用synchronized来进行同步。

当一个变量被定义为volatile后,保证了此变量对所有线程的可见性,即当一条线程修改了这个值,新值对于其他线程来说是立即可知的。但是java里的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
由于volatile变量只保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。(1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。(2)变量不需要与其他状态变量共同参与不变约束。

使用volatile变量的第二个语义是禁止指令重排序优化。普通的变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获得正确的结果,但不能保证变量赋值操作的顺序与代码中的执行顺序一致。
指令重排序会干扰程序的并发执行。

12.3.4 对于long和double型变量的特殊规则

12.3.5 原子性、可见性和有序性

原子性(Atomicity):有java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,大致可以认为基本数据类型的访问读写具备原子性。如果应用场景需要更大的范围保证原子性,java内存模型提供了lock和unlock操作来满足这种需求,反映到代码中就是同步块—synchronized关键字,因此synchronized块之间的操作也具备原子性。

可见性(Visibility):可见性是指一个线程修改了共享变量的值,其他线程能够立即得知这个修改,synchronized和final也能实现可见性。

有序性(Ordering):如果在本线程观察,所有操作都是有序的,如果在一个线程观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。java提供了volatile和synchronized来保证线程之间的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

12.3.6 先行发生原则(happens-before)

java语言中有一个先行发生原则,它是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A产生的影响能被操作B观察到。
时间先后顺序和先行发生顺序之间基本没有太大的关系,一切必须以先行发生为准。

12.4 Java与线程

12.4.1 线程的实现

线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
Thread类与大部分java API有显著差别,它的所有关键方法都是声明Native的,在Java API中,一个Native方法意味着这个方法没有使用或无法使用平台无关的手段来实现。
实现线程的3种方式:使用内和线程线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

  1. 使用内核线程实现
    内核线程(Kernel-Level Thread, KLT) 是直接由操作系统内核支持的线程。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口—轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上讲的线程。
    但轻量级进程会有局限性:由于是基于内核线程实现的,所以各种线程操作,都需要进行系统调用,需要在用户态(User Mode)和内核态(Kernel Mode)来回切换。
    轻量级进程和内核线程1:1的关系
  2. 使用用户线程实现
    狭义上的用户线程指完全建立在用户空间的线程库上,系统内和不能感知线程存在的实现。这种线程不需要切换到内核态,快速低消耗,可以支持更大的线程数量。但是所有的线程操作都需要用户程序自己处理。
    进程与用户线程之间1:N的关系
  3. 使用用户线程和轻量级进程混合实现
    操作系统提供支持的轻量级进程作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。
    在这里插入图片描述
  4. Java线程的实现
    对于Sun JDK, 使用一对一的线程模型实现,一条java线程就映射到一条轻量级进程中。
    在Solaris平台,既支持一对一也支持多对多

12.4.2 Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有2种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
如果使用协同式线程调度,线程的执行时间由线程本身控制,线程把自己的工作执行完之后,要主动通知系统切换到另一个线程上。实现简单,没有什么线程同步问题,但由于线程执行时间不可控制,容易阻塞。
如果使用抢占式线程调度,每个线程由系统来分配时间,线程的切换不由线程本身决定,这种就不容易发生一个线程导致整个进程阻塞的问题。java使用的线程调度方式就是抢占式调度

12.4.3 状态转换

– New:新创建的一个线程,处于等待状态。
–Runnable:可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在 Runnable 中包含了 Ready、Running 两个状态,当线程调用了 start() 方法后,线程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是谦让的允许当前线程让出CPU,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争CPU,直至执行。
–Timed_waiting:指定时间内让出CPU资源,此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil。
–Wating:可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过 notify、notifyall 唤醒。下列方法都可以触发:Object.wait、Thread.join、LockSupport.park。
–Blocked:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如 synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。
–Terminated:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。

阻塞状态和等待状态的区别是:阻塞状态在等待一个排他锁,这个事件将在另外一个线程放弃这个锁时发生;而等待状态则是在等待一定时间,或者唤醒动作的发生,在程序等待进入同步区域时,线程将进入这种状态。
(在进入synchronized方法之前因为抢不到锁对象而进入阻塞状态,进入阻塞队列。进入到synchronized方法后由于调用了wait()方法而进入等待状态,此时进入等待队列,等待其它线程调用它的notify()方法将他唤醒。)
线程状态转换关系

第13章 线程安全和锁优化

13.2 线程安全

如果一个对象可以安全地被多个线程同时使用,那它就是线程安全的。
更精确的定义:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得准确的结果,那么这个对象是线程安全的。

13.2.1 Java语言中的线程安全

按照线程安全的“安全程度”由强至弱排序,可以将java语言中各种操作共享的数据分为以下5类:

  1. 不可变
    不可变(Immutable)的对象一定是线程安全的。如果共享数据是基本数据类型,那么使用final关键字就可以保证不可变。如果共享数据是一个对象,那就需要对象的行为不会对其状态产生任何影响才行,比如String对象,他是一个典型不可变对象,我们调用它的substring()、replace()等方法都不会影响它原来的值,而是返回一个新构造的字符串对象。
    最简单就是把对象中带有状态的变量声明为final

  2. 绝对线程安全
    Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。vector是一个线程安全的容器,因为它的add()、get()、size()这类方法被synchronized修饰。但是在多线程环境中,如果不在方法调用端作额外的同步措施的话,使用这段代码仍然不安全。

  3. 相对线程安全
    相对线程安全就是通常意义上讲的线程安全,它需要保证对这个对象单独的操作是安全的。大部分的线程安全类都属于这种类型,如Vector、HashTable等。

  4. 线程兼容
    线程兼容是指对象本身不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发环境中可以安全使用。如ArrayList和HashMap等。

  5. 线程对立
    线程对立是指无论调用端是否采取了同步措施,都无法在多线程中并发使用的代码。很少出现。

13.2.2 线程安全的实现方法

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

    最基本的互斥同步手段是synchronized关键字,synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和moniterexit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果java程序中的synchronized明确指定了参数对象,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
    在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的执行monitorexit指令会将锁的计数器减1,当计数器为0时,锁就被释放。如果获取对象的锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放。

    还可使用java.util.concurrent(J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock和synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有区别。ReentrantLock增加了一些高级功能:等待可中断、可实现公平锁、以及锁可以绑定多个条件。等待可中断指持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。可实现公平锁指多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

  2. 非阻塞同步
    互斥同步的最主要问题是进行线程阻塞和唤起所带来的性能问题,因此这种同步也叫阻塞同步(Block Synchronized)。从处理问题的方式上来说,互斥同步属于一种悲观的并发策略。随着硬件指令集的发展,有了另一种选择:基于冲突检测的乐观并发策略,就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,就采取其他补偿措施。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作叫非阻塞同步(Non-Block Synchronized)

    硬件保证一个从语义上看起来要多次操作的行为只通过一条处理器指令就能实现,这类常用的指令有:测试并设置(Test-and-Set)、获取并增加(Fetch-and-Increment)、交换(Swap)、比较并交换(Compare-and-Swap)简称CAS、加载链接/条件存储(Load-Linked/Store-Conditional)简称LL/SC。后面两个是现代新增的。CAS指令需要三个操作数,是一个原子过程。CAS操作由sun.misc.Unsafe类里面的compareAndSwapInt()等几个方法包装提供。
    CAS显然无法覆盖互斥同步的所有使用场景,还会发生“ABA"问题,改用传统的互斥同步可能会比原子类更高效。

  3. 无同步方案
    可重入代码 线程本地存储

13.3 锁优化

13.3.1 自旋锁和自适应自旋

前面提到互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这给系统的并发性带来了极大的压力。同时,共享数据的锁定状态只会持续很短时间,为了这段时间去挂起和恢复线程不值得。
自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。 由于线程在这一过程中保持执行,因此是一种忙等待。 一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

13.3.2 锁消除

锁消除指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那么就可以把他们当成栈上数据对待,认为他们是线程私有的,同步加锁就无需进行。

13.3.3 锁粗化

我们编写代码的时候,总是推荐将同步块的作用范围限制的尽量小–只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那即使没有线程竞争频繁的进行互斥同步操作会导致不必要的性能损耗。如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

13.3.4 轻量级锁

传统的锁机制称为重量锁,轻量级锁不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。
要理解轻量锁和偏向锁,必须从HotSpot虚拟机的对象头的内存布局开始介绍。HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,比如哈希码(HashCode)、GC分代年龄等,这部分长度为64bit,官方称为”Mark Word",它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法去对象类型数据的指针。
在这里插入图片描述
虚拟机用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果这个操作成功了,那么这个线程就拥有了该对象的锁。
轻量锁提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,轻量锁会比传统重量级锁更慢。

13.3.5 偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提升程序的运行性能。如果说轻量锁是在无竞争的情况下使用CAS操作去除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁会偏向于第一个获得他的线程,如果接下来该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。
偏向锁可以提高带有同步但无竞争的程序性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值