线程获取锁的过程:
成功获取锁的线程,它会从等待队列中出列,并得到共享资源;
没有获取到锁的线程,继续在等待队列中,阻塞在lock方法;
线程释放锁的过程:
通过 在finally语句中的unlock方法,将锁释放,然后等待队列中的线程去竞争这个锁;
Java 中锁的类型,参考:https://www.cnblogs.com/button123/p/14759956.html
- 公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
- 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于 Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是 Reentrant Lock重新进入锁。
不可重入锁如何死锁的:
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁(但避免下面这种情况)。
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
- 独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
- 互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock和 Synchronized
读写锁在Java中的具体实现就是ReadWriteLock,其读锁是共享锁,其写锁是独享锁;
- 乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
CAS:
当多个线程使用CAS获取锁,只能有一个成功,其他线程返回失败,继续尝试获取锁;
CAS操作中包含三个参数:V(需读写的内存位置)+A(准备用来比较的参数)+B(准备写入的新值):若A的参数与V的对应的值相匹配,就写入值B;若不匹配,就写入这个不匹配的值而非B;
AtomicBoolean:常用于布尔值在多线程下放在if中进行判断,因为多线程下的boolean是非线程安全的,可用AtomicBoolean的compareAndSet方法进行设定;
这个方法作用是:比较AtomicBoolean和expect的值,如果一致则把AtomicBoolean的值设成update,if中再接着往下面的逻辑走;如果不是的话就不更新值;
内部原理就是用到了CAS,性能优于Synchronized的实现方法;
- 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
- 偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
- 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
Synchronized底层实现:
1.在编译的时候,会生成对应的monitorenter/monitorexit指令同步块的进入和退出;
2.JDK6之前,完全是依靠操作系统内部的互斥锁实现的。因此JVM 对此进行了大刀阔斧地改进,也就是采用常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁大大改进了其性能。
对象头:
因为java中任意对象都可以用作锁,储存对象和线程及锁之间的映射关系,所以将其存放在对象头中;
偏向锁:JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁;
轻量级锁:
- 如果有另外的线程试图锁定某个用了偏向锁的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
锁的变换:
锁升级:
偏斜锁=》轻量级锁=》重量级锁(互斥锁)
锁降级:
JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级
ReentrantLock与Synchronized的区别:
- ReentrantLock与Synchronized的区别:
- ReentrantLock
- 更加的灵活,但必须手动释放锁
- 可选择是否为公平锁
- 只适合代码块的锁
- 依赖于JDK实现的(java类)
- 可被中断,并抛出中断异常,释放锁
- 可选择获取锁的超时时间,尝试获取锁
- synchronized
- 无需释放锁,自动处理
- 可修饰方法,类,代码块
- 非公平锁,如果阻塞则必须等待cpu调度
- 依赖于JVM实现的(关键字)
- ReentrantLock
- ReentrantLock与Synchronized的共通点:
- 都是互斥锁
Wait、Notify、NotifyAll和Sleep之间的关系:
首先了解线程调度器和时间分片:
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间,线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行(因为抢占式线程调度:当高优先级线程准备运行时,而低优先级线程正在运行,虚拟机会或早或晚暂停低优先级线程,让高优先级的线程运行)。线程调度并不受到Java虚拟机控制,而是通过抢占式线程调度策略进行线程控制。
时间分片举例: 假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行。通过时间片轮询机制可以保证同等优先级任务能够轮流的使用CPU资源。
1.Notify和NotifyAll区别:
notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到,这跟非公平锁的弊病是一个意思;
2.sleep和wait方法的区别:
- wait会释放所持有锁;而sleep不会释放锁资源,直接让线程进入阻塞状态,因为其他线程拿不到锁资源,如果其他线程包含了Synchronized,那么该线程将不会得到执行
- wait只能在同步方法和同步块中使用,而sleep任何地方都可以
- wait无需捕捉异常,而sleep需要
- sleep是Thread的方法,而wait是Object类的方法
线程沉睡和唤醒用wait和notify方法。(都在synchronize中使用,而synchronize一般都是在线程中的某个方法用的,因为为了同步线程,所以是一个Thread对象创建出多个threadA,B...引用 来调用这个线程方法)
注意线程的上述方法和Object自带的wait()和notifyAll()方法的区别,wait()和notifyAll()一定要在synchronize同步语句中添加,因为调用这两个方法一定要获得对象锁,synchronize块就是为了获取锁,有synchronize块的线程处于运行状态,没有它的线程则处于阻塞状态;
调用wait时,会将对象锁放弃;之后其他线程均分机会来获取这个锁
常见问题: