关于Synchronized

18 篇文章 2 订阅

Synchronized 锁的是什么

在一些业务简单或某些单机系统不要求高性能高效率问题的情况下,可以使用 synchronized 进行线程安全处理,那么 synchronized 它到底锁的是什么

同步方法(非静态)测试示例一

public class SynchronizedTest {

    public synchronized void test() {
        System.out.println("test 方法开始调用...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test 方法结束调用...");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                SynchronizedTest synchronizedTest = new SynchronizedTest();
                synchronizedTest.test();
            }).start();
        }
    }
}

结果:
test 方法开始调用...
test 方法开始调用...
test 方法开始调用...
test 方法结束调用...
test 方法结束调用...
test 方法结束调用...
  • 上面的程序起动了三个线程,同时运行 SynchronizedTest 类中的 test() 方法,虽然 test() 方法加上了 synchronized,但是查看结果,貌似 synchronized 没起作用
  • 很明显,在 for 循环下面,我们创建了 3SynchronizedTest 实例。所以我们怀疑 synchronized 没起作用,与这 3SynchronizedTest 实例有关

同步方法(非静态)测试示例二

public class SynchronizedTest {

    public synchronized void test() {
        System.out.println("test 方法开始调用...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test 方法结束调用...");
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        for (int i = 0; i < 3; i++) {
            new Thread(synchronizedTest::test).start();
        }
    }
}

结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
  • 很明显,这一次在 for 循环下面,我们创建了 1SynchronizedTest 实例,而且 synchronized 起作用了。达到了预期效果
  • 结论:在非静态的方法上,加上 synchronized 时,它锁得是方法所在类的实例

同步方法(静态)测试示例二

public class SynchronizedTest {

    public static synchronized void test() {
        System.out.println("test 方法开始调用...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test 方法结束调用...");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(SynchronizedTest::test).start();
        }
    }
}

结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
  • 在一个静态的方法上,加上 synchronized 时,synchronized 起作用了。达到了预期效果
  • 结论:在静态的方法上,加上 synchronized 时,它锁得是静态方法所在类的 Class 对象

同步代码块测试示例一

public class SynchronizedTest {

    public synchronized void test() {
        synchronized (this) {
            System.out.println("test 方法开始调用...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test 方法结束调用...");
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        for (int i = 0; i < 3; i++) {
            SynchronizedTest synchronizedTest = new SynchronizedTest();
            new Thread(synchronizedTest::test).start();
        }
    }
}

结果:
test 方法开始调用...
test 方法开始调用...
test 方法开始调用...
test 方法结束调用...
test 方法结束调用...
test 方法结束调用...
  • 上面的程序起动了三个线程,同时运行 SynchronizedTest 类中的 test() 方法,虽然 test() 方法加上了 synchronized,但是查看结果,貌似 synchronized 没起作用
  • 很明显,在 for 循环下面,我们创建了 3SynchronizedTest 实例。所以我们怀疑 synchronized 没起作用,与这 3SynchronizedTest 实例有关

同步代码块测试示例二

public class SynchronizedTest {

    public synchronized void test() {
        synchronized (this) {
            System.out.println("test 方法开始调用...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test 方法结束调用...");
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        for (int i = 0; i < 3; i++) {
            new Thread(synchronizedTest::test).start();
        }
    }
}

结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
  • 在使用 synchronized (this) { } 同步代码块起时作用了。达到了预期效果
  • 结论:在使用 synchronized (this) { } 同步代码块时,它锁得是当前所在类的实例

同步代码块测试示例三

public class SynchronizedTest {

    public synchronized void test() {
        synchronized (SynchronizedTest.class) {
            System.out.println("test 方法开始调用...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test 方法结束调用...");
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        for (int i = 0; i < 3; i++) {
            SynchronizedTest synchronizedTest = new SynchronizedTest();
            new Thread(synchronizedTest::test).start();
        }
    }
}
  • 在使用 synchronized (类.class) { } 同步代码块起时作用了。达到了预期效果
  • 虽然在 for 循环下面,我们创建了 3SynchronizedTest 实例,显然没有干扰到预期效果,说明与这 3SynchronizedTest 实例无关
  • 结论:在使用 synchronized (类.class) { } 同步代码块时,它锁得是当前所在类的 Class 对象

Synchronized 锁总结

同步方法

