Java并发编程分享

参考及引用

  1. java并发编程实战
  2. 深入浅出Java虚拟机
  3. thinking in java
  4. effective java
  5. concurrent programming in java design

线程安全性

Q1:什么是线程?

线程也被称之为一个轻量级进程, 是基本的调度单位.
线程允许在同一个进程中同时存在多个程序控制流.

线程会共享进程范围内的资源, 例如内存句柄.
但每个线程都有各自的线程计数器,栈,以及局部变量等.

Java并发编程分享_第1张图片

由于同一个进程中的所有线程都将共享进程的内存地址空间.
因此这些线程都能访问相同的变量,并在同一个堆上分配对象.
因此需要一种比在进程间共享数据粒度更细致的数据共享机制.

Q2: 什么是线程安全性?

当多个线程访问某个类时, 这个类始终都能表现出正确的行为, 则称这个类是线程安全的. 在线程安全性的定义中,最核心的概念就是正确性.即, 某个类的行为与其规范完全一致.

Q3: 多线程带来的问题?

  1. 安全性问题 竞态条件: 对于同一个对象,由多个线程读写.非串行因素导致变量结果难以预测.
  2. 活跃性问题 死锁, 饥饿, 活锁
  3. 性能问题 线程调度器挂起活跃线程转而运行另一个线程时,会出现上下文切换操作. 当线程共享数据, 必须使用同步机制.同步机制或抑制某些编译器优化.

Q4: 如何实现线程安全性?

  1. 线程封闭
    如果仅在单线程内访问数据,就不需要同步. 是实现线程安全性最简单的方式之一.
    当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性, 即使被封闭的对象不是线程安全的.[CPJ]
    1. ad-hoc线程封闭: 维护线程封闭性的职责完全由程序实现来承担.
      在volatile变量上存在一种特殊的线程封闭: 确保只有单个线程对共享的变量执行写入操作, 就可以安全的共享变量.将修改操作封闭在单线程内, 防止竞态条件的发生
    2. 栈封闭: 局部变量
    3. ThreadLocal
  2. 不变性
    无状态对象一定是线程安全的.
    不可变对象一定是线程安全的.
    满足:
    对象创建以后其状态就不能修改.
    对象的所有域都是final类型.
    对象是正确创建的.
  3. 安全发布
    1. 可变对象对需通过安全的方式来发布, 这通常意味着在发布和使用该对象的线程时都必须使用同步.
      在静态初始化函数中初始化一个对象引用.
      将对象的引用保存到volatile类型的域或者atomicReferance对象中.
      将对象的引用保存到某个正确构造对象的final类型域中.
      将对象的引用保存到一个由锁保护的域中. (可以放在线程安全的集合类中)
    2. 在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象(只读共享).
    3. 对于可变对象, 不仅发布时需要使用同步, 而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性.
      要安全的共享可变对象,这些对象就必须被安全的发布, 并且鼻血是安全的或者由某个锁保护起来的.

内存模型

Q1: 什么是内存模型?

wiki:

The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language.

Java虚拟机规范中,试图定义一种java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异.其主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.[JVM P362]

Java并发编程分享_第2张图片

ref: 图 线程/主内存/工作内存三者关系.

JMM中,定义了8种操作来完成: lock,unlock,read,load,use,assign,store,write. JMM是围绕着在并发过程中,如何处理原子性,可见性和有序性这3个特征来建立的.

  1. 原子性: read,load,assign,use,store,write指令可以直接保证操作的原子性.大致可以认为基本类型的数据的访问读写是具有原子性的(long和double的非原子协定需要注意).更大范围的原子性由lock和unlock保证.
  2. 可见性: 当一个线程修改了共享变量的值,其他线程能够立刻得知.
  3. 有序性: 在本线程内观察,所有操作是有序的;如果在一个线程中观察另外一个线程,所有操作都是无序的. 前半句指"线程内表现为串行语义",后半句则指指令"重排列现象"和"工作内存和主内存同步延迟"现象.

JMM为程序中的所有操作定义了一个偏序关系[1].称之为happens-before(先行发生)关系.

要想保证执行操作B的线程看到操作A的结果,(无论AB是否在同一个线程).那么AB之间则需要必须满足Happens-Before关系.若AB之间不存在Happens-before关系,则JVM可以对它们任意的重排列.

  1. 程序顺序规则: 程序中操作A在操作B之前,那么在线程中A操作也在操作B之前.
  2. 监视器锁规则: 在监视器上的解锁操作必须在同一监视器锁上的加锁操作之前执行.
  3. volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行.
  4. 线程启动规则: 如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
  5. 线程技术规则: 线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false.
  6. 中断规则: 当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行.
  7. 传递性: 如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么A在C前执行.
public class A {
    private int value = 0;
    public void setValue(int value) {
        this.value = value;
    }
    public int getValue() {
        return this.value;
    }
}

判断上例是否线程安全?

Q2: volatile关键字

  1. volatile关键字保证了变量的可见性.
  2. volatile抑制指令重排列. 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.
  3. 那么volatile变量是线程安全的么?并不是.

