✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:迷雾散尽后,天光大亮,我看清了远处的灯塔,奔走在漫漫时光中,褪去青涩,我终将成为我故事里的主角。
synchronized 从字面意思上是 “同步” 指的是 “互斥”。
“同步” 和 “异步” 在一起讨论又是不一样的意思
例如去餐馆吃饭
- 同步:老板把饭做好,我在前台等着然后自己打包带走。(调用者自己来负责获取到调用结果)
- 异步:老板把饭做好,我在椅子上做好等着老板端到我面前。(调用者自己不负责获取调用结果,是由被调用者把算好的结果主动推送过来)
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
LOCK 这个指令是存在互斥的,当 t1 线程进入 LOCK 之后,t2 也尝试 LOCK ,t2 的 LOCK 就不会直接成功。t2 执行 LOCK 的时候发现 t1 已经加上锁了,t2 此处无法完成 LOCK 操作,就会阻塞等待(BLOCKED),要阻塞等到 t1 把锁释放(UNLOCK),当 t1 释放锁之后,t2 才有可能获取到锁(从 LOCK 中返回,并且继续往下执行),t2 到底能不能拿到锁得看有多少竞争者,竞争者是指已经进入了 LOCK 指令,进入 BLOCKED 状态的线程,才是竞争者!!!
在加锁的情况下,线程执行的三个指令就被岔开了,岔开之后,就能保证一个线程 sava 之后,另一个线程才 load ,于是此时计算结果就准了
刷新内存:
synchronized 的工作过程:
1. 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {public synchronized void methond() {//...}
}
2. 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {public synchronized static void method() {//...}
}
3. 修饰代码块: 明确指定锁哪个对象
public class SynchronizedDemo {public void method() {synchronized (this) {//...}}
}
() 里的 this 指的是,是针对哪个对象进行加锁!加锁操作是针对一个对象来进行的,相当于是针对 this 来进行加锁
举例上厕所,滑稽老铁是线程,厕所门锁是要锁的对象
由上可知多个线程去调用 method 方法的时候,其实就是在针对这个 this 指向的对象来加锁,此时如果一个线程获取到锁了,另外的线程就要阻塞等待,但是如果多个线程尝试对不同的对象加锁,则相互之间不会出现互斥的情况。
在 Java 中,任何一个对象,都可以作为锁的对象(都可以放在 synchronized 的括号中),在其他语言,如C++,Python…都是专门搞了一类特殊对象来用作加锁对象,大部分正常对象不能用来加锁。在 Java 中每个对象,内存空间中有一个特殊的区域,对象头(JVM 自带的,对象的一些特殊的信息)。一个对象分为对象头和对象的属性,对象头里是 JVM 自动添加的信息(其中就有和加锁相关的标记信息),对象的属性里是咱们自己写的代码的信息。
synchronized static void func(){//...
}
JVM 加载类的时候就会读取 .class 文件,构造类对象在内存中,类名.class 的方式就能拿到这个类的类对象
无论是使用哪一种用法,使用 synchronized 的时候都是要明确锁对象! (明确是对哪个对象进行加锁)
,只有当两个线程针对同一个对象进行加锁的时候,才会发生竞争,如果是两个线程针对不同的线程进行加锁则没有竞争,因为想加的锁被别人获取到了,而产生的阻塞等待。
public synchronized static void func1(){//...
}public static void func2(){synchronized (Counter.this){//...}
}
这两者写法视为是等价的!!!类对象是整个程序唯一的!这样加锁,但凡调用到 func1 和 func2 ,之间都会产生竞争
public synchronized void func3(){//...
}public void func4(){synchronized (this){//...}
}
这两者写法视为是等价的!!!此处加锁是针对 this,而 this 可以有多个!!
如:
Counter count1 = new Counter();
Counter count2 = new Counter();
count1.func4(); 和 count2.func4(); 不会产生竞争,这两个 func4 里面的 this 是不同的,一个是 count1,一个是 count2。
public class Demo15 {public static Object locker1 = new Object();public static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{synchronized (locker1){System.out.println("t1 start");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 finish");}});t1.start();Thread t2 = new Thread(()->{synchronized (locker2){System.out.println("t2 start");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2 finish");}});t2.start();}
}
t1 在 finish 之前,t2 就能 start 说明两个 synchronized 之间没有产生竞争
加上同一把锁之后,就可以看到 t1 执行完 finish 之后释放了锁,然后 t2 才进行 start,发生了锁竞争。
注意:
- 即使先写了 t1.start ,后写了 t2.start ,不一定是 t1 先执行,t2 后执行!!start 操作是在系统内核里创建出线程(构造 PCB,加入链表里),具体这个线程的入口方法开始执行,还是要看系统的调度!!(t1 t2 执行顺序不确定)
针对类对象加锁和当前对象加锁是基本没有区别的,有区别就应该是形式上不一样,因为加了 static 之后,形式就成为了 synchronized (类名.class),还是一样的结果。实例上也有区别,类对象只有唯一一个实例,只要是使用了它就会有互斥关系,普通的对象加锁,则是有多个实例。
上面不推荐使用是因为把所有的关键方法都无脑加了 synchronized ,加锁的代价会牺牲很大的运行速度!!!加锁之后就容易产生阻塞等待,如 t1 加锁成功,t2 尝试加锁,就会进入阻塞等待的状态 (BLOCKED 状态) (t1 t2 并发不起来)。当 t1 解锁之后,t2 不一定能立即获取到锁,还得看操作系统具体的调度,一旦涉及到调度,要隔多久才会实现调度是不确定的!!
因为加锁涉及到了一些线程的阻塞等待和线程的调度,可以视为一旦使用了锁,就和 "高性能" 告别了。
ConcurrentHashMap 内部做了一系列的优化手段,来提高效率,所以推荐使用