2024/8/14

1、synchronized

它的作用是什么?

先讲一下互斥性

在多线程编程中,有时候需要保证多个线程对共享资源的访问是互斥的。

也就是同一时刻只能有一个线程访问共享资源,synchronized可以保证这一点。

synchronized能够保证同一时刻只能有一个线程进入同步代码块或方法。

锁定静态方法,锁定的是类对象。当多个线程同时调用同一个类的不同实例的同一个静态方法,会阻塞。同一个类的相同实例的不同静态方法也会阻塞,因为锁定的是类

锁定非静态方法,锁定的是这个对象本身。当多个线程同时调用这个对象的不同静态方法时,会被阻塞

锁定代码块,当多个线程同时调用一个对象的同一个代码块会被阻塞。

synchronized修饰的方法在抛出异常时,会释放锁吗?

当使用synchronized修饰的方法在执行过程中出现异常时,JVM会自动释放该线程所持有的锁,以确保不会出现死锁的情况。

synchronized 是公平锁还是非公平锁

非公平,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,不过这种抢占的方式可以预防饥饿

新来的线程有机会在较短时间内获得锁,而等待时间较长的线程可能需要再次等待。

synchronized底层源码如何实现?

1、synchronized修饰代码块,是通过monitorEnter和monitorExit指令实现的。

在代码块上使用Synchronized关键字实现锁定时,Java编译器会在编译时将synchronized关键字转化成monitorEnter和monitorExit指令。实现对代码块加锁和解锁操作。其中,monitorEnter指令会获取对象的锁,并将计数器加1,而monitorExit指令会释放对象的锁,并将锁计数器减1

2、synchronized 修饰方法,是在方法访问标志(flags)中添加 ACC_SYNCHRONIZED 标志,表示该方法是同步方法
在静态方法和实例方法上使用synchronized关键字实现锁定时,Java虚拟机会在方法的访问标志(flags)中添加ACC_SYNCHRONIZED标志,表示该方法是同步方法。当线程调用该方法时,Java虚拟机会自动获取该方法所属对象的锁,并在方法执行结束时释放该锁,实现对该方法的加锁和解锁操作。

synchronized 本质上是通过什么保证线程安全的?

原子性:通过 monitorEnter、monitorExit、ACC_SYNCHRONIZED 保证同一时刻只有一个线程进入同步代码块或同步方法。

可见性
当线程进入同步代码块时,它会首先获取该对象的锁,然后清空工作内存中该对象的值,并从主内存中重新读取该对象的值到工作内存中。当线程执行完同步代码块并释放锁时,会将工作内存中该对象的值刷新到主内存中,从而使得其他线程可以看到该对象最新的值。

有序性
Acquire 屏障和 Release 屏障的作用,同步块内部的指令必须按照程序的原有顺序执行,不能进行重排。

synchronized的锁升级了解吗?

  • 第一点:因为 synchronized 的原子性是通过操作系统的Mutex Lock互斥量实现的,所以每次申请互斥量都需要从用户态转换到内核态。
  • 第二点:JVM 线程是和内核线程 1:1 实现的,所以对线程的阻塞和唤醒也是需要从用户态转换到内核态。

在jdk1.6之后,它有着无锁到偏向锁到轻量级锁到重量级锁的转换。

性能和安全性的平衡

synchronized 锁升级过程可以分为以下几个阶段:

偏向锁阶段(Biased Locking):在没有锁竞争的情况下,JVM 会偏向第一个持有锁的线程一直持有锁,避免以后加锁解锁的开销。

轻量级锁阶段(Lightweight Locking):是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

重量级锁阶段:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。重量级锁是指,JVM 会将对象的 Mark Word 中的锁标志位设置为“指向互斥量的指针”,此时,线程在访问对象时需要进行阻塞等待。当其他线程释放锁时,JVM 会唤醒等待的线程,然后再次尝试获取锁。

需要注意的是,锁升级过程是不可逆的,即锁从偏向锁升级到轻量级锁后,就不能再回到偏向锁的状态。同样的,锁从轻量级锁升级到重量级锁后,也不能再回到轻量级锁的状态。因此,在使用 synchronized 时需要根据实际情况进行选择,避免不必要的锁升级操作,提高程序的执行效率和性能。

2、reentrantLock

AQS

ReentrantLock 对线程的阻塞和唤醒是通过 AQS 来实现的。

AQS 里面维护了一个双向链表来阻塞和唤醒线程,一个 volatile 修饰的 state 变量维护锁的状态

public abstract class AbstractQueuedSynchronizer {
    // 头节点,懒加载
    private transient volatile Node head;
    // 尾节点,懒加载
    private transient volatile Node tail;
    // 锁状态和锁重入的数量,0 代表无线程占用
    private volatile int state;        
}

非公平锁加锁

// lock()方法源码
final void lock() {
    // 先通过CAS的操作将state值改为1,期待值为0;
    if (compareAndSetState(0, 1))
        // 如果CAS成功,将独占线程设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 失败代表有竞争
        acquire(1);
}

非公平锁的加锁很粗暴,上来就尝试获取锁。如果获取锁成了,就把独占线程设置为当前线程

独占线程的作用:

  • 实现可重入锁
  • 防止释放锁的线程不是拥有锁的线程的情况出现

synchronized 关键字是JVM底层支持的解锁的时候会将同步代码块中的数据刷回主内存,那在上述代码中,还需要对 i 加上volatile关键字才能保证可见性最终保证线程安全?

其实是不需要的,AQS 中的 state 是 volatile 修饰的,加锁和解锁的过程都会操作 state,而之前在《深入浅出volatile关键字》中我们了解到,对 volatile 修饰的关键字进行操作时会将本地缓存中的数据都刷新回主内存,所有中间操作的数据也都会刷新回主内存,可见性自然也可以保证

ThreadLocal

ThreadLocal有两个典型的使用场景:

  • 线程封闭:ThreadLocal会为每个线程创建自己独有的变量副本,每个线程对变量的修改都是对自己的副本的修改,而不会影响其他线程的副本,做到了资源隔离。
  • 上下文传递:ThreadLocal保存的信息,可以被同一个线程的不同方法访问到。可以使用ThreadLocal进行上下文传递,减少方法之间的传参,类似于全局变量的作用。

如何使用呢?

主要是四个API。构造方法、set、get、remove

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值