线程安全问题以及解决方案,同步锁synchronized的使用,单例模式的设计,线程的等待唤醒机制(例子),线程的声明周期,锁的释放,死锁(十六)

300 阅读16分钟

线程安全

1、什么是线程安全问题?

​ 当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

如下:

public class TestSafe {
    public static void main(String[] args) {
        SaleTicketThread s1 = new SaleTicketThread("窗口1");
        SaleTicketThread s2 = new SaleTicketThread("窗口2");
        SaleTicketThread s3 = new SaleTicketThread("窗口3");

        s1.start();
        s2.start();
        s3.start();

        //代码运行出现了问题:负数票或重复票
    }
}

class SaleTicketThread extends Thread{
    private static int total = 10;

    public SaleTicketThread(String name) {
        super(name);
    }

    public void run(){
        while(total>0){
            try {
                Thread.sleep(10);//让程序慢一点,也是为了让问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(getName() + "卖出一张票,剩余:" + total + "张");
        }
    }
}

造成上述结果的原因如图所示: 卖票的线程安全问题分析.png

2 如何解决线程安全问题?

思路:对影响共享数据的代码加“锁”,一个线程未执行完之前,其他的线程就只能等待。

如何加锁?

JSE阶段只介绍同步锁,synchronized

3.同步的语法

(1)同步代码块

(2)同步方法

4.同步代码块

synchronized(锁对象){
	代码
}

锁对象又称为监视器对象(门卫)。

注意:

(1)锁对象的选择问题

(2)锁的代码范围问题

把影响和访问共享数据的代码都要锁进去

5.锁对象有什么要求?

(1)什么类型的对象可以作为锁对象?

​ 任意引用数据类型的对象都可以。

(2)锁对象的要求

​ 必须多个具有竞争关系的线程,要使用“同一个”锁对象。(同一个是指内存地址一样)

把买票的例子,代码重新处理一下:

①:资源类

买票的这个例子中,票是资源

②:线程类

模拟卖票的窗口

③测试类

创建线程对象,资源对象等,并且启动线程

public class TestSafe2 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket(1000);

        //分为三个窗口同时卖票
        Window w1 = new Window("窗口1",ticket);
        Window w2 = new Window("窗口2",ticket);
        Window w3 = new Window("窗口3",ticket);

        //启动线程
        w1.start();
        w2.start();
        w3.start();
    }
}

class Window extends Thread{
    private Ticket ticket; //如果是同一个ticket对象,那么就是同一个“电影”的票

    public Window(String name, Ticket ticket) {
        super(name);
        this.ticket = ticket;
    }

    public void run(){
       // synchronized (this) {//(1)这里的this是Window类型(2)三个线程使用的是同一个Window对象  ==>这里不能用this
        //synchronized (ticket) {//(1)这里的ticket是Ticket类型(2)三个线程使用的是同一个ticket
                            //把while (ticket.getTotal() > 0) 锁进去,范围又太大了,导致其他线程没机会了
        /*while (ticket.getTotal() > 0) {
            synchronized (ticket){ //这样又范围太小了,  ticket.getTotal() > 0没锁进来
                try {
                    Thread.sleep(10);//模拟两次卖票之间的时间间隔
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }

        }*/

        while(true){
            synchronized (ticket){
                if(ticket.getTotal()>0) {
                    try {
                        Thread.sleep(10);//模拟两次卖票之间的时间间隔
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket.sale();
                }else{
                    break;
                }
            }
        }
    }
}

class Ticket{
    //票有很多信息,编号等,这里暂时忽略,我们只关注数量
    private int total;

    public Ticket(int total) {
        this.total = total;
    }

    public int getTotal() {
        return total;
    }

