您的位置:首页 > 编程语言 > Java开发

Java多线程编程实战指南(核心篇)读书笔记(二)

2017-08-03 21:51 232 查看
(尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76651408冷血之心的博客)

博主准备恶补一番Java高并发编程相关知识,接下来将阅读该书,并且进行比较详细的总结,好记性不如烂笔头,加油。Java多线程编程实战指南(核心篇)读书笔记(二),主要记录该书第三章的基本概念等知识,后续部分将会持续更新哦~欢迎关注本博客。

目录:
Java多线程编程实战指南(核心篇)读书笔记(一)

Java多线程编程实战指南(核心篇)读书笔记(二)
Java多线程编程实战指南(核心篇)读书笔记(三)

Java多线程编程实战指南(核心篇)读书笔记(四)

Java多线程编程实战指南(核心篇)读书笔记(五)

Java线程同步机制线程同步机制简介:从广义上说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字和一些相关的API,如Object.wait( )/.notify( )等

锁概述:线程安全问题的产生:多个线程并发访问共享变量、共享资源
临界区:锁的持有线程在其获得锁之后和释放锁之前的这段时间内所执行的代码被称为临界区。
 Java虚拟机对锁的实现划分:内部锁:通过synchronized关键字实现
显示锁:通过java.util.concurrent.locks.Lock接口的实现类实现

锁的作用:保护共享数据以实现线程安全,包括保障原子性可见性有序性原子性:锁通过互斥来保障原子性,互斥是指一个锁一次只能被一个线程所持有,所以,临界区代码只能被一个线程执行,即保障了原子性。
可见性:通过写线程冲刷处理器缓存和读线程刷新处理器缓存实现。获得锁之后,需要刷新处理器缓存,使得前面写线程所做的更新可以同步到本线程
释放锁需要冲刷处理器缓存,使得当前线程对共享数据的改变可以被推送到下一个线程处理器的高速缓冲中。

有序性:写线程在临界区中所执行的一系列操作在读线程所执行的临界区看起来像是完全按照源代码顺序执行的。原子性和可见性>>有序性

与锁相关的重要概念:可重入性:一个线程在持有一个锁的时候可以再次申请该锁如何实现可重入性?
可重入锁可以被理解为一个对象,该对象包含一个计数器属性,获取锁计数器+1,释放锁计数器-1;

锁的争用与调度:锁可以被看做多线程程序访问共享数据时所持有的一种排他性资源
锁的调度:包括公平调度策略和非公平调度策略
内部锁属于非公平锁;显示锁则两者都支持

锁的粒度:锁的粒度过粗会导致线程在申请锁的时候需要进行不必要的等待,影响性能

锁的开销和可能导致的问题:锁的开销主要包括锁的申请和释放产生的开销,锁可能导致上下文切换,开销主要是处理器时间
问题:锁泄漏:指一个线程获得锁之后,由于程序的错误,致使该锁一直无法被释放而导致其他线程一直无法获得该锁的现象。
锁的不正当使用还会导致死锁、锁死等线程活性故障

内部锁:synchronized关键字内部锁表现为整体并发中的局部串行
内部锁的调度:JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。
入口集中的线程被称为内部锁的等待线程
调度:当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次(上一次申请失败,才会出现在入口集中)申请锁的机会;被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁
即这是一种非公平的内部锁调度策略。

显示锁:Lock接口基本概念:显示锁提供了一些内部锁不具备的特性,但并不是内部锁的替代品
显示锁的使用方法:创建Lock接口的实例
在访问共享数据前申请相应的显示锁
在临界区中访问共享数据
共享数据访问结束后释放锁

显示锁的调度:支持公平和非公平的调度方式,默认采用非公平调度
显示锁和内部锁的比较:内部锁简单,但是不灵活
显示锁支持在一个方法内申请锁,却在另一个方法里释放锁
显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁

