点击 Mr.绵羊的知识星球 解锁更多优质文章。
目录
一、介绍
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的可见性,从而避免了线程安全问题。同时,通过双重检查锁定的方式,将同步的粒度降到方法内部,提高了程序的性能。
参考文章