    public void sale(){
        //synchronized (this) {//(1)这里this是Ticket类型(2)3个卖票的线程,使用的this对象是否是同一个
                            //锁这里范围太小
            total--;
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
       // }
    }
}

6 为什么任意类型的对象都可以当锁?而且为什么要求是同一个对象?

​ 因为Java对象在内存中存储时分为三个部分:

(1)对象头

​ ①当前对象所属的类型

​ ②对象的hashcode值

​ ③线程id-----哪个线程现在占有锁对象,只有锁对象中标记的线程具有执行同步代码块的权力,其他线程只能等待

​ ④...其他

(2)对象的实例对象

(3)对齐空白

7.同步方法:静态方法

语法格式:

【其他修饰符】 synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{

}

无论是同步代码块还是同步方法,好不好用,能不能解决线程安全问题,都是从两个方面来说:

(1)锁对象的选择

同步方法的锁对象是默认。

非静态方法,默认就是this对象。

静态方法,默认就是当前类的Class对象。

对象的类型以及多个线程是否用同一个对象。

(2)锁的范围问题

把影响和访问共享数据的代码都要锁进去。

public class TestSale3 {
    public static void main(String[] args) {
        Piao piao  = new Piao(1000);
        MaiPiao m1 = new MaiPiao("窗口1",piao);
        MaiPiao m2 = new MaiPiao("窗口2",piao);
        MaiPiao m3 = new MaiPiao("窗口3",piao);
        m1.start();
        m2.start();
        m3.start();
    }
}

class MaiPiao extends Thread{
    private static Piao piao;

    /*
    在讲类初始化和实例初始化。
    类初始化:给静态变量初始化的。只和静态代码块和静态变量显式赋值有关。
    实例初始化:给非静态变量初始化。 和构造器、非静态代码块、非静态变量显式赋值、super()和super(xx)有关。
    静态变量正常是不会通过构造器进行初始化的。
     */
    public MaiPiao(String name,Piao piao) {
        super(name);
        this.piao = piao;
    }

    /*
    run()是非静态,默认锁对象是this
    (1)这里的this对象的类型是MaiPiao
    (2)这里的this,三个线程用的是同一个吗?不是
    不合适
     */
    /*public synchronized void run(){
        while(true){
            if(piao.getTotal()>0){//和共享数据有关
                piao.sale();
            }else{
                break;
            }
        }
    }*/

    public void run(){
        while(true){
            if(piao.getTotal()>0) {//双重条件  同步方法里也有该条件判断,所以仍然是安全的
                saleOneTicket();
            }else{
                break;
            }
        }
    }

    /*
   saleOneTicket()是静态,默认锁对象是当前类的Class对象
    (1)这里的当前类的Class对象就是MaiPiao的Class对象,
    (2)这里的Class对象,三个卖票线程用的是同一个吗?是,只要是同一个类,Class对象就是同一个

    Class对象是什么?
    我们想一想,所有的Java类有共同特征,
    都有5大成员,都能new对象...

    类的定义:一类具有相同特性的事物的抽象描述。
    那么所有的Java类具有共同特征,那么可以用一个类(Class)来描述类。
    Class类的一个对象,就是一个具体的类,比如是Piao类,MaiPiao类,Student类。

    Java中把某个类加载在内存之后,会用一个Class对象进行表示。
    即只要用的是同一个类,那么就是同一个Class对象。
     */
    public synchronized static void saleOneTicket(){
        if(piao.getTotal()>0){//和共享数据有关
            piao.sale();
        }
    }
}

class Piao{
    //票有很多信息,编号等,这里暂时忽略,我们只关注数量
    private int total;

    public Piao(int total) {
        this.total = total;
    }

    public int getTotal() {
        return total;
    }

    /*
    非静态方法,默认锁对象是this
    this:(1)类型是Piao类型(2)多个线程是否用了同一个piao对象,这里是
    范围太小
     */
   /* public synchronized void sale(){
        total--;
        System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
    }*/
    public void sale(){
        total--;
        System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
    }
}

8 同步方法:非静态方法

(1)锁对象:this,非静态方法,默认就是this

​ ①:类型:对象的所属类

​ ②:多个线程用同一个锁对象

(2)范围:通常情况下,就是选一次业务逻辑代码

public class TestSale4 {
    public static void main(String[] args) {
        CinemaTicket c = new CinemaTicket(1000);

        CinemaWindow w1 = new CinemaWindow("窗口1",c);
        CinemaWindow w2 = new CinemaWindow("窗口2",c);
        CinemaWindow w3 = new CinemaWindow("窗口3",c);
        w1.start();
        w2.start();
        w3.start();
    }
}
//线程类
class CinemaWindow extends Thread{
    private CinemaTicket ticket;

