JUC并发编程第四篇,Java中的各种锁之乐观锁和悲观锁、公平锁和非公平锁、可重入锁以及死锁基础
- 一、乐观锁和悲观锁
- 二、公平锁和非公平锁
- 三、可重入锁(递归锁)
- 四、死锁
一、乐观锁和悲观锁
乐观锁:
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁:
适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- synchronized关键字和Lock的实现类都是悲观锁。
二、公平锁和非公平锁
举例,从日常生活中的卖票场景理解公平锁和非公平锁
class Ticket {
private int number = 50;
//默认用的是非公平锁(false),改为true就是公平锁
private Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
}
}finally {
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start();
}
}
- 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
从上边的场景看公平锁和非公平锁存在的问题
- 非公平锁:可以减少CPU唤醒线程的开销,整体的吞吐效率会提高,但是可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
- 公平锁:所有的线程都能得到资源,不会饿死在队列中,但吞吐量会下降很多。
默认非公平锁和公平锁存在的意义?
-
非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
-
当采用非公平锁时,1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程,在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
-
公平锁保证了排队的公平性, 避免了“锁饥饿”问题。
如何选择非公平锁和公平锁
- 如果为了更高的吞吐量,非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
非公平锁和公平锁源码上的差别
- 差别就是公平锁会判断当前的线程是不是位于同步队列的首位,是就返回true,否就返回false。
三、可重入锁(递归锁)
- 同一个线程在外层方法获取锁,再进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。
- 简单的来说就是:在一个 synchronized 修饰的方法或代码块的内部调用本类的其他 synchronized 修饰的方法或代码块时,是永远可以得到锁的。
- Java中 ReentrantLock 和 synchronized 都是可重入锁
Synchronized 的重入实现原理
- 每个锁对象拥有一个 锁计数器 和 一个指向持有该锁的线程的指针。
- 当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
- 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
四、死锁
- 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉它们都将无法进行下去。
产生死锁主要原因
- 竞争资源
- 进程运行推进的顺序不合适
- 资源分配不当
手写一个死锁
final Object objectLockA = new Object();
final Object objectLockB = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
}
}
},"B").start();
- 死锁排查:使用 jconsole 图形化界面检测死锁