3.1 从轻松的乐观锁和悲观锁开讲
● 悲观锁: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁
● 乐观锁: 认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命—佛系锁
3.2 通过8种情况演示锁运行案例,看看锁到底是什么
3.2.1 锁相关的8种案例演示code
package com.bilibili.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static synchronized void sendEmail()
{
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----sendEmail");
}
public synchronized void sendSMS()
{
System.out.println("-----sendSMS");
}
public void hello()
{
System.out.println("-------hello");
}
}
/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1 标准访问有ab两个线程,请问先打印邮件还是短信
* 2 sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
* 3 添加一个普通的hello方法,请问先打印邮件还是hello
* 4 有两部手机,请问先打印邮件还是短信
* 5 有两个静态同步方法,有1部手机,请问先打印邮件还是短信
* 6 有两个静态同步方法,有2部手机,请问先打印邮件还是短信
* 7 有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
* 8 有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
*
* 笔记总结:
* 1-2
* * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* * 7-8
* * 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* * *
* * * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * *
* * * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/
public class Lock8Demo
{
public static void main(String[] args)//一切程序的入口
{
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"a").start();
//暂停毫秒,保证a线程先启动
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start();
}
}
/**
*
* ============================================
* 1-2
* * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
*
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* 7-8
* 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* *
* * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* *
* * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
**/
3.2.2 synchronized有三种应用方式
● 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁;
● 作用于代码块,对括号里配置的对象加锁
● 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
3.3 公平锁和非公平锁
3.3.1 何为公平锁/非公平锁
● 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的----- Lock lock = new ReentrantLock(true)—表示公平锁,先来先得。
● 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)---- Lock lock = new ReentrantLock(false)—表示非公平锁,后来的也可能先获得锁,默认为非公平锁。
面试题:
● 为什么会有公平锁/非公平锁的设计?为什么默认非公平?
○ 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
○ 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
● 什么时候用公平?什么时候用非公平?
○ 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
3.3.2 预埋伏AQS
后续深入分析
3.4 可重入锁(递归锁)
3.4.1 概念说明
是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。
3.4.2 可重入锁种类
● 隐式锁(即synchronized关键字使用的锁),默认是可重入锁
○ 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
● 显式锁(即Lock)也有ReentrantLock这样的可重入锁
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object o = new Object();
/**
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
new Thread(() -> {
synchronized (o) {
System.out.println("---------------外层调用");
synchronized (o) {
System.out.println("---------------中层调用");
synchronized (o) {
System.out.println("---------------内层调用");
}
}
}
}, "t1").start();
/**
* 注意:加锁几次就需要解锁几次
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("---------------外层调用");
lock.lock();
try {
System.out.println("---------------中层调用");
lock.lock();
try {
System.out.println("---------------内层调用");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t2").start();
}
}
3.5 死锁及排查
3.5.1 概念
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。
产生原因:
● 系统资源不足
● 进程运行推进顺序不合适
● 系统资源分配不当
3.5.2 写一个死锁代码case
public class DeadLockDemo {
static Object a=new Object();
static Object b=new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a){
System.out.println("t1线程持有a锁,试图获取b锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("t1线程获取到b锁");
}
}
},"t1").start();
new Thread(() -> {
synchronized (b){
System.out.println("t2线程持有a锁,试图获取a锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("t2线程获取到a锁");
}
}
},"t2").start();
}
}
3.5.3 如何排查死锁
● 纯命令
○ jps -l
○ jstack 进程编号
● 图形化
○ jconsole