一文带你了解synchronized全貌

1 什么是synchronized

synchronized是Java中用来保证线程同步的关键字, 可用于修饰普通方法, 静态方法和代码块。当线程想要执行被synchronized修饰的代码时,需要先获得锁,执行完毕后(退出或抛出异常)释放锁。synchronized可以保障代码的原子性、可见性和有序性,广泛应用于并发编程中。
java中的每一个对象都可以作为锁,具体表现为以下三种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchronized括号里配置的对象。

1.1 修饰普通方法

当synchronized修饰普通方法时,锁住的是当前实例对象。如果多个线程是访问同一个实例对象的该方法,就会出现锁争夺,只有一个线程能够获得锁,其他线程需要等待锁的释放才能继续执行。如果不是同一个实例对象,则不会出现锁争夺的情况,各自持有自己实例的锁。因此,在并发编程中,如果多个线程需要访问同一个对象的同步方法,需要考虑锁的粒度以及对象的创建和销毁方式,避免出现死锁等问题。

同一个实例Demo:
public class HasSelfPrivateNum{
    synchronized public void testMethod() {
        try {
            System.out.println(Thread.currentThread().getName() + " begin "
                    + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();

        new Thread(num1::testMethod).start();
        new Thread(num1::testMethod).start();
    }
}

运行结果:

Thread-0 begin 1683466751162
Thread-0 end 1683466754166
Thread-1 begin 1683466754166
Thread-1 end 1683466757173
不同实例Demo:
public class HasSelfPrivateNum{
    synchronized public void testMethod() {
        try {
            System.out.println(Thread.currentThread().getName() + " begin "
                    + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();
        HasSelfPrivateNum num2 = new HasSelfPrivateNum();

        new Thread(num1::testMethod).start();
        new Thread(num2::testMethod).start();
    }

}

运行结果:

Thread-0 begin 1683466692533
Thread-1 begin 1683466692534
Thread-0 end 1683466695540
Thread-1 end 1683466695540

1.2 修饰静态方法

当多个并发线程访问一个对象object中的synchronized静态同步代码块时,每个线程都会尝试获取该类的Class对象锁,只有一个线程能够获得锁,其他线程需要等待锁的释放才能继续执行。因此,在一个时间内只能执行一个线程,另一个线程必须等待当前线程执行完这个同步代码块以后才能执行该代码块。
需要注意的是,synchronized静态同步代码块锁住的是该类的Class对象,而不是实例对象,因此即使多个线程分别使用不同的实例对象,只要它们访问的都是该类的静态同步代码块,也会出现锁争夺的情况。
在使用synchronized静态同步代码块时,需要根据实际情况选择锁住的对象,避免出现锁争夺等问题,确保程序的正确性和效率。

public class HasSelfPrivateNum{
    synchronized static public void testMethod() {
        try {
            System.out.println(Thread.currentThread().getName() + " begin "
                    + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        new Thread(HasSelfPrivateNum::testMethod).start();
        new Thread(HasSelfPrivateNum::testMethod).start();
    }

}

运行结果:

Thread-0 begin 1683468238496
Thread-0 end 1683468241508
Thread-1 begin 1683468241508
Thread-1 end 1683468244523

1.3 修饰同步方法块

当前Class类

如果使用synchronized关键字锁住的是当前Class类,一个时间内只能执行一个线程,其他线程必须等待当前线程执行完这个同步代码块以后才能执行该代码块。这是因为在Java虚拟机中,类对象是全局唯一的,只有一个。因此,使用当前类作为锁,可以保证同一时刻只有一个线程能够访问被synchronized关键字修饰的代码块,从而达到线程安全的目的。

public class HasSelfPrivateNum{
     public void testMethod() {
        try {
            synchronized (HasSelfPrivateNum.class){
                System.out.println(Thread.currentThread().getName() + " begin "
                        + System.currentTimeMillis());
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + " end "
                        + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();
        HasSelfPrivateNum num2 = new HasSelfPrivateNum();
        new Thread(num1::testMethod).start();
        new Thread(num2::testMethod).start();
    }

}

运行结果:

Thread-0 begin 1683468334121
Thread-0 end 1683468337122
Thread-1 begin 1683468337122
Thread-1 end 1683468340136
当前对象

如果使用synchronized关键字锁住的是当前对象,每个对象都有一个对应的监视器锁(也称为内置锁或对象锁),只有获取了该锁的线程才能访问被synchronized关键字修饰的代码块,其他线程需要等待该锁的释放。如果同一时刻有多个线程尝试获取同一个对象的锁,就会出现锁争夺的情况。如果是同一时刻多个线程尝试获取同一个类不同对象的锁, 就不会出现锁争夺的情况。

public class HasSelfPrivateNum{
     public void testMethod() {
        try {
            synchronized (this){
                System.out.println(Thread.currentThread().getName() + " begin "
                        + System.currentTimeMillis());
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + " end "
                        + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();
        HasSelfPrivateNum num2 = new HasSelfPrivateNum();
        new Thread(num1::testMethod).start();
        new Thread(num2::testMethod).start();
    }

}

结果:

Thread-0 begin 1683469088004
Thread-1 begin 1683469088004
Thread-1 end 1683469091018
Thread-0 end 1683469091019

2 Synchronized 原理

Synchronized关键字实现同步的原理是通过对对象的监视器(monitor)进行操作实现的。当多个线程访问同步代码块时,在执行到该代码块时,线程会尝试获取对象的monitor,如果获取成功则执行同步代码块,执行完毕后会释放monitor。如果获取monitor失败则线程进入阻塞状态,直到获取monitor后再执行同步代码块。
在方法上使用Synchronized关键字实现同步,底层实现同样是通过monitor进行操作。当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,则执行线程先持有对象的monitor,然后执行方法,最后在方法完成时释放monitor。
Synchronized代码块和方法都可以实现同步,但是Synchronized代码块可以更细粒度地控制同步范围,避免过度同步。Synchronized代码块使用monitorenter和monitorexit实现同步,方法执行到monitorenter时,计数器+1,方法执行到monitorexit时,计数器-1,计数器为0表示未占有锁,可以获取锁。注意,一个Synchronized块有两个monitorexit,一个是方法正常执行时释放,一个是在执行过程中发生异常时虚拟机自动释放。

public class Test {
    public void testMethod(){
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"start");
        }
    }
}

读者可以自行运行javap -c -v Test.class进行验证

3 Synchronized 锁升级

java SE 1.6为了减少获得锁和释放锁带来的性能消耗, 引入了“偏向锁”和“轻量级锁”,在java SE 1.6中, 锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这个几个状态会随竞争情况逐渐升级。所可以升级但不能降级,意味着偏向锁升级成为轻量级锁后不能降级为偏向锁。

3.1 偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁使用了一种等到竞争出现才释放锁的机制。当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着, 如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。
image.png

3.2 轻量级锁

  1. **轻量级锁加锁 **

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  1. **轻量级锁解锁 **

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图是两个线程同时争夺锁,导致锁膨胀的流程图。
image.png

4 Synchronized与Lock

4.1 Synchronized的缺陷

  • **效率低: **锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断个正在使用锁的线程,相对而言,Lock可以中断和设置超时。
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活。
  • **无法知道是否成功获得锁:**相对而言,Lock可以拿到状态。

4.2 Lock解决相应问题

  • lock(): 加锁
  • unlock(): 解锁
  • tryLock(): 尝试获取锁,返回一个boolean值
  • tryLock(long,TimeUtil): 尝试获取锁,可以设置超时

Synchronized加锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值