读写锁(Read/Write Lock):允许多个线程可以同时读取共享变量,但是一次只允许一个线程对共享变量进行更新
适用场景:只读操作比写操作要频繁的多
读线程持有锁的时间比较长

锁的适用场景check-then-act操作
read-modify-write操作
多个线程对多个共享数据进行更新

线程同步机制的底层助手:内存屏障概念重温:获取锁之后,需要刷新处理器缓存(确保该锁的当前持有线程可以读取到前一个持有线程所做的更新)
释放锁之后,需要冲刷处理器缓存(确保该锁的持有线程对这些数据的更新对该锁的后续持有线程可见)

内存屏障:在指令序列中就像一堵墙一样使其两侧的指令无法穿越(即不可以重排序)。

内存屏障分类:按照可见性保障分类:加载屏障(LoadBarrier):刷新处理器缓存,获得锁之前插入
存储屏障(StoreBarrier):冲刷处理器缓存,释放锁之后插入

按照有序性保障:获取屏障(AcquireBarrier):在一个读操作插入该屏障
释放屏障(ReleaseBarrier):在一个写操作插入该屏障

内存屏障在锁中的使用:获取锁
加载屏障
获取屏障
临界区
释放屏障
存储屏障
其中:3和5用来禁止指令重排序

Java线程同步机制就是使用内存屏障在具体实现的

锁与重排序:“许进不许出”原则:临界区外的语句可以被编译器重排序到临界区之内,但是临界区之内的语句不可以被重排序到临界区之外

具体规则:临界区内的操作不允许被重排序到临界区之外
临界区内的操作之间可以重排序
临界区外的操作之间也可以重排序

解释:Java虚拟机会在临界区的开始之前和结束之后分别插入一个获取屏障和释放屏障,从而禁止临界区内的操作被排到临界区之前和之后

轻量级同步机制:volatile关键字用来读取变量的相对新值
volatile被称为轻量级锁:保证可见性和有序性
仅仅能保证volatile变量操作的原子性,但是没有锁的排他性
但是它不会导致上下文切换
作用volatile关键字可以保证对long/double型变量的写操作具有原子性
volatile变量写操作与内存屏障:普通变量的读写操作
释放屏障
写操作
存储屏障

volatile变量读操作与内存屏障:加载屏障
读操作
获取屏障
普通变量的读写操作

volatile变量的开销:不会导致上下文切换,开销比锁小
读取变量的成本比临界区中读取变量要低,但是其读取成本可能比读取普通变量要高;因为每次读取都从高速缓存或者主内存中读取,无法被暂存在寄存器中,从而无法发挥访问的高效性

应用场景:使用volatile变量作为状态标志
保障可见性
替代锁:利用该变量对写操作的原子性
volatile适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量
我们可以将多个线程共享的一组状态变量合并成一个对象,用于一个volatile变量来引用该对象,从而替代锁

单例模式:基于双检锁的volatile的单例模式
基于静态内部类的单例模式
基于枚举类型的单例模式

CAS与原子变量Compare and Swap
i++其实是一个read-modify-write操作,通过CAS可以转变为if-then-act操作
Atomic包中都是基于CAS实现
ABA问题:对于共享变量V,当前线程看到他的值为A的那一刻,其它线程将其改为了B,当前线程执行CAS操作的时候该变量又被其它线程改为了A,那么此时我们是否认为变量V的值没有被其它线程更新过呢????
避免反复:每次更新,打下一个时间戳

对象的发布与逸出对象发布是指对象能够被其作用域之外的线程访问。
对象逸出是指当一个对象的发布出现我们不期望的结果或者对象发布本身不是我们所希望的时候,就成为对象逸出。

如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,我会持续更新后续章节学习笔记,可以进群366533258一起交流学习哦~

本群给大家提供一个学习交流的平台,内设菜鸟Java管理员一枚、精通算法的金牌讲师一枚、Android管理员一枚、蓝牙BlueTooth管理员一枚、Web前端管理一枚以及C#管理一枚。欢迎大家进来交流技术。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: