点击Mr.绵羊的知识星球解锁更多优质文章。

目录

一、介绍

二、特性

1. 线程安全

2. 互斥访问

3. 可重入性

4. 内置锁

三、实现原理

四、和其他锁比较

1. 优点

2. 缺点

五、注意事项和最佳实践

六、使用案例

1. 案例一

2. 案例二


一、介绍

synchronized是Java中最基本的同步机制之一,它通过在代码块或方法上添加synchronized关键字来实现线程的同步和互斥。使用synchronized可以确保多个线程在访问共享资源时不会发生冲突。

二、特性

1. 线程安全

使用synchronized可以确保多个线程在访问共享资源时不会发生冲突。

2. 互斥访问

同一时刻只能有一个线程访问共享资源。

3. 可重入性

同一个线程可以多次获得同一把锁,避免死锁。

4. 内置锁

每个Java对象都有一个内置锁,而synchronized就是使用对象的内置锁。

三、实现原理

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

synchronized是基于Java对象头中的标志位实现的。在Java对象头中,有两个标志位用于存储synchronized锁的信息:一个是表示当前对象是否被锁定的标志位,另一个是表示持有锁的线程的标识符。

当一个线程尝试获得一个被synchronized锁保护的资源时,JVM会首先检查该对象的锁标志位。如果锁标志位为0,表示该对象没有被锁定,JVM会将锁标志位设置为1,并将持有锁的线程标识符设置为当前线程的标识符。如果锁标志位为1,表示该对象已经被其他线程锁定,当前线程会进入阻塞状态,等待其他线程释放锁。

当一个线程释放一个被synchronized锁保护的资源时,JVM会将锁标志位设置为0,表示该对象已经被释放。同时,JVM会唤醒等待该对象锁的其他线程,使它们可以继续竞争锁。

四、和其他锁比较

synchronized和其他锁相比,具有以下优点和缺点:

1. 优点

(1)简单易用:synchronized是Java内置的锁机制,使用起来非常简单,不需要额外的依赖。

(2)高效:synchronized的实现非常高效,不会消耗过多的系统资源。

2. 缺点

(1)可重入性有限:虽然synchronized支持可重入性,但是同一个线程在持有锁的同时,不能获取该对象的其他锁。

(2)不能协调多个线程:synchronized只能协调两个线程之间的同步,不能协调多个线程之间的同步。如果需要协调多个线程之间的同步,需要使用其他的同步机制,如Lock、Semaphore、CountDownLatch等。

(3)灵活性差:synchronized只支持两种锁的获取方式:对象锁和类锁。如果需要更灵活的锁获取方式,需要使用其他的同步机制。

与其他锁相比,synchronized是一种简单、高效的同步机制,适用于大多数的并发场景。但是在一些特殊的场景下,需要使用其他的同步机制来协调多个线程之间的同步。

五、注意事项和最佳实践

1. synchronized是一种非常重要的同步机制,但是不要滥用,因为过多的同步会导致程序的性能下降,甚至引发死锁等问题。

2. 在使用synchronized时,尽量避免对锁对象进行修改,因为这样会破坏锁的语义,从而导致不可预料的问题。

3. 要考虑锁的粒度问题。如果锁的粒度过粗,会导致线程的竞争过于激烈,从而降低程序的并发性能;如果锁的粒度过细,会导致锁的竞争过多,从而增加线程的上下文切换开销。因此,需要根据实际情况合理地确定锁的粒度。

4. 尽量使用局部变量代替成员变量,因为局部变量的访问速度比成员变量快,从而可以提高程序的并发性能。

5. 在使用synchronized时,要注意锁的可见性问题。如果一个变量被多个线程共享,并且其中一个线程修改了这个变量的值,那么其他线程可能无法看到这个修改,从而导致不一致的结果。因此,在使用synchronized时,要确保被同步的变量对所有线程都可见。

六、使用案例

1. 案例一

(1) 场景

实现一个计数器,在increment()和decrement()方法上使用了synchronized关键字,保证了成员变量count线程安全。

(2) 代码

/** * Counter 计数器 * * @author wxy * @date 2023-04-05 */public class Counter {private int count = 0;/** * 将count+1 */public synchronized void increment() {count++;}/** * 将count-1 */public synchronized void decrement() {count--;}public synchronized int getCount() {return count;}}class CounterTest {private static final int FOR_COUNT = 100;public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();// 线程t1将count+1执行1000次Thread t1 = new Thread(() -> {for (int i = 0; i  {for (int i = 0; i < FOR_COUNT; i++) {counter.decrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());}}

在上面的示例中,Counter类表示一个计数器,提供了increment()、decrement()、getCount()方法,用于对计数器进行增加、减少和获取操作。这三个方法都使用了synchronized关键字,确保在多线程环境下对计数器进行同步和互斥访问。

2. 案例二

(1) 场景

使用synchronized的案例是实现线程安全的单例模式。

单例模式是一种常用的设计模式,(关于单例设计模式,看这篇文章),它确保一个类只有一个实例,并且提供了全局的访问点。然而,在多线程环境下,如果不采取措施,可能会创建多个实例,从而违背了单例模式的原则。因此,需要使用synchronized来确保线程安全。

(2) 代码

/** * 单例设计模式 * 采用加锁实现, 每次调用方法都加锁效率低(不推荐) * * @author wxy * @date 2023-04-05 */public class SingletonCase1 {private static SingletonCase1 instance;private SingletonCase1() {// 私有构造方法}public static synchronized SingletonCase1 getInstance() {if (instance == null) {instance = new SingletonCase1();}return instance;}}

在上面的示例中,getInstance()方法使用了synchronized关键字,这种方式可以确保线程安全,但是肯定会导致程序的性能下降,因为每次调用getInstance()方法时都会进行同步。为了解决这个问题,可以使用双重检查锁定(double-checked locking)的方式,将同步的粒度降到方法内部。

下面是一个使用双重检查锁定实现线程安全的单例模式的示例:

/** * 单例设计模式 * 采用双重检锁实现(推荐) * * @author wxy * @date 2023-04-05 */public class SingletonCase2 {/** * volatile修饰: 保证该变量具有可见性、禁止指令重排 */private static volatile SingletonCase2 instance;private SingletonCase2() {// 私有构造方法}public static SingletonCase2 getInstance() {if (instance == null) {synchronized (SingletonCase2.class) {if (instance == null) {instance = new SingletonCase2();}}}return instance;}}

在上面的示例中,getInstance()方法首先检查instance是否为null,如果为null,才会进入同步块。在同步块内部,再次检查instance是否为null,如果为null,才会创建一个Singleton对象。由于加入了volatile关键字,确保instance的可见性,从而避免了线程安全问题。同时,通过双重检查锁定的方式,将同步的粒度降到方法内部,提高了程序的性能。

参考文章

1. synchronized详解