- 从一个Java代码案例开始分析,线程安全问题造成的原因,从而引出synchronized、CAS原理等知识,并对其进行总结。
- 想要学好并发,必须要把Jvm学好。而Jvm的学习(学过一点),是需要投入大量的时间。目前,站在android开发的角度对并发的某些点进行学习;
一.案例分析
- Java下的线程安全分析,代码示例如下
- 以上的代码如何执行呢?是不是很有规律的,一个+然后再一个-,答案不是。
- 对counter增加volatile修饰,会实现很有规律的,一个+然后再一个-吗,答案不是。
- 原因:volatile处理的是一次性的原子操作(简单理解,就是一次完成的操作);
- 而以上代码存在的问题是:线程上下文切换带来的指令执行问题(暂时不太好理解这句话,不急,很快就知道);
- 我们需要查看汇编指令
- count++的字节码指令一共有4个:13-16行
- 我们按照代码来分析一下字节码指令
- 开始是线程2先执行,在线程2执行前3个字节码指令的时候,资源不够,会将时间片的使用权交给线程1
- 线程1执行完之后,主内存的是counter是-1,接着,又切换回线程2继续执行,那么,主内存中的counter就是1了。
- 总结一下
- ++操作,不是单纯的原子操作,里面从指令层面来说,它是4个指令完成一次加完之后的赋值(提数据,再计算,再赋值);
- 该案例引出的问题是:线程的上下文切换导致的安全性问题;
- 关于该问题会引出两个概念,临界区和竞态条件;
二.临界区和竞态条件
1.概念
- 临界区:一个程序运行多个线程本身没有问题,出现问题最大的地方在于多个线程访问共享资源。多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码为临界区;
- 竞态条件:多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称之为竞态条件(指多个代码行程的一组原子);
2.规避手段
- 为了避免临界区的竞态条件发生,JAVA提供多种手段进行规避
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
三.synchronized
1.对象锁
-
采用互斥方式让同一时刻最多只有一个线程持有对象锁,其它线程在获取这个对象锁会被阻塞,不用担心线程上下文切换;
-
Java默认对象提供锁:Jvm底层实现过程中,对于锁的判断依据全部扔到了对象上面进行判定;
-
synchronized的等价方案(存在弊端,容易产生内存泄漏)
- 推荐:单独给锁对象(更好可控)或者使用byte数组(byte[] b = new byte[0],更节省空间);
- 引出问题
- 为什么要用一个对象加锁?
- 锁到底加的什么内容?
- 引出问题
2.Jvm普通对象头32位Mark word
-
对象头中实际采用的是64位数据作为承载,如果使用的是数组,多出一个32位的长度
-
mark word:32位的数据,没有固定的数据格式(根据状态对于里面的数据进行变更);
3.Mark中的数据对于并发的支持
- 线程分布的不同锁分类:
- 偏向锁: 只针对于一个线程,单个线程体系下加锁,本质就只有一把,直接应用markword解决识别问题;
- 轻量级锁:只针对于两个线程,利用栈区结构来存储线程ID不同;
- 重量级锁:两个以上线程,利用一个全新的结构来存储不同的ID;
4.Monitor
- 先从案例分析重量级锁(最初的实现方案是Monitor)
public static void main(String[] args) throws InterruptedException {
byte[] b = new byte[0];
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++)
{
synchronized (b){
counter++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
synchronized (obj){
counter--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
- 当碰到synchronized的时候,会对对象头部的markWord进行修改,此时,重量级锁的标志位为01,向Jvm申请一把锁(可以理解成创建一个Monitor-实现具体锁的业务),然后把Monitor对象的地址塞入mark头;
- Monitor的实现,见下图
-
当Owner中的内容释放掉之后,会从entryList(等待容器)中提取线程,至于哪个线程去Owner容器是采取竞争的方案(通过算法);
-
基于公不公平的实现原理是基于上图的位置实现;
-
没有所谓的公平锁存在;
-
notify实际上就是,把在waitSet的线程移到了entryList中;
-
如果是单线程运行的情况下,需不需要加锁?
我们现在是纤细monitor占用空间大,而且在某些场景下没有比较
场景:
1.单个线程在运行---》仅仅只要进行标记一下--->直接标记在我们markword上面
2.两个线程在运行---》我的需求,要不就是1用,要不就是2用,我也不要monitor,但是需要有数据的记录
这个时候我们要想个地方来存两个线程的数据,所以我找了个地方,就是栈帧里面,好处是什么?
3.多个线程在运行(需要monitor的支持)
- 轻量级锁
- 锁膨胀:就是在线程慢慢开辟的过程中,它的处理方案的变更;
- Monitor对象过程注意事项
- 执行同步代码块内容,然后唤醒entryList中其他线程时,此处采取竞争策略,先到不一定先得,所以synchronize锁是非公平;
- 非公平锁: 在锁可用的时候,一个新到来的线程要占有锁,可以不需要排队,直接获得;
- 公平锁: 在锁可用的时候,一个新到来的线程要占有锁,需要排队,等待执行;
- 有没有比synchronized速度更快的方案?
- 利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronize阻塞算法,J.U.C在性能上有了很大的提升;
四.CAS
-
一套同步主内存跟工作内存数据的方案
-
实际上是在逻辑层面运用算法和判定去规定数据异常的方案;
-
CAS机制当中使用了3个基本操作数:
- 内存地址V
- 旧的预期值A
- 要修改的新值B
-
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
-
总结,通过以上,我们认识JAVA锁(了解底层实现),JAVA锁不是处理互斥的唯一方案,以及CAS理论的应用也可以解决互斥;
评论区