synchronized 关键字 - 监视器锁monitor lock

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:迷雾散尽后,天光大亮,我看清了远处的灯塔,奔走在漫漫时光中,褪去青涩,我终将成为我故事里的主角。

 

🍓一. 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 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

 

🍑二. 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();
    }
}
  1. 两者使用不同的锁得出的结果

在这里插入图片描述
t1 在 finish 之前,t2 就能 start 说明两个 synchronized 之间没有产生竞争

  1. 两者使用同一把锁,只需要 synchronized 后面括号里的 this 改为一样即可,同为 locker1 或者 同为 locker2。

在这里插入图片描述
加上同一把锁之后,就可以看到 t1 执行完 finish 之后释放了锁,然后 t2 才进行 start,发生了锁竞争。

注意:

  • 即使先写了 t1.start ,后写了 t2.start ,不一定是 t1 先执行,t2 后执行!!start 操作是在系统内核里创建出线程(构造 PCB,加入链表里),具体这个线程的入口方法开始执行,还是要看系统的调度!!(t1 t2 执行顺序不确定)
  • 针对类对象加锁和当前对象加锁是基本没有区别的,有区别就应该是形式上不一样,因为加了 static 之后,形式就成为了 synchronized (类名.class),还是一样的结果。实例上也有区别,类对象只有唯一一个实例,只要是使用了它就会有互斥关系,普通的对象加锁,则是有多个实例。

 

🍇三. Java 标准库中的线程安全类

  1. Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder
  1. 但是还有一些是线程安全的. 使用了一些锁机制来控制.
  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap(推荐使用)
  • StringBuffer

上面不推荐使用是因为把所有的关键方法都无脑加了 synchronized ,加锁的代价会牺牲很大的运行速度!!!加锁之后就容易产生阻塞等待,如 t1 加锁成功,t2 尝试加锁,就会进入阻塞等待的状态 (BLOCKED 状态) (t1 t2 并发不起来)。当 t1 解锁之后,t2 不一定能立即获取到锁,还得看操作系统具体的调度,一旦涉及到调度,要隔多久才会实现调度是不确定的!!

因为加锁涉及到了一些线程的阻塞等待和线程的调度,可以视为一旦使用了锁,就和 "高性能" 告别了。

ConcurrentHashMap 内部做了一系列的优化手段,来提高效率,所以推荐使用

  1. 还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
  • String
  • 35
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Redamancy丶早晚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值