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