✨个人主页:bit me?
✨当前专栏:Java EE初阶?
✨每日一语:迷雾散尽后,天光大亮,我看清了远处的灯塔,奔走在漫漫时光中,褪去青涩,我终将成为我故事里的主角。
目 录
?一. synchronized 的特性?二. synchronized 使用示例?三. Java 标准库中的线程安全类
?一. synchronized 的特性
synchronized 从字面意思上是 “同步” 指的是 “互斥”。
“同步” 和 “异步” 在一起讨论又是不一样的意思
例如去餐馆吃饭
synchronized 会起到互斥效果, 某个线程执行到某个对象的 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 的工作过程:
获得互斥锁从主内存拷贝变量的最新副本到工作的内存执行代码将更改后的共享变量的值刷新到主内存释放互斥锁
?二. 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 自动添加的信息(其中就有和加锁相关的标记信息),对象的属性里是咱们自己写的代码的信息。
锁类对象(整个 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),还是一样的结果。实例上也有区别,类对象只有唯一一个实例,只要是使用了它就会有互斥关系,普通的对象加锁,则是有多个实例。
?三. Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施. ArrayListLinkedListHashMapTreeMapHashSetTreeSetStringBuilder 但是还有一些是线程安全的. 使用了一些锁机制来控制. Vector (不推荐使用)HashTable (不推荐使用)ConcurrentHashMap(推荐使用)StringBuffer上面不推荐使用是因为把所有的关键方法都无脑加了 synchronized ,加锁的代价会牺牲很大的运行速度!!!加锁之后就容易产生阻塞等待,如 t1 加锁成功,t2 尝试加锁,就会进入阻塞等待的状态 (BLOCKED 状态) (t1 t2 并发不起来)。当 t1 解锁之后,t2 不一定能立即获取到锁,还得看操作系统具体的调度,一旦涉及到调度,要隔多久才会实现调度是不确定的!!
因为加锁涉及到了一些线程的阻塞等待和线程的调度,可以视为一旦使用了锁,就和 "高性能" 告别了。
ConcurrentHashMap 内部做了一系列的优化手段,来提高效率,所以推荐使用
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的 String