1. 乐观锁和悲观锁
①. 悲观锁
什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源synchronized关键字和Lock的实现类都是悲观锁
②. 乐观锁
概念:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作乐观锁在Java中通过使用无锁编程来实现,最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升。乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
2. 到底锁的什么
locks是接口:java.util.concurrent.locks
synchronize是java关键字
3. 公平锁和非公平锁:获得锁的概率
①. 什么是公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到。
非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象。
1. 公平锁:基于绝对时间获取锁,FIFO
2. 非公平锁:优化线程切换
注意:synchronized 和 ReentrantLock 默认是非公平锁
②. 排队抢票案例(公平出现锁饥饿)
锁饥饿:我们使用5个线程买100张票,使用ReentrantLock默认是非公平锁,获取到的结果可能都是A线程在出售这100张票,会导致B、C、D、E线程发生锁饥饿(使用公平锁会有什么问题)。
③. 源码解读(ReentrantLock默认是非公平锁)
公平锁:排序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获锁。先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以ReentrantLock默认是非公平锁,公平锁要多一个方法,所以非公平锁的性能更好(aqs源码)
④. 为什么会有公平锁、非公平锁的设计?为什么默认非公平?面试题
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销
⑤. 什么时候用公平?什么时候用非公平?面试题
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了
否则那就用公平锁,大家公平使用
4. 可重入锁(又名递归锁)
①. 什么是可重入锁?
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞。如果是1个有synchronized修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
所以Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
可重入锁解决的问题:
1. 线程再次获得锁
2. 锁的最终释放:n次获得锁,对应n次释放锁。锁才能最终释放。
②. 可重入锁这四个字分开解释
③. 代码验证synchronized和ReentrantLock是可重入锁
//synchronized 是可重入锁
class Phone{
public synchronized void sendSms() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendSms");
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendEmail");
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* * 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
* **/
public class ReenterLockDemo {
/**
* t1 sendSms
* t1 sendEmail
* t2 sendSms
* t2 sendEmail
* @param args
*/
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
}
}
//ReentrantLock 是可重入锁
class Phone implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tget");
set();
} finally {
lock.unlock();
}
}
private void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tset");
} finally {
lock.unlock();
}
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* <p>
* 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
**/
public class ReenterLockDemo {
/**
* Thread-0 get
* Thread-0 set
* Thread-1 get
* Thread-1 set
*/
public static void main(String[] args) {
Phone phone = new Phone();
Thread t3 = new Thread(phone);
Thread t4 = new Thread(phone);
t3.start();
t4.start();
}
}
④. 可重入锁的种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁,在同步块、同步方法使用
(在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的)
显示锁(即Lock)也有ReentrantLock这样的可重入锁
(lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程, 锁几次就要释放几次?)
⑤. Synchronized的重入的实现机理(为什么任何一个对象都可以成为一个锁):_owner, _count
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放
5. 死锁
5.1 什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
5.2 产生死锁的原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
5.3 如何排除死锁方式
1. cmd: jps, jstat 进程号
2. jconsole
5.4 java避免死锁的方法
1. 一个线程一个锁,一个资源
2. 使用定时锁代替内部锁机制: lock.tryLock(timeout)
3. 对于数据库的锁,加锁和解锁必须在一个数据库连接里。