Java之synchronized(含与ReentrantLock的区别解读)

 更新时间:2025年01月03日 10:08:13   作者:心流时间  
文章主要介绍了`synchronized`和`ReentrantLock`的区别,包括它们的实现原理、公平性、灵活性、可中断性等方面,同时,文章详细解释了`synchronized`的使用方法,包括修饰实例方法、静态方法和代码块的情况,以及如何分析代码是否互斥和可重入性

1. synchronized与ReentrantLock的区别

区别点synchronizedReentrantLock
是什么?关键字,是 JVM 层面通过监视器实现的类,基于 AQS 实现的
公平锁与否?非公平锁支持公平锁和非公平锁,默认非公平锁
获取当前线程是否上锁可以(isHeldByCurrentThread())
条件变量支持条件变量(newCondition())
异常处理在 synchronized 块中发生异常,锁会自动释放在 ReentrantLock 中没有在 finally 块中正确地调用 unlock() 方法,则可能会导致死锁
灵活性1自动加锁和释放锁手动加锁和释放锁
灵活性2允许尝试去获取锁而不阻塞(如 tryLock 方法),并且可以指定获取锁等待的时间(如 tryLock(long time, TimeUnit unit))。
可中断性不可中断,除非发生了异常允许线程中断另一个持有锁的线程,这样持有锁的线程可以选择放弃锁并响应中断。1.tryLock(long timeout, TimeUnit unit);2.lockInterruptibly()和interrupt()配合使用
锁的内容对象,锁信息保存在对象头中int类型的变量来标识锁的状态:private volatile int state;
锁升级过程无锁->偏向锁->轻量级锁->重量级锁
使用位置普通方法、静态方法、代码块代码块(方法里的代码,初始化块都是代码块)

2. synchronized的作用

在Java中,使用synchronized关键字可以确保任何时刻只有一个线程可以执行特定的方法或者代码块。这有助于防止数据竞争条件(race conditions)和其他由于线程间共享资源而产生的问题。

当一个方法或代码块被声明为synchronized,它意味着在该方法或代码块执行期间,其他试图获得相同锁的线程将被阻塞,直到持有锁的线程释放该锁。这个锁通常是对象的一个监视器(monitor),对于静态方法来说是类的Class对象,对于实例方法则是拥有该方法的对象。

synchronized可以限制对共享资源的访问,它锁定的并不是临界资源,而是某个对象,只有线程获取到这个对象的锁才能访问临界区,进而访问临界区中的资源。

保证线程安全

当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出正常的行为(与自己预想的结果一致),那我们就可以说这个类是线程安全的。

造成线程安全问题的主要诱因有两点

  1. 存在共享数据(也称临界资源)
  2. 存在多条线程共同操作共享数据

当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能)。

3. synchronized的使用

下面三种本质上都是锁对象

3.1 修饰实例方法

作用于当前实例,进入同步代码前需要先获取实例的锁

  • 示例代码:
public class SynchronizedDemo2 {
    int num = 0;

