目录
一、线程死锁
1.1 死锁必要条件
- 互斥:至少有一个资源是不可共享的,即一次只能被一个线程使用
- 占有并等待:一个线程至少持有一个资源,并等待获取其他线程持有的资源
- 非抢占:资源只有被线程显式释放后才能被其他线程获取,不能被强制抢占
- 循环等待:一组线程形成一个循环,每个线程都在等待下一个线程所持有的资源
1.2 避免和预防
破坏死锁的必要条件即可
- 破坏请求与保持条件:一次性申请所有的资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,主动释放手上占有的资源
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放
二、Java中的锁
2.1 公平锁/非公平锁
- 公平锁:多个线程按照申请锁的顺序来获取锁
- 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其不像ReentrantLock是通过AQS来实现线程调度,所以并没有任何方法使其变成公平锁。
2.2 可重入锁
又名递归锁,是指同一个线程在外层方法获取锁时,在进入内层方法会自动获取锁。
ReentrantLock和Synchronized都是可重入锁,可一定程度上避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
2.3 独享锁/共享锁
也是互斥锁/读写锁的一种广义说法
- 独享锁:该锁一次只能被一个线程所持有(Synchronized、ReentrantLock)
- 共享锁:该锁可被多个线程所持有
Lock的另一个实现类ReadWriteLock,其读锁是共享锁,写锁是独享锁。
该锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的。
2.4 乐观锁/悲观锁
并不是指具体的什么类型的锁,而是指看待并发同步的角度。
- 悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。【利用各种锁】
- 乐观锁:认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新。乐观的认为,不加锁的并发操作是没有事情的。【常采用CAS算法】
悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,不加锁会带来大量的性能提升。
2.5 分段锁
分段锁是一种锁的设计,并不是具体的一种锁。对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想。ConcurrentHashMap中的分段锁称为Segment,类似于HashMap的结构,即内部拥有一个数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)
当需要put元素时,并不是对整个hashmap进行加锁,而是先通过hashcode知道他要放在哪个分段中,然后对这个分段进行加锁。所以当多线程put时,只要不是放在同一个分段中,就实现了真正的并行的插入。
2.6 偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized,通过对象监视器在对象头中的字段来表明。
- 偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
- 轻量级锁:指当锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
- 重量级锁:当锁为轻量级锁时,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
三、关键字volatile
被比喻成“轻量级的synchronized”
和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量,无法修饰方法及代码块等。
被volatile修饰的共享变量,具有以下两点特性:
- 保证了不同线程对该变量操作的内存可见性
- 禁止指令重排序
特点:
1、保证可见性
- 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
2、不保证原子性
3、禁止指令重排