并发基本原理以及常用工具类介绍

2019-05-21  本文已影响0人  胖嘟嘟洒酒疯

并发编程的挑战:

如何减少上下文切换:

无锁并发编程、CAS算法、使用最少线程

分析线程状态使用jstack命令dump线程信息 ex:jstack 8444

避免死锁的几个常见方法:

Java并发机制的底层实现原理:

volatile的两条实现原则:
synchronized的实现原理与应用:

Java中的每一个对象都可以作为锁。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

对象头:

在虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据、对齐填充

对象头一般由三部分组成: mark word、指向类的指针、数组长度(只有数组对象才有)
32位虚拟机中对象头存储结构如图所示:(Epoch 偏向锁时间戳)


image

锁介绍:

偏向锁:

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否
存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

轻量级锁:

1)轻量级锁加锁
​ 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并
将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用
CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失
败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
​ 2)轻量级锁解锁
​ 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成
功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是
两个线程同时争夺锁

如何实现原子操作:

(1)使用cas循环实现原子操作

(2)使用锁机制实现原子操作

JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

从源代码到指令序列的重排序:

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序。
​ 2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序。
​ 3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行。

指令重排序对多线程的影响:

class ReorderExample {
    int a = 0;
    boolean flag = false;
    
    public void writer() {
        a = 1; // 1
        flag = true; // 2
    }
    Public void reader() {
        if (?flag) { // 3
        int i = a * a; // 4
        ……
    }
}

flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行
writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,<font color=#FF0000>不一定</font>能看到线程A在操作
1对共享变量a的写入

volatile的特性:


并发编程工具类学习:

1、Unsafe

简书地址-不安全类讲解

脚本之家-不安全类通俗详解

1、直接内存访问

Unsafe的直接内存访问:用Unsafe开辟的内存空间不占用Heap空间,当然也不具有自动内存回收功能。做到像C一样自由利用系统内存资源。

2、Unsafe类源码分析

Unsafe的大部分API都是native的方法,主要包括以下几类:

1)Class相关。主要提供Class和它的静态字段的操作方法。

2)Object相关。主要提供Object和它的字段的操作方法。

3)Array相关。主要提供数组及其中元素的操作方法。

4)并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等。

5)Memory相关。提供了直接内存访问方法(绕过Java堆直接操作本地内存),可做到像C一样自由利用系统内存资源。

6)系统相关。主要返回某些低级别的内存信息,如地址大小、内存页大小。

2、AQS

AQS深度剖析

3、CountDownLatch

CountDownLatch的用法:

CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

4、Semaphore

Semaphore(信号量)用来控制并发访问数量,当一个任务进入时,资源计数器会减一(获得许可证),当任务离开时,资源计数器加一(归还许可证),当计数器为0时,如果有任务需要获取许可才能执行操作,则此任务会阻塞。

5、CyclicBarrier

多个线程相互等待,同时执行。

上一篇下一篇

猜你喜欢

热点阅读