    public CinemaWindow(String name, CinemaTicket ticket) {
        super(name);
        this.ticket = ticket;
    }

    public void run(){
        while(true){
            if(ticket.getTotal()>0){
                ticket.sale();
            }else{
                break;
            }
        }
    }
}

//资源类对象
class CinemaTicket {
    private int total;

    public CinemaTicket(int total) {
        this.total = total;
    }

    public int getTotal() {
        return total;
    }

    /*
    (1)锁对象:this,非静态方法,默认就是this
    A:类型:CinemaTicket
    B:多个线程是否用同一个锁对象,是
    (2)范围:通常情况下,就是选一次业务逻辑代码
        例如:卖票,从查询是否还有票,到卖一张票完成,是一次业务逻辑
             生产xx东西,产品比较复杂,需要组装,一次业务逻辑,就是包含,不同零件的生产,到组装完成

             要求,两个线程,分别打印数字,一个线程连续打印5个数字之后,才能换另一个线程。  一次业务逻辑就是打印5个数字。

     */
    public synchronized void sale(){
        if(total>0) {
            total--;
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
        }else{
//            System.out.println("没票了");
            throw new RuntimeException("没票了");
        }
    }
}

单例模式

1.什么是单例设计模式?

(1)什么是单例?

​ 单例:就是唯一的对象,即某个类在整个系统中只有唯一的对象。

(2)什么是设计模式

​ 设计模式可以理解为一套经验。之前的很多程序员,在遇到一些问题时,采用某个代码结构,发现这个代码结构很好用,就把这些好用的代码结构总结出来,后面的程序员就可以套用。

如:

Spring是管理所有JavaBean(Java类)的容器在整个系统中只有一个。数据库连接池是用来管理,Java连接数据库的连接对象,这个池在整个系统中只有一个。

2.如何实现单例模式?

(1)饿汉式

(2)懒汉式

如何区别是饿汉式还是懒汉式?

对象的创建时间:

饿汉式在类加载和初始化时,就创建了唯一的对象,不管这个对象现在用不用,都会直接把它创建出来。

​ 饿汉式:后期用的时候方便,直接拿来用就好了,不用当场创建

懒汉式:在使用的时候再创建。

​ 懒汉式:可以节省类初始化的时间,减少对象占用内存的时间。

3.饿汉式实现方式

(1)enum声明的枚举实现

enum Singleton1{
    INSTANCE;
}

(2)类似jdk1.5之前的枚举代码改装实现

class SingleTwo{
    public static final SingleTwo INSTANCE = new SingleTwo();//INSTANCE是SingleTwo的唯一对象
    private SingleTwo(){

    }

(3)对(2)的改装

class SingleThree{    
    private static final SingleThree INSTANCE = new SingleThree();
    //INSTANCE是SingleTwo的唯一对象    

