点击 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_COUNT; i++) {
                counter.increment();
            }
        });

        // 线程t1将count-1执行1000次
        Thread t2 = new Thread(() -> {
            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详解

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