并发与锁知识
Posted 小智RE0
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发与锁知识相关的知识,希望对你有一定的参考价值。
最近在看大佬的知识讲解:
【计算机知识串讲】编程中的并发与锁
文章目录
CPU概述
CPU作为重要的计算机组件,由计算器与控制器组成,使用总线连接其他设备.
南桥主要用来连接一些带宽比较低的设备,比如鼠标,键盘等USB连接设备;
北桥主要用来连接带宽比较高的设备,比如CPU,RAM存储器,PCIE显卡…
CPU的常见参数:
架构
:
- X86 个人PC/服务器
- ARM 智能手机…移动设备
- MIPS 小型路由器
频率
:
例如 5GHZ : 每秒钟运行5G次基础的指令运算
核心数,线程数
:
每个CPU可集成多个CPU核心,可并行处理任务.
大多数情况下 核心数等于线程数,但也有超线程技术.
Cache
CPU缓存
Linux系统下查看CPU状态的常用指令
可以在proc/cpuinfo 这个文件目录下查看;
cat /proc/cpuinfo
可以使用top指令查看CPU的运行情况,内存的使用情况
top
vmstat命令可以查看虚拟机系统的各种参数;
vmstat
可用uptime命令;可以用来查看服务器已经运行了多久,CPU的运行情况.
uptime
进程,线程,协程
进程是计算机进行资源文件管理,调度,分配的最小单位,而线程是CPU的最小执行单位.
比如说Node.js是单线程的,但是他也可以应对高并发的场景,但它的IO是异步处理的
线程在执行时会有一些性能损耗,这些性能损耗主要来自于线程的创建,销毁和切换
;
线程本身在使用时就需要向CPU申请资源,而申请内核态会进入到内核态,由内核态帮助完成操作.
对于线程的创建和销毁
来说,可使用线程池来完成,
线程的切换可使用协程来完成操作;
协程作为一种用户自定义的线程,不涉及用户态与内核态的转化;
上面第一种直接进行线程的上下文切换,会产生一定的开销;
下面采用协程完成切换操作,减少了线程切换的开销;
synchronized
关于synchronized
在小知识点记录:同步与锁的基础知识之前有记录;
首先看这样一段demo;
public class Demo
public static void main(String[] args)
//触发时间;
long end = 10000;
long[] x = new long[1];
for (int i = 0; i < end; i++)
new Thread(()->x[0]++;).start();
System.out.println(x[0]);
运行后并没有达到预期的值10000;
那么实际上这里
x[0]++
的操作也就是分为了三步:
先获取到x[0]的值,然后加1处理,然后再将值重新赋值给x[0];
在jdk1.6之前锁的方式都是以重量级
锁的状态存在; 这里就需要涉及到用户态与内核态的切换.
操作锁的方法在C语言内部都有封装使用到:
那么对于重量级锁如何优化呢?
可以 定义一个state状态
,使用0,1 来区分不同的状态,比如 : 0
表示资源未被使用, 1
表示资源正在使用.
可编写这样一段伪代码:
//上锁;
void lock()
while(state == 1);
state = 1;
持有者 = 当前线程;
//开锁;
void unlock()
if(持有者 = 当前线程)
state = 0;
持有者 = null;
else
throw new Exception();
初步来看这样的设计还算比较合适,但是获取不到资源的线程会一直阻塞产生消耗;
尝试采用CAS(比较并且交换)的机制;进行优化;
这样采用while循环和CAS机制 也就是常说的自旋锁
,避免了线程进入内核态;整个操作都在用户态进行;
但这种方式也是有消耗的,因为一直自旋处理,也会对cpu产生消耗.
//上锁;
void lock()
while(!CAS(state,0,1));
持有者 = 当前线程;
//开锁;
void unlock()
if(持有者 = 当前线程)
state = 0;
持有者 = null;
else
throw new Exception();
在jdk1.4之后又对自旋锁进行了优化,即可以设定自旋的次数, 当自旋进行了一定的次数还没有获取到资源时就放弃自旋, 进入到 阻塞状态;
即适应性自旋锁
.
锁膨胀
这部分知识,先看对象, 对象基本由对象头
和对象数据内容
组成;
这部分在学习内存知识中有接触过;
在对象头中,普通对象一般包含markword与klassword,而数组对象还会具有
ArrayLength
;
其中在mark Word
中会记录到GC分代年龄,锁状态的信息…
比如这样的分布图;
锁标志位判断:
00
:轻量级锁,10重量级锁
;11
GC标志;01
:无锁/偏向锁;
对于无锁和偏向锁的判断,在偏向锁中会记录
线程的Id
,
轻量级锁中有指向栈中锁记录
lockRecord
的指针;
重量级锁中有指向互斥量monitor
的指针
大佬分享的源码链接:
synchronized的示例代码: https://github.com/sunwu51/notebook/blob/master/20.06/A.java
拉取到代码A之后;
可以在pom.xml中添加依赖;
但是不建议使用jol库;
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
首先运行测试方法1
;正常创建的对象,状态无锁
可看到对象的默认hashcode
为0;当第一次计算后才会将hashcode
的值存入; 可知hashcode
是惰性的.
接着模拟gc回收
,注意gc不一定会立即触发;
这次模拟gc执行后,对象的年龄增加了1岁;
执行测试方法2
;正常创建的对象,状态无锁,要是直接加锁就会变为轻量级锁.
执行测试方法3
;
当我们的程序启动一段时间后,正常创建的对象,状态就变为偏向锁, 并且此时的thread为0;
-XX:BiasedLockingStartupDelay=0可以指定默认就使用偏向锁,而不是无锁
执行测试方法4
;
在测试方法3的基础上:当我们的程序启动一段时间后,正常创建的对象,状态就变为偏向锁, 并且此时的thread为0; 那么此时当另一条线程尝试获取对象锁时,此时锁升级为轻量级锁
.
执行测试方法5
;
在测试方法4的基础上,当出现竞争时,就升级为重量级锁
执行测试方法6
,模拟锁竞争的过程;
具体看锁的工作原理
;
比如这个偏向锁加锁:
首先这个线程的id获取到之后CAS判断状态,
cas(thread, 0 , 当前线程);
状态为0直接获取到锁对象;
当状态为1时:
(1)若为可重入,则在栈上创建一个lockRecord
锁记录;指向它;
注意lockRecord
由两部分区域:M区:markword,O区:存放指针.
(2)那么就说明有其他的线程已经持有当前对象锁;那么此时需要升级为轻量级锁
;
偏向锁的解锁过程:
只需要删除在栈上最近的一个O区中指向当前对象的
lockRecord
锁记录;
轻量级锁:
也是CAS判断,注意判断的是指向锁记录
lockRecord
的指针;cas(指针, 0 , 当前创建的lockRecord);
状态为0直接获取到锁对象;
状态为1则:
(1)先找当前线程对应的栈,是否可找到对应的指针,若是则为可重入;
(2)说明有其他线程获取锁,那么就得升级为重量级锁;
ReentrantLock
ReentrantLock 锁
主要是基于AQS, 抽象同步队列;
- 基于CAS机制,获取到对象锁,将对象锁的持有者变为自己;
- 若获取不到对象锁,使用方法
tryAqire()
方法尝试获取,若获取到则将对象锁的持有者变为自己;若没有获取到则判断是否为可重入状态; - 若可重入则使用即可,若不是可重入 则使用
addWaiter()
方法,也就是将当前的线程作为一个node节点
存入到双向队列的尾部, 当存入队列后,这个node节点会被返回,触发acquireQueued()
方法,即队列中的节点请求获取对象锁;会判断是否为队列中的头结点
;[注意 之前所说的队列head和tail
都是空的,这里的头结点指的就是head节点的下一个节点]; - 若是头结点,则可使用
tryAqire()
方法尝试获取对象锁; 注意要是tryAqire()
方法失败的话,就走线程park
状态; - 若不是头结点,则判断线程的标志位,但是
第一次标志位没有修改
,那么就再次循环===>还是先判断是否为队列的头节点;(…),若不是队列的头结点;则可以让当前线程进入到park状态
[即线程让出当前CPU的资源];即发生阻塞; - 注意一个问题:要是在队列中的线程已经被park阻塞了,上一个线程执行完毕时,这时park状态的线程正要被触发
unpark
解除阻塞状态,此时又new来了一个新的线程,那么他们就会有竞争关系,而 之前阻塞状态的线程解除阻塞状态的这个过程还是比较消耗的…,竞争起来有一定的压力. - 那么实际上
ReentrantLock
是有一种公平锁状态的,它就不存在新来的线程和队列中解除阻塞状态
的线程 进行竞争,而是会直接将新来的线程存入到队列的尾部.
大佬分享的源码链接:
用到的lock的示例代码: https://github.com/sunwu51/notebook/blob/master/20.06/B.java
在测试方法1进行debug调试;
public static void test1() throws InterruptedException
Lock l = new ReentrantLock();
Thread t1 = new Thread(()->
l.lock();
System.out.println("线程" + Thread.currentThread().getId() +"拿到锁");
try
Thread.sleep(100000000000000000l);
catch (InterruptedException e)
System.out.println("线程" + Thread.currentThread().getId() +"释放锁");
l.unlock();
);
t1.start();
Scanner sc = new Scanner(System.in);
sc.nextLine();
new Thread(()->
l.lock();
System.out.println("线程" + Thread.currentThread().getId() +"拿到锁");
l.unlock();
).start();
sc.nextLine();
t1.interrupt();
就会进入到加锁方法lock()
;
其中有方法setExclusiveOwnerThread
将当前线程作为对象锁的持有者
;
紧接着线程进入休眠状态;
控制台输入一段内容,回车;继续debug.
那么此时这个线程过来就得acquire()
尝试;
可看到acquire()
方法的内部定义; 有tryAcquire()
方法 以及 acquireQueued
方法
tryAcquire()
方法的内部,实际是非公平锁的tryAcquire ;即nonfairTryAcquire()
方法
然后是addWaiter
方法将线程封装为Node节点, 放入到队列的尾部.
然后是acquireQueued
方法;
这里会看到一个for循环;
接下来在控制台输入一行文字后,回车, 第一个线程就会释放对象锁;
释放方法使用unlock()
那么实际上释放时会使用到方法 release()
;
注意方法unparkSuccessor()
来唤醒一个节点;
最终采用unpark
方法唤醒节点中封装的线程; 解除了阻塞
后面第二个线程被唤醒了,那么就会在队列的节点中开始判断处理…
接着来看测试方法2
创建线程,在持有锁的状态下,休眠5秒钟;然后循环100次进行加锁释放锁操作,每个线程在操作后会休眠10毫秒;那么也就保证了 第一个线程休眠5秒钟时,其他的100个线程就会
park
阻塞状态,按次序地排入到等待的队列中.
public static void test2() throws InterruptedException
Lock l = new ReentrantLock();
new Thread(()->
l.lock();
try
Thread.sleep(5000l);
catch (InterruptedException e)
e.printStackTrace();
l.unlock();
).start();
for(int i=0;i<100;i++)
int t = i;
new Thread(()->
l.lock();
System.out.println(t);
l.unlock();
).start();
Thread.sleep(10);
关于锁的总结:
以上是关于并发与锁知识的主要内容,如果未能解决你的问题,请参考以下文章