synchronized 关键字用法和原理

151 阅读4分钟

synchronized关键字是同步锁,依赖于对象而存在,每一个对象有且仅有一个同步锁,当我们调用一个被synchronized所修饰的方法或者代码块的时候,就可以获取到当前对象的的锁

synchronized是原理

synchronized是通过对象中monitor(监视锁)来实现的,而monitor底层又依赖于操作系统的互斥锁来实现的,而阻塞或者唤醒一个线程都是需要操作系统来帮忙,这就需要从用户态转到内核态,这个过程相对于来说比较长,这就是为什么synchronized效率低的原因,所以synchronized被称之为”重量级锁”

1、monitor监视锁

当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 如果一个monitor的进数为0,则该线程可以进入,然后monitor的进数加1
  • 如果线程已经占有的monitor,只是重新进入的话,monitor的进数加1
  • 如果monitor被其他线程所占用的,则该线程会进入阻塞状态,直到monitor的进数等于0的情况下,该线程才会重新去竞争monitor的所有权

2、synchronized锁

在java SE1.6中对synchronized进行了优化,它也不在是那么重量级了,不同的场景会采用不同的锁

  • 偏向锁:适用于锁没有竞争的情况下,假如共享变量只有一个线程在访问的时候,就不会存在同步的操作,该线程重新进入或者退出同步代码块,都不需要进行解锁或者加锁的操作,当有其他线程来竞争的时候,偏向锁会膨胀成轻量锁
  • 轻量锁:当多个线程同时竞争一个锁,只有一个线程可以获取到锁,其他线程并不会阻塞,而是进入自旋状态,也就是空循环,直到占有锁的线程解锁以后,其他线程再次竞争锁,线程进入自旋状态下会消耗CPU资源,当自旋的次数超过次数限制时,锁会再次膨胀成为重量级锁
  • 重量级锁:重量级锁又称为互斥锁,依赖于monitor锁来实现,会造成线程阻塞,每一次唤醒以及阻塞线程都需要从用户态切换到内核态,时间较长,但是支持的吞吐量较大

尽管采用了偏向锁和轻量锁的优化方案,但是依旧有着一些问题,比如在锁竞争比较激烈的情况下,中间多了膨胀锁以及撤销锁的步骤,不但没有提高效率,反而大大的降低了效率,此时就需要显示的去禁用偏向锁

3、synchronized锁各阶段优缺点

偏向锁:

  • 优点:线程重新进入和退出线程不需要加锁以及解锁的操作,和执行非同步代码块的速度仅相差纳秒级别
  • 缺点:当线程竞争的情况下,需要撤销原有的偏向锁,使用轻量锁,会造成额外的锁撤销消耗
  • 适用场景:使用只有一个线程去获取锁的情况下

轻量锁:

  • 优点:线程不会阻塞,响应速度快
  • 缺点:线程会进入自旋状态,消耗CPU资源
  • 适用场景:并发量较小,同步块执行速度较快,追求响应速度

重量级锁:

  • 优点:线程不会自旋,不会消耗CPU
  • 缺点:线程会进入阻塞状态,每次唤醒和阻塞线程会从用户态切换到内核态,所以响应速度较慢
  • 适用场景:追求吞吐量

synchronized用法

  • 修饰代码块,作用于当前对象
public void test5() {
    synchronized (this) {
        for (int i = 0; i < 10; i++) {
            count++;
           
            Thread.sleep(1000);
            log.info("{}-{}", Thread.currentThread().getName(), i);
        }
        countDownLatch.countDown();
    }
}

@SneakyThrows
public static void main(String[] args) {
    SynchronizedTest SynchronizedTest = new SynchronizedTest();
    new Thread(() -> SynchronizedTest.test5()).start();
    new Thread(() -> SynchronizedTest.test5()).start();
    countDownLatch.await();
}

// 多线程的情况,从执行结果可以看出只有当线程0执行完毕后,线程1才可以执行, 被synchronized修饰得代码块只作用于当前对象

image.png

  • 修饰方法,作用于当前对象
@SneakyThrows
public synchronized void test6() {
    for (int i = 0; i < 10; i++) {
        count++;

        Thread.sleep(1000);
        log.info("{}-{}", Thread.currentThread().getName(), i);
        countDownLatch.countDown();
    }
}


@SneakyThrows
public static void main(String[] args) {
    SynchronizedTest SynchronizedTest = new SynchronizedTest();
    new Thread(() -> SynchronizedTest.test6()).start();
    new Thread(() -> SynchronizedTest.test6()).start();
    countDownLatch.await();
}

// 多线程的情况,从执行结果可以看出只有当线程0执行完毕后,线程1才可以执行, 被synchronized修饰得方法只作用于当前对象

image.png

  • 修饰静态方法,作用于当前类对象
@SneakyThrows
public synchronized static void test7() {
    for (int i = 0; i < 10; i++) {
        count++;

        Thread.sleep(1000);
        log.info("{}-{}", Thread.currentThread().getName(), i);
        countDownLatch.countDown();
    }
}


@SneakyThrows
public static void main(String[] args) {
    SynchronizedTest SynchronizedTest1 = new SynchronizedTest();
    SynchronizedTest SynchronizedTest2 = new SynchronizedTest();
    new Thread(() -> SynchronizedTest1.test7()).start();
    new Thread(() -> SynchronizedTest2.test7()).start();
    countDownLatch.await();
}

// 多线程的情况,从执行结果可以看出只有当线程0执行完毕后,线程1才可以执行, 通过不同的对象调用同一个静态方法,结果还是按照同步执行的,也就说明了被Synchronized所修饰的静态方法,作用于类对象

image.png

  • 修饰类,作用于类对象

public void test1() {
    synchronized (SynchronizedTest.class) {
        for (int i = 0; i < 10; i++) {

            log.info("当前线程:{} - {}", Thread.currentThread().getName(), i);
        }
        countDownLatch.countDown();
    }
}

@SneakyThrows
public static void main(String[] args) {
    SynchronizedTest SynchronizedTest1 = new SynchronizedTest();
    SynchronizedTest SynchronizedTest2 = new SynchronizedTest();
    new Thread(() -> SynchronizedTest1.test1()).start();
    new Thread(() -> SynchronizedTest2.test1()).start();
    countDownLatch.await();
}

// 多线程的情况,从执行结果可以看出只有当线程0执行完毕后,线程1才可以执行, 通过不同的对象调用还是同步在执行,也就说明了被Synchronized所修饰的类,作用于类对象 image.png