JUC并发编程(3):Java多线程锁

222 阅读16分钟

Java多线程锁

1、多线程锁的8种情况(经典8锁问题)

1.1、多线程锁概述

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用 其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法

所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是不同的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象,其他的静态同步方法都必须等待该方法释放锁后才能获取锁

1.2、问题演示

synchronized 演示,使用手机发短信或发邮件,注意两个点:锁的对象以及锁的范围,通过具体的实例进行分析:创建两个线程安全方法和一个普通方法

8锁问题如下:

  • 标准访问有ab两个线程,请问先打印邮件还是短信
  • 停4秒在短信方法内,先打印短信还是邮件
  • 新增一个普通的hello方法,请问先打印邮件还是hello
  • 有两部手机,请问先打印邮件还是短信
  • 两个静态同步方法,同1部手机,请问先打印邮件还是短信
  • 两个静态同步方法,2部手机,请问先打印邮件还是短信
  • 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
  • 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信

1、问题1:标准访问有AB两个线程,请问先打印邮件还是短信

手机有发送短信和发邮件的功能,创建两个线程分别发送短信和邮件,是先打印短信还是邮件呢?

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();//start方法执行的时间是不确定的
        
        //保证先执行ThreadA
        Thread.sleep(100);
        
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public synchronized void sendSMS() throws Exception {
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果

------sendSMS
------sendEmail

2、问题2:停4秒在短信方法内,先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();

        Thread.sleep(100);

        new Thread(()->{
            try {

                phone1.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception { 
        System.out.println("------sendEmail");
    }
}

运行结果

------sendSMS
------sendEmail

总结:

  • 案例1,2锁的都是当前对象this,即Phone1,同一时刻,只能有唯一一个线程去访问这些synchronized方法
  • ThreadA先获取锁访问sendSMS方法,ThreadB必须等到ThreadA释放锁才能访问sendEmail方法,尽管sendSMS方法中途停了4s,但是ThreadA依然没有释放锁
  • 一个对象里面如果有多个synchronized方法,某一个时刻内,只要个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的个线程去访问这些synchronized方法。
  • 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

3、问题3:新增普通的hello方法,是先打短信还是hello

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();

        Thread.sleep(100);

        new Thread(()->{
            try {
                //phone1.sendEmail();
                phone1.getHello();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}

结果

------getHello
------sendSMS

getHello是非同步方法,在线程A阻塞时,线程B会正常执行getHello(),线程A阻塞4秒后执行sendSMS()。加个普通方法后发现和同步锁无关

4、问题4:现在有两部手机,先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {

        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();

        Thread.sleep(100);

        new Thread(()->{
            try {
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果

------sendEmail
------sendSMS

总结:

  • 线程A使用phone1发送短信,线程B使用phone2发送邮件,由于使用的是两个实例对象(非静态方法),所以不是同一个锁
  • 因此线程A拿到锁执行sendSMS方法,线程B会正常拿自己的锁去执行sendEmail方法
  • 线程B先执行sendEmail,线程A阻塞4秒后执行sendSMS,有两个Phone对象,两个线程锁的不是同一个Phone对象,换成两个对象后,不是同一把锁了。

5、问题5:两个静态同步方法,1部手机,先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        
        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();
        
        Thread.sleep(100);
        
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public static  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public static synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果:

------sendSMS
------sendEmail

6、问题6:两个静态同步方法,2部手机,先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();
        Thread.sleep(100);
        new Thread(()->{
            try {
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public static  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public static synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果:

------sendSMS
------sendEmail

总结:

  • 案例5,6都是线程阻塞4秒后依次打印
  • 静态同步方法锁的是类Phone.Class,而不是具体的Phone1当前对象
  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的普通同步方法用的都是同一把锁一实例对象本身,对于静态同步方法,锁的是当前类的Class对象
  • 一旦一个静态同步方法获取锁后,不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象,其他的静态同步方法都必须等待该方法释放锁后才能获取锁

7、问题7:1个静态同步方法、1个普通同步方法、1部手机、先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();

        Thread.sleep(100);

        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public static  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public  synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果:sendEmail先打印,等待4秒打印出sendSMS

------sendEmail
------sendSMS

8、问题8:1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadA").start();
        Thread.sleep(100);
        new Thread(()->{
            try {

                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"ThreadB").start();
    }
}

class Phone {
    public static  synchronized void sendSMS() throws Exception {
        //停留四秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public  synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
}

结果

------sendEmail
------sendSMS

总结:

与案例7一样,两个线程锁的对象不一样,不存在锁竞争

当一个线程试图访问同步代码时,它首先必须得到锁,退出或出异常时必须释放锁。所有的普通同步方法用的都是同一把锁—实例对象本身,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。

所有的静态同步方法用的也是同一把锁一类对象本身,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

1.3、总结

问题1-2

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

问题3-4

  • 加个普通方法后发现和同步锁无关
  • 换成两个对象后,不是同一把锁了,各自拿自己的锁执行方法,相互不影响,影响的先后顺序只是每个方法的执行时间,情况立刻变化。

问题5-6

都换成静态同步方法后,情况又变化,三种 synchronized 锁的内容有一些差别:

  • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身。
  • 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
  • 对于同步方法块,锁的是 synchronized 括号内的对象

问题7-8

当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。

所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。

所有的静态同步方法用的也是同一把锁——**类对象本身,**就是我们说过的唯一模板Class,具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

**synchronized实现同步的基础:Java中的每一个对象都可以作为锁。**具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象

2、公平锁和非公平锁

2.1、概述

什么是公平锁和非公平锁

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到,因此效率相对低
  • 非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象,即效率高,但是线程容易饿死
  • 注意:synchronized 和 ReentrantLock 默认是非公平锁

2.2、排队抢票案例

//第一步  创建资源类,定义属性和和操作方法
class LTicket {
    //票数量
    private int number = 30;

    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    //卖票方法
    public void sale() {
        //上锁
        lock.lock();
        try {
            //判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

public class LSaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    //创建三个线程
    public static void main(String[] args) {

        LTicket ticket = new LTicket();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"CC").start();
    }
}

输出结果

AA :卖出30 剩余:29
AA :卖出29 剩余:28
AA :卖出28 剩余:27
AA :卖出27 剩余:26
AA :卖出26 剩余:25
AA :卖出25 剩余:24
AA :卖出24 剩余:23
AA :卖出23 剩余:22
AA :卖出22 剩余:21
AA :卖出21 剩余:20
AA :卖出20 剩余:19
AA :卖出19 剩余:18
AA :卖出18 剩余:17
.........

我们使用3个线程买100张票,使用ReentrantLock默认是非公平锁,获取到的结果可能都是A线程在出售这100张票,会导致B、C线程发生锁饥饿,即都是A线程执行,而BC线程都没执行到(或者某个线程出现的概率很大,其余线程出现的概率很小),出现了非公平锁

2.3、ReentrantLock源码解读

通过查看源码,带有参数的ReentrantLock(true)为公平锁,ReentrantLock(false)为非公平锁,而底层主要是调用NonfairSync()FairSync()

ReentrantLock构造器源码

public ReentrantLock() {
    sync = new NonfairSync();
}


public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

具体其非公平锁与公平锁new FairSync() : new NonfairSync();的源码

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final boolean initialTryLock() {
        Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (getExclusiveOwnerThread() == current) {
            if (++c < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(c);
            return true;
        }
        return false;
    }

总结

  • 公平锁:排序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获锁
  • 先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以
  • ReentrantLock默认是非公平锁,公平锁要多一个方法,所以非公平锁的性能更好(aqs源码)

2.4、卖票问题解决

修改代码为private final ReentrantLock lock = new ReentrantLock(true); 这样就是公平锁,可以让每个线程都有出现的机会。 结果如下:

AA :卖出30 剩余:29
AA :卖出29 剩余:28
AA :卖出28 剩余:27
AA :卖出27 剩余:26
AA :卖出26 剩余:25
AA :卖出25 剩余:24
AA :卖出24 剩余:23
AA :卖出23 剩余:22
AA :卖出22 剩余:21
AA :卖出21 剩余:20
AA :卖出20 剩余:19
AA :卖出19 剩余:18
AA :卖出18 剩余:17
AA :卖出17 剩余:16
BB :卖出16 剩余:15
AA :卖出15 剩余:14
CC :卖出14 剩余:13
BB :卖出13 剩余:12
AA :卖出12 剩余:11
CC :卖出11 剩余:10
......

2.5、面试题

为什么会有公平锁、非公平锁的设计?为什么默认非公平?

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销线程的开销

什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用

3、可重入锁(又名递归锁)

3.1、概述

什么是可重入锁?

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞

如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚,所以Java中ReentrantLockSynchronized都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁

3.2、实例

synchronizedlock都是可重入锁

  • sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁。
  • 而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构。

代码验证synchronizedReentrantLock是可重入锁

synchronized锁机制

Object o = new Object();
new Thread(()->{
    synchronized(o) {
        System.out.println(Thread.currentThread().getName()+" 外层");

        synchronized (o) {
            System.out.println(Thread.currentThread().getName()+" 中层");

            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+" 内层");
            }
        }
    }

},"t1").start();

结果:synchronized (o)代表锁住当前{ }内的代码块

t1 外层
t1 中层
t1 内层

lock锁机制

public class SyncLockDemo {

    public synchronized void add() {
        add();
    }

    public static void main(String[] args) {
        //Lock演示可重入锁
        Lock lock = new ReentrantLock();
        //创建线程
        new Thread(()->{
            try {
                //上锁
                lock.lock();
                System.out.println(Thread.currentThread().getName()+" 外层");

                try {
                    //上锁
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+" 内层");
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }finally {
                //释放锁
                lock.unlock();
            }
        },"t1").start();

        //创建新线程
        new Thread(()->{
            lock.lock();
            System.out.println("aaaa");
            lock.unlock();
        },"aa").start();
    }
}

结果

t1 外层
t1 内层
aaaa

在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁。 要把握上锁与解锁的概念,都要写上。 (lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程)

3.3、源码解析

Synchronized的重入的实现机理(为什么任何一个对象都可以成为一个锁)

  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
  • 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
  • 当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放

image.png

4、死锁

4.1、概述

什么是死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源造成互相等待资源的现象而陷入死锁

image.png

产生死锁的原因:

  • 系统资源不足
  • 系统资源分配不当
  • 进程运行顺序不当

4.2、死锁演示

public class DeadLock {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁b");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (b) {
                System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁a");
                }
            }
        },"B").start();
    }
}

结果:程序发生阻塞

A持有锁a,试图获取锁b
B持有锁b,试图获取锁a

4.3、死锁的排除

排除死锁方式一:纯命令

  • jps 类似于linux中的ps -ef查看进程号

  • jstack 自带的堆栈跟踪工具

通过用idea自带的命令行输入 jps -l 查看其编译代码的进程号后jstack 进程号查找死锁问题

D:\studySoft\Idea201903\JavaSelfStudy>jps
10048 Launcher
6276 DeadLockDemo
6332 Jps
9356
D:\studySoft\Idea201903\JavaSelfStudy>jstack 6276 (最后面有一个发现了一个死锁)
2021-07-28 16:05:36
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003592800 nid=0x830 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"b" #15 prio=5 os_prio=0 tid=0x00000000253d5000 nid=0x1ba8 waiting for monitor entry [0x0000000025c8e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)
        - waiting to lock <0x0000000741404050> (a java.lang.Object)
        - locked <0x0000000741404060> (a java.lang.Object)
        at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"a" #14 prio=5 os_prio=0 tid=0x00000000253d3800 nid=0xad8 waiting for monitor entry [0x0000000025b8e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
        - waiting to lock <0x0000000741404060> (a java.lang.Object)
        - locked <0x0000000741404050> (a java.lang.Object)
        at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"Service Thread" #13 daemon prio=9 os_prio=0 tid=0x000000002357b800 nid=0x1630 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x00000000234f6000 nid=0x1fd4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x00000000234f3000 nid=0x5c0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x00000000234ed800 nid=0x1afc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x00000000234eb800 nid=0x2ae0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x0000000023464800 nid=0xc50 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x000000002345f800 nid=0x1b0c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000000023451000 nid=0x2028 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000002343f800 nid=0x1ea0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000233eb800 nid=0x10dc runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000233d3000 nid=0xafc in Object.wait() [0x000000002472f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000021d0d000 nid=0x28ec in Object.wait() [0x000000002462f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000741006b40> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000741006b40> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

JNI global references: 2504


Found one Java-level deadlock:
=============================
"b":
  waiting to lock monitor 0x0000000021d10b58 (object 0x0000000741404050, a java.lang.Object),
  which is held by "a"
"a":
  waiting to lock monitor 0x0000000021d13498 (object 0x0000000741404060, a java.lang.Object),
  which is held by "b"

Java stack information for the threads listed above:
===================================================
"b":
        at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)
        - waiting to lock <0x0000000741404050> (a java.lang.Object)
        - locked <0x0000000741404060> (a java.lang.Object)
        at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
"a":
        at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
        - waiting to lock <0x0000000741404060> (a java.lang.Object)
        - locked <0x0000000741404050> (a java.lang.Object)
        at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

5、乐观锁和悲观锁

悲观锁(synchronized关键字和Lock的实现类都是悲观锁)

  • 什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源
  • synchronized关键字和Lock的实现类都是悲观锁

乐观锁

  • 概念:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
  • 乐观锁在Java中通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的,适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升

乐观锁一般有两种实现方式

  • 采用版本号机制
  • CAS算法实现
//悲观锁的调用方式
public synchronized void m1(){
    //加锁后的业务逻辑
}

//保证多个线程使用的是同一个lock对象的前提下
ReetrantLock lock=new ReentrantLock();
public void m2(){
    lock.lock();
    try{
        //操作同步资源
    }finally{
        lock.unlock();
    }
}

//乐观锁的调用方式
//保证多个线程使用的是同一个AtomicInteger
private  AtomicInteger atomicIntege=new AtomicInteger();
atomicIntege.incrementAndGet();

6、自旋锁

什么是自旋锁?

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

如下方法就是一个标准的自旋锁

public final int getAndAddInt(0bject o, Long offset, int delta) {
    int v;
    do{
        v = getIntVolatile(o, offset);
    } while ( ! compareAndSwapInt(o, offset, v, v + delta));
    return V;
}

如何手写一个自旋锁

//自旋锁
public class AtomicReferenceThreadDemo {
    static AtomicReference<Thread>atomicReference=new AtomicReference<>();
    static Thread thread;

    //枷锁
    public static void lock(){
        thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
        while(!atomicReference.compareAndSet(null,thread)){

        }
    }

    //释放锁
    public static void unlock(){
        System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
        atomicReference.compareAndSet(thread,null);
    }
    public static void main(String[] args) {

        new Thread(()->{
            AtomicReferenceThreadDemo.lock();
            try { 
                TimeUnit.SECONDS.sleep(3);  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            AtomicReferenceThreadDemo.unlock();
        },"A").start();

        new Thread(()->{
            AtomicReferenceThreadDemo.lock();
            AtomicReferenceThreadDemo.unlock();
        },"B").start();
    }
}

CAS缺点

  1. 循环时间长开销很大
  2. 引出来ABA问题(在CAS篇章将详细说明)