    public synchronized void add() {
//    public void add() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
    public static class AddDemo extends Thread {
        private SynchronizedDemo2 synchronizedDemo2;

        public AddDemo(SynchronizedDemo2 synchronizedDemo2) {
            this.synchronizedDemo2 = synchronizedDemo2;
        }
        @Override
        public void run() {
            this.synchronizedDemo2.add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
    	// 要想拿到临界资源,就必须先获得到这个对象的锁。
        SynchronizedDemo2 synchronizedDemo2 = new SynchronizedDemo2();
        
        AddDemo addDemo1 = new AddDemo(synchronizedDemo2);
        AddDemo addDemo2 = new AddDemo(synchronizedDemo2);
        AddDemo addDemo3 = new AddDemo(synchronizedDemo2);

        addDemo1.start();
        addDemo2.start();
        addDemo3.start();

        // 阻塞主线程
        addDemo1.join();
        addDemo2.join();
        addDemo3.join();

        // 打印结果
        System.out.println(synchronizedDemo2.num);

    }
}
  • 打印:

期望结果:30000

无synchronized结果:23885

有synchronized结果:30000

synchronize作用于实例方法需要注意:

  • 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
  • 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法

3.2 修饰静态方法

作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁

锁定静态方法需要通过类.class,或者直接在静态方法上加上关键字。但是,类.class不能使用this来代替。

注:在同一个类加载器中,class是单例的,这也就能保证synchronized能够只让一个线程访问临界资源。

  • 示例代码:
public class SynchronizedDemo1 {
    static int num = 0;

	// 加上synchronized保证线程安全
	public static synchronized void add() {
    // public static void add() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }

    // 同上
    public static void add1() {
        synchronized (SynchronizedDemo1.class) {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }

    public static class AddDemo extends Thread {
        @Override
        public void run() {
            SynchronizedDemo1.add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddDemo addDemo1 = new AddDemo();
        AddDemo addDemo2 = new AddDemo();
        AddDemo addDemo3 = new AddDemo();
        addDemo1.start();
        addDemo2.start();
        addDemo3.start();

        // 阻塞主线程
        addDemo1.join();
        addDemo2.join();
        addDemo3.join();

        // 打印结果
        System.out.println(SynchronizedDemo1.num);

    }
}
  • 打印:

期望结果:30000

无synchronized结果:14207

有synchronized结果:30000

3.3 修饰代码块

需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

若是this,相当于修饰实例方法

  • 示例代码:
public class SynchronizedDemo3 {

    private static Object lockobj = new Object();
    private static int num = 0;

    public static void add() {
        synchronized (lockobj) {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }

    public static class AddDemo extends Thread {
        @Override
        public void run() {
            SynchronizedDemo3.add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddDemo addDemo1 = new AddDemo();
        AddDemo addDemo2 = new AddDemo();
        AddDemo addDemo3 = new AddDemo();
        addDemo1.start();
        addDemo2.start();
        addDemo3.start();

        // 阻塞主线程
        addDemo1.join();
        addDemo2.join();
        addDemo3.join();

        // 打印结果
        System.out.println(SynchronizedDemo3.num);

    }
}
  • 打印:

期望结果:30000

无synchronized结果:28278

有synchronized结果:> 示例代码:

4. 分析代码是否互斥

分析代码是否互斥的方法,先找出synchronized作用的对象是谁,如果多个线程操作的方法中synchronized作用的锁对象一样,那么这些线程同时异步执行这些方法就是互斥的。

  • 示例代码:
public class SynchronizedDemo4 {
    // 作用于当前类的实例对象
    public synchronized void m1() {
    }

    // 作用于当前类的实例对象
    public synchronized void m2() {
    }

    // 作用于当前类的实例对象
    public void m3() {
        synchronized (this) {
        }
    }

    // 作用于当前类Class对象
    public static synchronized void m4() {
    }

    // 作用于当前类Class对象
    public static void m5() {
        synchronized (SynchronizedDemo4.class) {
        }
    }

    public static class T extends Thread {
        SynchronizedDemo4 demo;

        public T(SynchronizedDemo4 demo) {
            this.demo = demo;
        }

        @Override
        public void run() {
            super.run();
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo4 d1 = new SynchronizedDemo4();
        Thread t1 = new Thread(() -> {
            d1.m1();
        });
        Thread t2 = new Thread(() -> {
            d1.m2();
        });
        
        Thread t3 = new Thread(() -> {
            d1.m3();
        });
        
        SynchronizedDemo4 d2 = new SynchronizedDemo4();
        Thread t4 = new Thread(() -> {
            d2.m2();
        });
        
        Thread t5 = new Thread(() -> {
            SynchronizedDemo4.m4();
        });
        
        Thread t6 = new Thread(() -> {
            SynchronizedDemo4.m5();
        });
        
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}

结论:

  • 线程t1、t2、t3中调用的方法都需要获取d1的锁,所以他们是互斥的
  • t1/t2/t3这3个线程和t4不互斥,他们可以同时运行,因为前面三个线程依赖于d1的锁,t4依赖于d2的锁
  • t5、t6都作用于当前类的Class对象锁,所以这两个线程是互斥的,和其他几个线程不互斥

5. synchronized的可重入性

  • 示例代码:
public class SynchronizedDemo5 {
    synchronized void method1() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        method2();
        System.out.println("method1 thread-" + Thread.currentThread().getName() + " end");
    }

    synchronized void method2() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("method2 thread-" + Thread.currentThread().getName() + " end");
    }

    public static void main(String[] args) {
        SynchronizedDemo5 t5 = new SynchronizedDemo5();
        new Thread(t5::method1, "1").start();
        new Thread(t5::method1, "2").start();
        new Thread(t5::method1, "3").start();
    }
}
  • 打印:

method2 thread-1 end

method1 thread-1 end

method2 thread-3 end

method1 thread-3 end

method2 thread-2 end

method1 thread-2 end

  • 结论:

当线程启动的时候,已经获取了对象的锁,等method1调用method2方法的时候,同样是拿到了这个对象的锁。所以synchronized是可重入的。

6. 发生异常synchronized会释放锁

  • 示例代码:
public class SynchronizedDemo6 {
    int num = 0;
    synchronized void add() {
        System.out.println("thread" + Thread.currentThread().getName() + " start");
        while (num <= 7) {
            num++;
            System.out.println("thread" + Thread.currentThread().getName() + ", num is " + num);
            if (num == 3) {
                throw new NullPointerException();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6();
        new Thread(synchronizedDemo6::add, "1").start();
        Thread.sleep(1000);
        new Thread(synchronizedDemo6::add, "2").start();
    }
}

打印:

thread1 start
thread1, num is 1
thread1, num is 2
thread1, num is 3
Exception in thread “1” java.lang.NullPointerException
at com.xin.demo.threaddemo.lockdemo.synchronizeddemo.SynchronizedDemo6.add(SynchronizedDemo6.java:14)
at java.lang.Thread.run(Thread.java:748)
thread2 start
thread2, num is 4
thread2, num is 5
thread2, num is 6
thread2, num is 7
thread2, num is 8

  • 结论:

发生异常synchronized会释放锁

7. synchronized的实现原理与应用(包含锁的升级过程)

我的另一篇读书笔记:Java并发机制的底层实现原理

锁的升级过程:无锁->偏向锁->轻量级锁->重量级锁,详细情况还是看上面这篇文章

  • 无锁
  • 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入
  • 轻量级锁:当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的CPU在用户态和内核态间转换的消耗。轻量级锁时,CPU是用户态。
  • 重量级锁:两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗CPU,轻量级锁会升级成重量级锁。重量级锁时,CPU是内核态。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • mybatis-plus自带保存接口,主键Id不是从1开始问题及解决

    mybatis-plus自带保存接口,主键Id不是从1开始问题及解决

    文章描述了在使用MyBatis-Plus的save方法保存数据时,遇到主键ID过大导致报错的问题,提出的解决方案包括使用@TableId注解让MyBatis-Plus使用数据库的自增模式,使用truncatetable方法重置表并备份数据,以及通过建表语句设置主键ID的初始值
    2025-12-12
  • java接口性能从20s优化到500ms示例详解

    java接口性能从20s优化到500ms示例详解

    这篇文章主要为大家介绍了java接口性能从20s优化到500ms的操作技巧示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • java实现ArrayList根据存储对象排序功能示例

    java实现ArrayList根据存储对象排序功能示例

    这篇文章主要介绍了java实现ArrayList根据存储对象排序功能,结合实例形式分析了java针对ArrayList的相关运算、排序操作技巧,需要的朋友可以参考下
    2018-01-01
  • 使用maven依赖详解

    使用maven依赖详解

    本文主要介绍了Maven的基础知识,包括Maven的简介、仓库类型、常用命令、场景举例、指令总结、依赖范围、settings.xml说明等,同时,还详细讲解了Maven依赖的传递、手动声明optional、冲突解决等概念
    2025-02-02
  • Java操作XML转JSON数据格式详细代码实例

    Java操作XML转JSON数据格式详细代码实例

    在Java中我们可以使用一些现成的库来实现XML到JSON的转换,下面这篇文章主要给大家介绍了关于Java操作XML转JSON数据格式的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-04-04
  • java发送http get请求的两种方式

    java发送http get请求的两种方式

    这篇文章主要为大家详细介绍了java发送http get请求的两种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • spring security登录认证授权的项目实践

    spring security登录认证授权的项目实践

    SpringSecurity主要实现了认证(Authentication)和授权(AccessControl),本文就来介绍一下spring security登录认证授权的项目实践,感兴趣的可以了解一下
    2025-01-01
  • SpringData Redis的具体使用

    SpringData Redis的具体使用

    本文主要介绍了SpringData Redis的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-04-04
  • Java实现的计时器【秒表】功能示例

    Java实现的计时器【秒表】功能示例

    这篇文章主要介绍了Java实现的计时器【秒表】功能,结合实例形式分析了Java结合JFrame框架的计时器功能相关操作技巧,需要的朋友可以参考下
    2019-02-02
  • java递归生成树型结构方式

    java递归生成树型结构方式

    文章介绍了如何使用Java递归生成树形结构,包括获取数据、生成树形结构、查询子节点等步骤,作者分享了自己的经验,希望能对大家有所帮助
    2024-12-12

最新评论