0819(035天 线程/进程06 synchronizedu详解)
每日一狗(田园犬西瓜瓜)
线程/进程06 synchronizedu详解
文章目录
1. synchronizedu的底层实现原理
synchronizedu是通过对象内部的一个叫做监视器锁monitor来实现的,监视器锁本质上又是依赖于从操作系统的Mutex Lock互斥锁来实现的,但是这个系统实现的互斥锁在不同线程之间的切换需要从用户态转换到核心态,这个成本很高,状态之间的转换需要耗费大量的时间。而这个时间成本就是Synchronized效率低的原因。所以这种依赖操作系统的Mutex Lock实现的锁才被称之为重量级锁。这也是JDK1.6以后引入偏向锁和轻量级锁的原因。
1.1 监视器锁
监视器锁被之上是依赖于底层的操作系统的互斥锁来实现的,互斥锁的实现成本较高
用户态和系统态进行状态转换所以慢。
1.2 加锁步骤
四种状态,成本和安全性依次递增,锁的升级拥有成本,不可逆。
- 无锁状态
- 偏向锁:没有竞争,依赖于对象头中的部分信息
- 轻量级锁:竞争较轻,采用忙等(无意义计算,不释放CPU)来等待拥有者释放锁
- 重量级锁:竞争比较厉害(忙等10次后升级),
1.3 被synchronizedu修饰编译后的
同步代码块的同步方法的实现区别
对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步代码块的入口位置和退出位置分别插入monitorenter和monitorexit(2个)字节码指令。一个进入两个退出(正常退出和非正常退出)。
被synchronized修饰的方法则被翻译为普通的方法调用和返回指令,由于VM字节指令集中没有synchronizedu关键字修饰的方法的相关指令,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,标识该方法时同步方法。
问:申请锁的流程
执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象(c++)。
- 先到ObjectMonitor对象中的EntryList队列中
- 然后尝试将修改锁的拥有者标记值owner设置为当前线程(这个修改也是用CAS实现的)
- 真:同时ObjectMonitor对象1的monitor中的计数器count加1,即获取到锁。
- 否:尝试自旋一定次数加锁,还是失败就进入对象的cxq队列阻塞等待。
synchronizedu是可重入的,非公平的锁,由于entryList的线程会先自旋尝试加锁,而不是直接加入cxq队列
执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象(c++)。当一个线程遇到synchronizedu同步程序时,会先到ObjectMonitor对象的EntryList队列中,随后会尝试修改ObjectMonitor对象的owner变量为当前线程,同时对ObjectMonitor对象的monitor中的计数器count加1,即获得对象锁。否则通过尝试自旋一定次数加锁,失败则进入ObjectMonitor对象的cxq队列阻塞等待。
1.4 Monitor对象
管程、监视器
作用:互斥
本质上是一个数据结构,并不是由Java实现的
任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制。可以理解 synchronized 就是 Java 中对管程的实现。管程提供了一种排他访问机制,这种机制也就是互斥。互斥保证了在每个时间点上,最多只有一个线程会执行同步方法。所以理解了 Monitor 对象其实就是使用管程控制同步访问的一种对象。
monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程斥就是通过mutex互斥锁实现的。
1.5 内存模型
分带年龄:搬一次家算一岁。
搬家:Java在进行垃圾回收的时候并不是看那个数据不用了,就把这个数据的存储区域进行赋空操作,这一块空地不确定会很好的适配下一个要存储的对象,所以在回收时会采用搬家式,把这一块区域的数据中不被回收的数据搬家到另一个区域,那些被标记回收的数据并不会被迁移,搬走后原来的存储区就会被规划为下一波的可迁移存储区。而这在一定规模上的一次搬迁就会为对象的分带年龄加一。
2. synchronizedu大总结
2.1 总结分区
1、线程同步的目的是为了保护多个线程访问一个共享资源时对资源的破坏【多线程访问和修改】。
2、线程同步方法是通过锁(监视者Mintor)来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的同步方法(可以访问静态同步方法,静态同步方法的锁是这个类对象A1.class)。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对需要具有原子操作的步骤做出分析,并保证原子操作期间别的线程无法访问竞争资源(加锁处理)。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是,一旦程序发生死锁,程序将死掉。
2.2 问题大区
1、为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁
因为这3个方法都会操作锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让我们获取到锁对象
2、synchronize 底层维护了几个列表存放被阻塞的线程
synchronized 底层对应的 JVM 模型为objectMonitor,使用了3个双向链表来存放被阻塞的线程:
_cxq(Contention queue)
:阻塞线程
_EntryList(EntryList)
:等待抢锁
_WaitSet(WaitSet)
:调用wait() 时,线程会被放入_WaitSet
中线程获取锁失败进入阻塞后,首先会被加入到
_cxq链表
,_cxq链表
的节点会在某个时刻被进一步转移到_EntryList链表。当持有锁的线程释放锁后,_EntryList链表头结点的线程会被唤醒,该线程称为successor(假定继承者),然后该线程会尝试抢占锁。
当我们调用wait() 时,线程会被放入
_WaitSet
,直到调用了notify()/notifyAll()后,线程才被重新放入_cxq
或_EntryList
,默认放入_cxq
链表头部。
3、为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?
因为被唤醒的线程并不是一定能获取到锁,该线程仍然是要去竞争锁的,这个竞争就说明了一切,而只是有机会成为,所以我们称它为假定的。
这也是synchronized为什么是非公平锁的一个原因。
4、为啥不公平
先该标志,后去唤醒自身存储的两个,如果这个时候刚好有一个从运行态过来抢锁,这个抢锁线程会直接进入自旋,但是去唤醒那两个队列中的线程还要时间代价。
5、synchronized 为什么是非公平锁?非公平体现在哪些地方?
synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:
1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
- 先将锁的持有者 owner 属性赋值为 null
- 唤醒等待链表中的一个线程(假定继承者)
在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。
2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的 ,也就是说你先进入链表,不代表你就会先被唤醒。
6、如果有多个线程都进入wait状态,那某个线程调用notify唤醒线程时是否按照进入wait的顺序去唤醒?
答案是否定的。
调用 wait 时,节点进入_WaitSet
链表的尾部。调用 notify 时,根据不同的策略,节点可能被移动到 cxq头部、cxq 尾部、EntryList 头部、EntryList 尾部等多种情况。所以, 唤醒的顺序并不一定是进入 wait 时的顺序。
7、notifyAll 是怎么实现全唤起的?
nofity 是获取 WaitSet 的头结点,执行唤起操作。
nofityAll 的流程,可以简单的理解为就是循环遍历WaitSet的所有节点,对每个节点执行notify 操作。
8、JVM 做了哪些锁优化?
偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化。
9、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
10、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?
偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。如果确定同步代码块会被多个线程访问或者竞争较大,可以通过XX:UseBiasedLocking 参数关闭偏向锁。
11、同步代码块的同步方法的实现区别
对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步代码块的入口位置和退出位置分别插入monitorenter和monitorexit(2个)字节码指令。一个进入两个退出(正常退出和非正常退出)。
被synchronized修饰的方法则被翻译为普通的方法调用和返回指令,由于VM字节指令集中没有synchronizedu关键字修饰的方法的相关指令,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,标识该方法时同步方法。
12、线程拿不到锁会如何
在尝试一定次数自旋加锁失败后,线程会进入cxq队列中阻塞。
13、合适需要同步
多线程的数据共享和数据修改
14、释放锁
修改owner为null,并且唤醒EntryList队列中的头节点。
15、学这一章节为了个啥
想尽办法不要出现死锁,出现了又该如何解决
3. 一些练习题
请按要求编写多线程应用程序,模拟多个人通过一个山洞:
1.这个山洞每次只能通过一个人,每个人通过山洞的时间为2秒;
2.随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过人的信息
public class 山洞 {
private static final Object LOCK = new Object();// 锁
private int num = 0;// 到山洞口需要过山洞的人
private int counter = 0;// 统计穿过山洞的人
private StringBuffer res = new StringBuffer("");// 日志记录
public void 过山洞() {
// 这里不需要进行同步处理,可能就会出现顺序问题
res.append("第" + (++num) + "人" + Thread.currentThread().getName() + "到达山洞口\n");
// 按照要求,只能一次过一个人,一个人过的时间为200ms
synchronized (LOCK) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
res.append("第" + (++counter) + "人" + Thread.currentThread().getName() + "穿过山洞\n");
}
}
public void show() {
System.out.println(res);
}
public static void main(String[] args) {
山洞 shan = new 山洞();
String[] arr = new String[] { "喷非", "轻轻", "小贩", "溜冰", "蒙奇", "白澄湖", "柏嘉芙", "骚味", "家了", "内蒙人" };
Thread[] ts = new Thread[arr.length];
Random r = new Random();
for (int i = 0; i < arr.length; i++) {
String name = null;
while (name == null) {
int pos = r.nextInt(arr.length);
String tmp = arr[pos];
if (tmp != null) {
name = tmp;
arr[pos] = null;
}
}
System.out.println(name);
ts[i] = new Thread(() -> {
shan.过山洞();
}, name);
ts[i].start();
}
for(Thread t:ts) {
if(t!=null)
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
shan.show();
}
}
龟兔赛跑规则:
- 总长100米
- 兔子每 0.1 秒 5 米的速度,每跑20米休息1秒
- 乌龟每 0.1 秒跑 2 米,不休息
- 当有一方到达终点时,另外一方立即终止。
采用模板模式
- 动物类
public abstract class 动物类 extends Thread {
protected int 长度 = 100;// 剩余的总长
protected boolean flag = true;
// 由于乌龟和兔子跑的方式不同,所以这里定义抽象方法,由兔子和乌龟子类具体实现
public abstract void 跑步();
// 这里实际上用于表示赛跑的实现,就是算法骨架
public void run() {
while (长度 > 0 && flag) {
跑步();
}
}
// 在需要回调数据的地方(两个子类需要),声明一个接口
static interface 结束接口 {
void win();
}
// 接口对象,用于当一方完成后回调实现另外一方立即终止
protected 结束接口 结束操作;
public void set结束操作(结束接口 结束操作) {
this.结束操作 = 结束操作;
}
public static void main(String[] args) {
兔子 tuzi = new 兔子();
乌龟 wugui = new 乌龟();
tuzi.set结束操作(new 结束接口实现类(wugui));
wugui.set结束操作(new 结束接口实现类(tuzi));
tuzi.start();
wugui.start();
}
}
- 结束接口实现类
public class 结束接口实现类 implements 动物类.结束接口 {
private 动物类 动物;
public 结束接口实现类(动物类 动物) {
this.动物 = 动物;
}
@Override
public void win() {
动物.flag=false;
}
}
- 兔子
public class 兔子 extends 动物类 {
// 规则:兔子每 0.1 秒 5 米的速度,每跑20米休息1秒
public void 跑步() {
int dis = 5;
长度 = 长度 - dis;
System.out.println("兔子跑了" + dis + "米,距离终点还有" + 长度 + "米");
if (长度 <= 0) {
长度 = 0;
System.out.println("兔子获得了胜利");
// 调用回调对象,让乌龟不要再跑了
if (结束操作 != null)
结束操作.win();
}
try {
// 每20米休息一次,休息时间是1秒
if ((100 - 长度) % 20 == 0) {
sleep(1100);
} else { // 每0.1秒跑5米
sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 乌龟
public class 乌龟 extends 动物类 {
//规则:乌龟每 0.1 秒跑 2 米,不休息
public void 跑步() {
int dis = 2;
长度 = 长度 - dis;
System.out.println("乌龟跑了" + dis + "米,距离终点还有" + 长度 + "米");
if (长度 <= 0) {
长度 = 0;
System.out.println("乌龟获得了胜利");
// 调用回调对象,让兔子 不要再跑了
if (结束操作 != null)
结束操作.win();
}
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4. 设计模式
设计模式:专家门为了在某种状态达到某种目的所提出的某种思想是一种知识,没有具体的实现。目前有23种三大类
- 创建型模式:如何创建对象
- 结构型模式:如何组合
- 行为型模式:对象之间的通信
4.1 生产者消费者模式
- 生产者负责给生产消费产品,缓冲区的
问:题
为啥要用
在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
均衡生产能力和消费能力。
作用
-
支持并发
-
均衡生产能力和消费能力。
-
解耦:生产者和消费者没有什么必然联系
实现
实现的总结
- 当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。不能更改为sleep,因为sleep不会释放锁
- 当执行notify/notifyAll方法时,会唤醒一个处于等待该对象锁的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
临界资源
资源池大小唯一
package com.yan2;
// 临界资源,临界资源的数量一定是小于使用资源的线程数
public class Basket {
// 利用volatile保证可见性
private volatile Object obj = null;
public synchronized void produce(Object obj) {
while (this.obj != null) {
try {
// 当前线程进入minitor的waitSet中
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.obj = obj;
// 唤醒处于waitSet中的所有线程
this.notifyAll();
System.out.println("生产了一个对象:" + obj);
}
public synchronized void consume() {
while (obj == null)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费了一个对象:" + obj);
this.obj = null;
this.notifyAll();
}
}
池子大小可控
package com.yang2;
public class Basket {
private Object[] arr;
public Basket() {
arr = new Object[5];
}
public synchronized void produce(Object obj) {
int pos;
while (true) {
pos = indexOfNull();
if (pos == -1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
arr[pos] = obj;
System.out.println(Thread.currentThread() + "-生产了一个数据-" + arr[pos]);
this.notifyAll();
}
public synchronized void consume() {
int pos;
while (true) {
pos = indexOfNonNull();
if (pos == -1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "-消费了一个数据-" + arr[pos]);
arr[pos] = null;
this.notifyAll();
}
private synchronized int indexOfNull() {
int res = -1;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == null) {
res = i;
break;
}
}
return res;
}
private synchronized int indexOfNonNull() {
int res = -1;
for (int i = arr.length - 1; i >= 0; i--) {
if (arr[i] != null) {
res = i;
break;
}
}
return res;
}
}
消费者
package com.yan2;
import java.util.Date;
public class Consumer implements Runnable {
private Basket resource = null;
public Consumer(Basket resource) {
this.resource = resource;
}
public void run() {
for (int i = 0; i < 20; i++) {
resource.consume();
}
}
}
生产者
package com.yan2;
import java.util.Date;
public class Producer implements Runnable {
private Basket resource = null;
public Producer(Basket resource) {
this.resource = resource;
}
public void run() {
for (int i = 0; i < 20; i++) {
Object obj = new Date();
resource.produce(obj);
}
}
}
测试
package com.yan2;
public class Test {
public static void main(String[] args) {
Basket resource = new Basket();
new Thread(new Producer(resource)).start();
new Thread(new Consumer(resource)).start();
}
}
4.2 单例(线程安全双检测)
- 饿汉(占内存)、单例
- 懒汉(有延迟、线程不安全)
- synchronized同步方法
public class Singleton3 {
private Singleton3() {
}
private static Singleton3 instance;
//必须加锁 synchronized 才能保证单例,但加锁会影响效率。
public static synchronized Singleton3 getInstance() {
if (instance == null)
instance = new Singleton3();
return instance;
}
}
- synchronized双检测同步方法
- 第一层检测是否为空,目的为了判定是否被创建了
- 同步区内部检测是否因为自己在等待锁的时候被别的线程所创建
//双检锁/双重校验锁
public class Singleton4 {
private volatile static Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null)
instance = new Singleton4();
}
}
return instance;
}
}
扩展小芝士
- 同步实力方法等价于synchronizedu(this){}、同步静态方法
- 重入多少次就要释放多少次
- stop立刻关闭线程容易导致数据异常。suspend会挂起线程,可能阻塞,有可能会引发死锁。
- monitorenter指令会获取锁、monitorexit会释放锁