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是内核态。

总结

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

相关文章

  • SpringBoot+Jersey跨域文件上传的实现示例

    SpringBoot+Jersey跨域文件上传的实现示例

    在SpringBoot开发后端服务时,我们一般是提供接口给前端使用,本文主要介绍了SpringBoot+Jersey跨域文件上传的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • java 对象参数去空格方式代码实例

    java 对象参数去空格方式代码实例

    这篇文章主要介绍了java 对象参数去空格方式代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • Java实现多线程的n种方法

    Java实现多线程的n种方法

    在现代编程中,多线程是一项关键技术,它使得程序能够同时执行多个任务,提高了系统的效率和性能,在Java中,有多种方法可以实现多线程,本文将详细介绍几种常见的Java多线程实现方法,需要的朋友可以参考下
    2024-11-11
  • Mybatis-plus apply函数使用场景分析

    Mybatis-plus apply函数使用场景分析

    Mybatis-plus 里面的 apply方法 是用于拼接自定义的条件判断,自定义时间查询,根据传进来的开始日期,查询所有该日期是数据,但是数据库中保存是时间,所以需要使用apply查询方式并格式化,这篇文章给大家介绍Mybatis-plus apply函数使用,感兴趣的朋友一起看看吧
    2024-02-02
  • Java8实现FTP及SFTP文件上传下载

    Java8实现FTP及SFTP文件上传下载

    这篇文章主要介绍了Java8实现FTP及SFTP文件上传下载,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • SpringBoot引入模板引擎实现视图解析

    SpringBoot引入模板引擎实现视图解析

    这篇文章主要介绍了SpringBoot引入模板引擎实现视图解析方法流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-10-10
  • Spring 开发过程中Value 注解的使用场景

    Spring 开发过程中Value 注解的使用场景

    这篇文章主要介绍了Spring 开发过程中Value 注解的使用场景,帮助大家更好的理解和使用spring框架,感兴趣的朋友可以了解下
    2020-11-11
  • 前端发送的请求Spring如何返回一个文件详解

    前端发送的请求Spring如何返回一个文件详解

    这篇文章主要给大家介绍了关于前端发送的请求Spring如何返回一个文件的相关资料,文中通过代码介绍的非常详细,对大家的学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-09-09
  • Spring AI TikaDocumentReader详解

    Spring AI TikaDocumentReader详解

    TikaDocumentReader是SpringAI中用于从多种格式文档中提取文本内容的组件,支持PDF、DOC/DOCX、PPT/PPTX和HTML等格式,它在构建知识库、文档处理和数据清洗等任务中非常有用
    2025-01-01
  • Java自动添加重写的toString方法详解

    Java自动添加重写的toString方法详解

    在本篇文章里小编给大家整理了关于Java自动添加重写的toString方法总结,需要的朋友们学习下。
    2019-07-07

最新评论