  • 在非静态的方法上,加上 synchronized 时,它锁得是方法所在类的实例
  • 在静态的方法上,加上 synchronized 时,它锁得是静态方法所在类的 Class 对象

同步代码块

  • 在使用 synchronized (this) { } 同步代码块时,它锁得是当前所在类的实例
  • 在使用 synchronized (类.class) { } 同步代码块时,它锁得是当前所在类的 Class 对象

Synchronized 原理

JVM 中的同步是基于进入和退出 Monitor 监视器对象实现的。每个对象实例都会有一个 MonitorMonitor 可以和对象一起创建、销毁

  • 当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合(阻塞队列)中,处于 BLOCKED 阻塞状态的线程,都会被加入到该队列中
  • 接下来当线程获取到对象的 Monitor 监视器时,Monitor 监视器是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex
  • 如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入WaitSet 集合(等待队列)中,等待下一次被唤醒。此时线程会处于 WAITING 等待状态或者 TIMEDWAITING 超时等待状态
  • 如果当前线程顺利执行完方法,也将释放 Mutex

总的来说,就是同步锁在这种实现方式中,因 Monitor 监视器是依赖于底层的操作系统实现的,存在用户态与内核态之间的切换(可以理解为上下文切换),所以增加了性能开销

Synchronized 优化升级

为了提升性能,jdk 1.6 引入了偏向锁、轻量级锁、重量级锁,来减少锁竞争带来的线程上下文切换

  • jdk 1.6 中,锁对象一共有 4 种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着竞争情况逐渐升级
  • 锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

偏向锁

经过 HotSpot 的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁

偏向锁升级为轻量级锁

在这里插入图片描述

  • 当线程 A 访问同步代码块并获取锁对象时,会在 Java 对象头和栈帧中记录偏向锁的线程 ID,因为偏向锁不会主动释放锁,因此以后线程 A 再次获取锁的时候,需要比较当前线程的线程 IDJava 对象头中的线程 ID 是否一致
  • 如果一致,说明还是线程 A 获取锁对象,则无需使用 CAS 来加锁、解锁
  • 如果不一致,说明线程 B 要竞争锁对象,而偏向锁不会主动释放,因此还是存储的线程 A 的线程 ID那么需要查看 Java 对象头中记录的线程 A 是否存活
  • 如果没有存活,那么锁对象被重置为无锁状态,线程 B 可以竞争将其设置为偏向锁**
  • 如果存活,那么立刻查找线程 A 的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程 A,撤销偏向锁,升级为轻量级锁

偏向锁的释放

偏向锁的释放采用只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争,然后其他线程使用 CAS 替换掉原来的线程的线程 ID

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要 CPU 从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋着等待锁释放

轻量级锁升级为重量级锁

  • 线程 A 获取轻量级锁时,会先把锁对象的对象头 MarkWord 复制一份到线程 A 的栈帧中创建用于存储锁记录的空间,然后使用 CAS 把锁对象的对象头中的内容替换为线程 A 存储的锁记录的地址
  • 如果在线程 A 复制对象头的同时(在线程 BCAS 之前),线程 B 也准备获取锁,复制了对象头到线程 B 的锁记录空间中,但是在线程 BCAS 的时候,发现线程 A 已经把锁对象的对象头换了,线程 BCAS 失败,那么此时线程 B 就尝试使用自旋锁来等待线程 A 释放锁
  • 但是如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,如果自旋次数到了线程 A 还没有释放锁,或者线程 A 还在执行,线程 B 还在自旋等待,这时又有一个线程 C 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转

为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

在这里插入图片描述

锁粗化

按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗

锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值