    private SingleThree(){    }    
    public static SingleThree getInstance() {
    return INSTANCE;    
    }
}

核心类库中的Runtime类就是(3)形式,

java.lang.Runtime类,它是代表当前JVM运行环境的对象。

4.懒汉式的实现方式(需要保证线程安全)

(1)同步方法

class SingleFour{
    private static SingleFour instance;
    private SingleFour(){}   
    /*    synchronized方法行不行?    
    (1)锁对象:  静态方法的锁对象是当前类的Class对象  SingleFour的Class对象只有一个    
    (2)锁范围:  一次业务逻辑,判断对象new没new,没new的话,就把对象new 对象    
    */    
    public static synchronized SingleFour getInstance(){//        return new SingleFour();//错误的,这样的话,调用一次getInstance(),就new一个,就不是单例了        
        if(instance == null){
                instance = new SingleFour();       
            }        
        return instance;    
        }
    }

(2)同步代码块

​ 优于同步方法,因为同步方法将synchronized加在了方法上,也就是说即使已经被创建出对象了,还是要等待这个锁释放,效率比较低

class SingleFive{
    private static SingleFive instance;    
    private static final Object lock = new Object();    
    private SingleFive(){    }    
    public static SingleFive getInstance() {//        
    synchronized (this){//静态方法中,不允许出现this//synchronized (""){//""它是一个对象,空字符串对象//        
    synchronized (SingleFive.class){//当前类的Class对象       
    if(instance == null) {//提高效率            
    synchronized (lock) {
    if (instance == null) {//保证唯一性
    instance = new SingleFive();
                }
            }
        }       
        return instance;    
        }
   }

(3)静态内部类

​ 内部类和外部类可以互访private成员,但是静态内部类不随外部类的加载而加载,它的静态成员也就不能马上被创建。这个静态内部类必须要被调用才能加载,也就是将INSTANCE对象实例化。也就完成了懒汉单例模式的实现。

class SingleSix{
private SingleSix(){}    //静态内部类    
private static class Inner
static final SingleSix INSTANCE = new SingleSix();
}    
public static SingleSix getInstance(){
return Inner.INSTANCE;    
}
}

5 单例模式的线程安全性讨论

饿汉式实现单例模式是线程安全的

懒汉式实现单例模式可能是线程不安全的。需要进行加锁的操作。

线程的等待唤醒机制

1.什么情况下会用到等待唤醒机制?

问题的场景:用来解决“生产者与消费者问题”。

​ 一些线程负责“生产”数据,一些线程负责“消耗”数据。如果这个数据是放到某个容器中(缓冲区),缓冲区是有大小限制的。这个过程中,生产的线程和消费的线程会有这样的情况,当数据是空的时候,消费者线程是不能消费,只能“等待”,消费者消费了数据后,可以“唤醒”生产者线程。当数据是满的时候,生产者线程是不能生成,只能“等待”,生产者生产了数据后,可以“唤醒"消费者线程。

2 如何实现等待唤醒机制?

它依赖于两个方法:

(1)wait方法:等待

(2)notify方法/notifyAll方法:通知/唤醒。

这两组方法在java.lang.Object类中

3.为什么这两个和线程有关系的方法,它不在Thread类中声明,反而在Object类声明?

​ wait方法和notify方法/notifyAll方法,必须由”锁“对象调用,否则会报java.lang.IllegalMonitorStateException(运行时异常)异常。

4 为什么生产者消费者问题,也要锁对象?

​ 因为生产者消费者问题也有线程安全问题。

​ 生产者线程会读和写数据,消费者线程会读和写数据,多个线程都会对某个共享数据进行读和写,就会有线程安全问题。

5 为什么wait方法和notify/notifyAll方法必须由”锁“对象调用?

因为wait方法需要释放锁,必须告诉锁对象,需要切换线程id标识了,所以必须要”锁“对象。

6 单个消费者与多个消费者问题

示例

​ 有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有1个厨师和1个服务员。

public class TestWaitNotify {
    public static void main(String[] args) {
        //创建工作台
        WorkBench w = new WorkBench();

        //创建厨师和服务员线程,并且启动
        Waiter waiter = new Waiter("翠花",w);
        Cook cook = new Cook("小高",w);

        waiter.start();
        cook.start();
    }
}
//工作台类
class WorkBench{
    private int total;//记录放在工作台上的快餐的数量
    private static final int MAX_VALUE = 10;