当且仅当满足以下所有条件时,才应该使用volatile变量:

1. 对变量的写入不依赖变量的当前值, 或者确保只有单个线程更新变量的值.

2. 该变量不会与其他状态变量一起纳入不变性条件中.

3. 在访问变量时不需要加锁.

Q1: 加锁引发的问题? 

分类 现象名称 常见原因 处理方案
活跃性问题 死锁 每个线程都拥有其他线程需要的资源, 同时又等待其他人已经拥有的资源,且每个人在获取到完整所需资源前都不会放弃已有资源.
  1. 数据库处理:检测到一组事务发生死锁(通过在等待关系的有向图中搜索循环)时, 选择一个牺牲者并放弃该事务. 
  2. 当一组JVM线程发生死锁时,则没有对应的回复机制.因此需要额外小心.
    2.1. 通过锁顺序避免死锁
    1. 可以确定线程获取锁的顺序
    2. 无法确定获取所顺序 hash值大小 + 加时锁
    3. 开放调用
    2.2. 资源死锁
    综上: 设计时考虑锁顺序,尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入文档且始终遵循.
饥饿

线程由于无法访问它所需的资源而不能继续执行.常见资源: CPU时钟周期.

如: 对线程优先级使用不当, 或持有锁的线程执行一些无法结束的结构.

注意线程优先级与平台相关.

注意线程执行结构

响应性问题 等待导致相应不及时. 视场景而定
活锁 线程不会阻塞,但也不能继续执行.多个相互协作的线程都对彼此进行响应从而修改各自状态,使得任何一个线程都无法继续执行.

在重试机制中引入随机机制

 Q2: 性能和可伸缩性?

分类 现象名称 现象描述 相关
性能和可伸缩性 性能和可伸缩性 上下文切换: 如果可运行的线程大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程可以使用CPU.这一过程将导致一次上下文切换. 它将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文.
内存同步

多线程下,需对共享变量进行同步.

  1. synchronized和volatile抑制编译器优化, 使用了内存栅栏(memory barrier), 可以刷新缓存,使缓存无效,刷新硬件的写缓冲,以及停止执行通道.
  2. synchronized针对无竞争的同步进行了优化(锁消除,轻量级锁, 偏向锁)
  3. 竞争优化:锁粗化

无须关注无竞争同步带来的开销.

同步会增加共享内存总线的通讯量, 而总线的带宽是有限的.

阻塞 当锁上发生竞争时,竞争失败的线程会阻塞.

自旋锁/通过操作系统挂起.

效率的高低取决于上下文切换的开销,以及等待时间.

 

Q3: 优化?

减少锁的竞争能够提高性能和可伸缩性.

有三种方式可以降低锁的竞争程度:

分配 优化方式 典型应用
优化

 
减少所的持有时间

缩小锁范围

减小锁粒度

降低锁的请求频率

锁分解

锁分段

使用带有协调机制的锁,这些机制允许更高的并发机制 放弃使用独占锁, 替换为并发容器,读写锁, 不可变对象和原子变量

锁分段实例: concurrentHashMap

Java并发编程分享_第3张图片

并发容器的AQS

Q1: 如何形容多线程下竞争.

元素: 多个线程, 共享的资源/对象

规则: 对于每个线程而言,在等待条件为真(获取对象锁/资源,信号量为真)前,一直阻塞,条件为真时继续执行.

条件队列(condition queue): 它使得一组线程,能够通过某种方式来等待特定的条件变为真. 条件谓词(condition predication):使某个操作成为状态依赖操作的前提条件.

e.g: 对于有界队列中,当队列不为空,take方法才能执行,否则必须等待.则,对于take方法来说,"队列不为空"就是它的条件谓次.

在条件等待中, 存在一种三元关系: 加锁, wait, 一个条件谓词.

Java并发编程分享_第4张图片

//画图:P245

将条件队列封装起来,这种建议与线程安全最常见的设计模式并不一致:该模式建议使用对象的内置锁来保护对象自身的状态.在该情况下,对象自身既是锁,又是条件队列.

Q2: AQS

abstract queued synchronizer AQS是一个用于构建锁和同步器的框架.解决了实现同步器时设计的大量细节问题, 如解决了等待线程采用FIFO队列操作顺序. JUC中的许多可阻塞类,如ReentrantLock, ReentrantReadWriteLock等,都是基于AQS构建的.

各种形式的获取和释放操作.

boolean acquire() throws InterruptedException {
	while(当前操作不允许获取操作){
		if(需要阻塞或许请求){
			如果当前线程不再阻塞队列中,则将其插入队列中
			阻塞当前线程		
		} else{
			return false;		
		}
	}
	可能更新同步器状态;
	如果线程位于队列中,则移除
	return ture;
}

void release(){
	更新同步器的状态;
	if(新的状态允许某个被阻塞的线程获取成功){
		接触队列中一个或多个线程的阻塞状态
	}	
}

管理同步器中的状态.

更新同步器中的状态.

你可能感兴趣的:(java,java,并发)