Java多线程编程实战指南 核心篇 总结-2

1. 线程安全性

线程安全性体现在三个方面:1.原子性 2.可见性 3.有序性

原子性:

原子操作是多线程环境下的一个概念,它是针对访问共享变量的操作而言的。

java中有两种方式实现原子性。一种是使用锁。另一种是利用处理器专门提供的CAS(Compare-and-Swap)指令。
它们的差别在于:
锁通常是在软件这一层次实现的,
而CAS是直接在硬件这一层次实现的,它可以被看作“硬件锁”。

在java语言中,long型和double型以外的任何类型的变量的写操作都是原子操作。
因为,long型和double型在java中是64位,而在32位JVM系统中,其写操作会通过两次执行完成,先写低32位,再写高32位。
尽管如此,java语言规范规定了,对volatile 修饰的long和double类型的写操作是具有原子性的。

java语言中针对任何变量的读操作都是原子操作。

可见性:

可见性问题与计算机的存储系统有关。程序中的变量可能会被分配到寄存器而不是主内存中进行存储。每个处理器都有其寄存器,而一个处理器无法读取另一个处理器上的寄存器中的内容。

那么怎么保证读取的正确性恩,一个处理器可以通过缓存一致性协议来读取其他处理器的高速缓存中的数据。

缓存一致性协议

    MESI(Modified-Exclusive-Shared-Invalid)协议对内存数据访问控制类似于读写锁,它针对同一个地址的读操作是并发的,针对同一个地址的写操作是独占的,即任意时刻对一个地址的写操作只能由一个处理器执行。

有序性:

MESI 协议中的一个缓存条目的Flag值有以下4中可能。

状态描述监听任务
M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。缓存行必须时刻监听所有试图读该缓存行相对应主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。(监听所有试图读该缓存行相对应主存的操作,会触发缓存行写入主存操作)
E 独享、互斥 (Exclusive)该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared)该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid)该Cache line无效。

 以下摘取《java多线程编程实战指南 核心篇》

下面看看使用MESI协议的处理器如何实现内存读、写操作的。假设内存地址A上的数据S是处理器Processor0和处理器Processor1可能共享的数据。

首先是Processor0读取数据S的实现:

如果P1状态为M,在往总线发送Read Response消息前,会触发缓存行写入主存操作,写入主内存后,相应的缓存条目状态会被更新为S。

下面讨论Processor0往地址A写数据S的实现:

当P0所找到的缓存条目状态为I时,往总线放送Read Invalidate消息(Read消息和Invalidate消息的组合,Read消息会触发其他某一个处理器的缓存条目状态仍然为M的变为S,Invalidate消息触发其他处理器的缓存条目状态仍然为S的变为I)。

从上面的例子看,在多线程共享变量的情况下,MESI协议已经能够保障一个线程对共享变量的更新对其他处理器上运行的线程来说是可见的,既然如此可见性有何以存在呢?

写缓冲器和无效队列

写缓冲器:

把之前的两个同步操作,转换为一个同步操作(发送Invalidate消息)一个异步操作(等待Invalidate response消息+写缓存条目)

无效队列:

存储转发:

 

重新分析可见性

重排序:

java平台包含两种编译器:静态编译器(javac)和动态编译器(JIT编译器,java运行态,解释器和JIT编译器共同作用)。

前者的作用是将java源代码(.java文本文件)编译为字节码(.class二进制文件),它是在代码编译阶段介入的。
后者的作用是将字节码动态编译为java虚拟机所在宿主机的本地代码(机器码),它是在java程序运行过程中介入的。

volatile关键字、synchronized关键字都能够实现有序性。

在java平台中,静态编译器(javac)基本上不会执行指令重排序,而JIT编译器则可能执行指令重排序,

为了优化处理器指令的执行效率,处理器会在不影响但线程结果的情况执行指令重排序(但会影响多线程的结果),为了优化CPU读写内存的效率引入了写缓存器和无效队列,但是由于它们的存在,会导致内存操作的重排序。

 

上下文切换

线程上下文切换在某种程度上可以被看作多个线程共享同一个处理器的产物。

因为java的线程使用抢占式调度策略,所在线程在CPU上是以断断续续的方式执行任务的。当线程在切入和切出时,操作系统就需要保存和恢复线程相应的进度信息,这个进度信息就被称为上下文(Context)。它一般包括通用寄存器的内容和程序计数器。在切出时,操作系统需要将上下文保存到内存中,以便被切出的线程稍后占用处理器继续其运行时能够在此基础上进展。在切入时,操作系统需要从内存中加载被选中线程的上下文。

从java应用的角度来看,一个线程的生命周期状态在Runnable状态与非Runnable状态(包括blocked、waiting、timed_waiting中的任意一个子状态)之间切换的过程就是一个上下文切换的过程。

上下文切换的分类及具体诱因

分类:自发性上下文切换和非自发性上下文切换

自发性上下文切换:
执行下列任意一个方法都会引起自发性上下文切换,

1. Thread.sleep(long milis)

2. Object.wait()/wait(long timeout)/wait(long timeout, int nanos)

3. Thread.join()/join(long timeout)

4. LockSupport.park()

5. Thread.yield()  (可能会,也可能不会导致上下文切换,这具体取决于线程调动器)。
线程发起I/O操作(确切的是阻塞式IO)或者等待其他线程持有的锁也会导致自发性上下文切换。

非自发性上下文切换:

导致非自发性上下文切换的常见因素包括被切出线程的时间片用完或者有一个被被切出线程优先级更高的线程需要运行。
从java平台角度来看,java虚拟机的垃圾回收(Garbage Collect)动作也可能导致非自发性上下文切换。这是因为垃圾回收器在执行垃圾回收的过程中可能需要暂停所有应用线程才能完成其工作。

资源调度策略的一个常见特性就是它能否保证公平性。

在资源的持有线程占用资源的时间相对长或线程申请资源的平均间隔时间相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下,可以考虑使用公平调度策略。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值