    //put方法是非静态方法,默认的锁对象就是this
    public synchronized void put(){
        try {
            if(total >= MAX_VALUE){
                this.wait(); //this是WorkBench类型,它也有这个wait方法,只要是Object的子类都有
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        total++;
        System.out.println(Thread.currentThread().getName() + "制作了一份快餐,工作台上的快餐数量是:" + total);
        this.notify();
    }

    public synchronized void take(){
        try {
            if(total <= 0){
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        total--;
        System.out.println(Thread.currentThread().getName() + "取走了一份快餐,工作台上的快餐数量是:" + total);
        this.notify();
    }
}

//服务员线程
class Waiter extends Thread{
    private WorkBench workBench;

    public Waiter(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    public void run(){
        while(true){
            workBench.take();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//厨师线程
class Cook extends Thread{
    private WorkBench workBench;

    public Cook(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    public void run(){
        while(true){
            workBench.put();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7 什么时候会报IllegalMonitorStateException?

​ IllegalMonitorStateException:非法监视器状态异常。

​ 因为wait和notify等方法不是由“锁”对象调用的,就会报这个异常。

8 多个生产者与多个消费者问题

(1)问题升级

​ 饭馆扩大了,有多个服务员和多个厨师。

(2)出现了新的问题以及解决方案

①出现了-1?

​ Ⅰ 思路一,唤醒对方,生产者只唤醒消费者,消费者只唤醒生产者,就可以,SE阶段不行。高级juc可以。

​ Ⅱ 思路二:如果被唤醒之后不着急往下执行 --和++等代码,而是重新判断 条件,就可以了。 使用循环

②程序卡住了?(线程全部休眠)

​ Ⅰ 思路一每次唤醒时,不要只唤醒一个,唤醒所有等待的线程,让这几个线程自己抢对象锁。可以使用notifyAll方法代替notify方法

​ Ⅱ 不要让线程无限等待,可以设置等待时间。wait()可以换成wait(时间)等一段时间之后,如果没人唤醒,自动醒来。

public class TestWaitNotifyAll {
    public static void main(String[] args) {
        //创建工作台
        WorkBench w = new WorkBench();

        //创建厨师和服务员线程,并且启动
        Waiter waiter1 = new Waiter("翠花",w);
        Cook cook1 = new Cook("小高",w);

        Waiter waiter2 = new Waiter("如花",w);
        Cook cook2 = new Cook("小丁",w);

        waiter1.start();
        cook1.start();

        waiter2.start();
        cook2.start();
    }
}
//工作台类
class WorkBench{
    private int total;//记录放在工作台上的快餐的数量
    private static final int MAX_VALUE = 1;//这里用1,是为了让问题及早出现

    //put方法是非静态方法,默认的锁对象就是this
    public synchronized void put(){
        try {
//            if(total >= MAX_VALUE){
            while(total >= MAX_VALUE){
                this.wait(); //this是WorkBench类型,它也有这个wait方法,只要是Object的子类都有
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        total++;
        System.out.println(Thread.currentThread().getName() + "制作了一份快餐,工作台上的快餐数量是:" + total);
//        this.notify();
        this.notifyAll();
    }

    public synchronized void take(){
        try {
//            if(total <= 0){
            while (total <= 0){
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        total--;
        System.out.println(Thread.currentThread().getName() + "取走了一份快餐,工作台上的快餐数量是:" + total);
//        this.notify();
        this.notifyAll();
    }
}

//服务员线程
class Waiter extends Thread{
    private WorkBench workBench;

    public Waiter(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    public void run(){
        while(true){
            workBench.take();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//厨师线程
class Cook extends Thread {
    private WorkBench workBench;

    public Cook(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    public void run() {
        while (true) {
            workBench.put();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

(3)关于上述问题的分析

①出现-1的情况

多个生产者多个消费者问题1:-1.png

②出现全部休眠的情况

多个生产者多个消费者问题2:卡住了.png

JVM的运行时内存:

(1)方法区

(2)堆

(3)Java虚拟机栈

(4)本地方法栈

(5)程序计数器:每一个线程都会在程序计数器中有一小块独立的内存,记录这个线程的下一条指令。

线程的生命周期

1.JDK1.5之前

(1)新建:new

(2)就绪:从新建-->就绪,经过了start(启动)

​ 只有就绪状态的线程,才能被CPU调度。

(3)运行:正在被CPU调度的程序。

​ 运行时间是非常短的,CPU分配的时间片到了,当前线程会从运行状态切换到就绪状态。从运行状态要切换到就绪状态等时,会对当前线程进行“快照”,记录当前线程运行到哪里,下次在被调用时,从那里接着运行。这个记录是由程序计数器来负责的。

线程运行到yield方法,时间片未到也会从运行状态到就绪状态。

(4)阻塞:当线程运行时,遇到了一些特殊的情况,会让线程从运行状态转入阻塞状态。

特殊情况有:

​ ①sleep

​ ②wait

​ ③IO阻塞操作等比较耗时的指令

​ ④join(加塞),其他线程执行加塞

​ ⑤等待锁

​ ⑥suspend(已过时)

从阻塞状态转为就绪状态:

​ ①sleep --->sleep时间到就自动转入就绪状态

​ ②wait --->wait时间到,或被notify/notifyAll

​ ③IO阻塞操作等比较耗时的指令 ---->需要的数据准备好了

​ ④join(加塞),其他线程执行加塞 --->join时间到或者加塞的线程执行完了。

​ ⑤等待锁 --->占用锁的线程释放了锁

​ ⑥suspend(已过时) --->resume(已过时)

(5)死亡:

​ 正常死亡:run方法结束

​ 非正常死亡:线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)。

​ 手动停止:调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)

2 JDK1.5之后

java.lang.Thread类把线程状态划分的更加详细,在Thread类中,用一个内部枚举类State规定了线程的几种状态。

​ State枚举类有6个常量对象:

NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED

​ (新建,可运行,阻塞,等待,限时等待,终止)

Runnable(可运行):就是就绪和运行状态,因为这两个之间很频繁,而且运行的时间非常短,所以就没有在Java线程对象中进行区分。

BLOCKED:等待锁。

WAITING

​ wait没有指定时间

​ join没有指定时间

​ LockSupport类的park方法

TIMED_WAITING

​ sleep();

​ 指定了时间的wait

​ 指定了时间的join

当从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

TERMINATED

​ 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

释放锁操作与死锁

1.释放锁的操作

(1)同步方法或同步代码块{}中的内容运行完了,自动释放锁。

(2)当前线程在同步代码块或同步方法出现了未处理的Error或者Exception,导致当前方法结束,自动释放锁。

(3)在同步代码块、同步方法执行了锁对象的wait()方法,当前线程被挂起,释放锁。

2.哪些操作不会释放锁?

(1)在同步代码块或者同步方法中遇到调用,Thread.sleep(),Thread.yield()方法

(2)调用了该线程的suspend()方法(已过时)

3.什么是死锁?

​ 当两个或者多个线程,互相等待对方释放它需要的锁时,就会导致死锁。

​ 当出现同步代码块或者同步方法嵌套使用时,就很容易出现死锁,所以要格外注意。

(1)sleep()不释放锁,wait()释放锁

(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll

(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明

​ 因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。

public class TestDeadLock {
    public static void main(String[] args) {
        Object girlFriend = new Object();
        Object money = new Object();

        BoyFriend boy = new BoyFriend(girlFriend, money);
        BangFei bang = new BangFei(girlFriend, money);

        boy.start();
        bang.start();
    }
}
class BoyFriend extends Thread{
    private Object girlFriend;
    private Object money;

    public BoyFriend(Object girlFriend, Object money) {
        this.girlFriend = girlFriend;
        this.money = money;
    }

    public void run(){
        synchronized (money){
            System.out.println("你放了我女朋友,我给你500万");
            synchronized (girlFriend){
                System.out.println("给绑匪500万");
            }
        }
    }
}
class BangFei extends Thread{
    private Object girlFriend;
    private Object money;

    public BangFei(Object girlFriend, Object money) {
        this.girlFriend = girlFriend;
        this.money = money;
    }

    public void run(){
        synchronized (girlFriend){
            System.out.println("你给我500万,我放人");
            synchronized (money){
                System.out.println("放人");
            }
        }
    }
}
//尝试多次运行时会偶发死锁,是因为两个线程同时进入最外层锁后,就会等待对方释放第二个锁,导致程序无法运行下去