多线程(基础知识)

目录

并发和并行

多线程的实现方式

多线程的实现方式有三种方式分别是:

1.继承Thread类的方式实现

2.实现Runnable接口的方式实现

3.利用Callable接口和Ftuture接口实现

多线程三种实现方式的比较

Thread中常见的成员方法

currentThread() 方法

sleep()方法

setPriority()和getPrioritry()方法

setDaemon()方法

yield()方法

join()方法

线程的生命周期

线程的安全问题


多线程,字面意思就是多个线程(多个线程执行程序),我们在之前学习的都是单线程,多线程我们在以后的开发中会经常使用到,举个例子,就比如说一个音乐播放器,我们在搜索的时候,同时还可以播放歌曲,这就是多线程.

并发和并行

并发:多个指令在在单个cpu执行
并发:多个指令在多个cpu上执行
那就离谱了,我的电脑明明就一个cpu呀,怎么说是多个cpu呢.其实我们的电脑有八核十六线程,十六核三十二线程,三十二核六十四线程.拿八核十六线程来说,八个核在十六个线程之间来回执行,我们注意,核在找线程时,时随机的,概论问题.

多线程的实现方式

多线程的实现方式有三种方式分别是:

1.继承Thread类的方式实现

过程如下:
1.定义一个类继承Thread类
2.重写Thread类中的run方法
2.在测试类中创建自己实现类的对象,并启动线程.
参考下面代码

public class Test {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        MyThread myThread2=new MyThread();
        myThread.setName("线程1 ");
        myThread2.setName("线程2 ");
        myThread.start();
        myThread2.start();
    }
}




public class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+ "hello word"+" "+i);
        }
    }
}

把上面代码复制到开发环境,会发现会交替执行输出hello word.

2.实现Runnable接口的方式实现

过程如下
1.自己创建一个类实现Runnable接口
2.重写run()方法
3.创建测试类,实例化自己实现的类
4.创建Thread对象,并将自己实例化的对象传入
5.启动线程
代码实现如下
 

public class Test {
    public static void main(String[] args) {
        MyRunnable myRunnable=new MyRunnable();
        Thread thread=new Thread(myRunnable);
        Thread thread2=new Thread(myRunnable);
        thread.setName("线程1");
        thread2.setName("线程2");
        thread.start();
        thread2.start();
    }
}


public class MyRunnable implements Runnable{
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+ "hello word"+" "+i);
        }
    }
}
3.利用Callable接口和Ftuture接口实现

过程如下
1.创建一个MyCallable类,实现Callable接口
2.重写call()方法(是有返回值的,表示多线程的运行结果
3.创建MyCallable对象(表示多线程要执行的任务)
4.船舰Future对象,Future是一个接口,其实是实现FutureTask类(作用:管理多线程的运行结果)
5.创建Thread类的对象,并启动
代码如下

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable =new MyCallable();
        //这里注意如果用Future作为类型要强转,这是因为Thread类实现了Runnable接
        //Future future=new FutureTask<>(myCallable);
        //Thread thread=new Thread((Runnable) future);
        FutureTask futureTask = new FutureTask<>(myCallable);
        Thread thread=new Thread(futureTask);
        thread.start();
        int get=futureTask.get();
        System.out.println(get);
    }
}


import java.util.concurrent.Callable;

public class MyCallable implements Callable {
    int sum=0;
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

上面就是实现多线程的三种方式

多线程三种实现方式的比较

优点 缺点
继承Thread类

代码简单,可以直接使用

Thread类中的方法

可扩展性差,

不能再继承其他的类

实现Runnable接口 扩展性强,实现接口的同时还可以继承类 代码比较复杂,不能直接使用Thread中的类
实现Callable接口 扩展性强,实现接口的同时还可以继承类 代码比较复杂,不能直接使用Thread中的类

Thread中常见的成员方法

方法名称 说明
String  getName() 返回此线程的名称
void   setName() 设置线程名称(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
final void setDaemon(boolean on) 设置为守护线程
public static void yield 出让线程/礼让线程
public static void join 插入线程/插队线程

下面对每个方法进行介绍,第一个和第二个方法,在前面代码中已经用过了,比较简单就不介绍了,补充一下,当我们不给线程设置名字的时候,它有一个默认的名字,格式是 : Thread-x(x是序号 从0开始),利用构造方法设置线程名字,要用到super关键字,因为构造方法是不能继承的,要是有父类的构造就要再写一个构造方法,并用super关键字

currentThread() 方法

该方法是获得当前线程的对象,这个方法我们上面也用到了,就是获得线程名字的时候用的,它获得了,线程的对象就可以调用该对象的方法.如果们在main方法中使用,结果就是打印main,当Java虚拟机启动之后,会自动的启动多条线程,其中一条就叫做main线程,它的作用就是调用main方法.

sleep()方法

哪条线程执行到这个方法,那么哪条线程就会在这个地方停留.停留时间自己设置,单位是毫秒,当时间到了之后,线程会自动醒来,自动执行下面的代码.

setPriority()和getPrioritry()方法

这个方法就是设置线程的优先级,在学习之前我们先学习一下线程的调度
抢占式调度(随机):就是多个线程在抢夺cup的执行权,cpu在选择执行哪条线程是不确定的,执行多长时间也是不确定的.
非抢占式调度:就是你一次我一次这样的调度
在Java中采用的式抢占式调度

我们利用setPriority()方法设置优先级,优先级越大,被执行的概率就越大,默认是5,最大是10,当我们把优先级设置为10的时候,并不意味着100%执行,只是执行的概率变大了.main方法默认也是5.
使用方式也很简单就是用创建好的线程对象去调用这个两个方法,就不演示了,非常简单.

setDaemon()方法

守护线程,表面意思就是让一个线程去守护某一个线程,当一个线程执行完之后,守护线程也会陆续执行完毕,注意这里不是立马就停止,而是陆续停止.还是创建两个线程对象,让其中一个线程调用这个方法,设置为守护线程,另一个不设置,我们可以看到当那个不是守护线程的线程执行完之后,守护线程也会陆续停止,即使守护线程的逻辑还没执行完也会陆续停止.这就是守护线程,也就是当非守护线程执行完了,守护线程也就没有存在的必要了.
守护线程的应用场景就好比我们用聊天软件聊天,你向一个人发送文件,在发送的途中你关闭了聊天,发送就会停止.

yield()方法

让出先线程,意思就是把线程让出去,如果要执行下面的代码,则需要重新抢夺cpu.

join()方法

就是强线程的执行权.

线程的生命周期

看下面这个图
多线程(基础知识)_第1张图片

线程的安全问题

一般线程安全

以买票问题为例子,有100张票,用三个窗口进行出售,我们就可以把三个窗口看成三个线程.看下面这段代码,我们以第二种方式来创建多线程.来看下面这段代码
 

public class Test {
    public static void main(String[] args) {
        Lottery lottery=new Lottery();
        Thread thread1=new Thread(lottery);
        Thread thread2=new Thread(lottery);
        Thread thread3=new Thread(lottery);
        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    }


public class Lottery implements  Runnable{
    int ticket=0;
    @Override
    public void run() {
        while(true){
            if(ticket<100){
                try {
                    Thread.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
            }else{
                break;
            }
        }
    }
}

执行结果如下
多线程(基础知识)_第2张图片

我们就发现不对劲,三个窗口同时在卖1张票,这不是我想要的结果.怎么解决这个问题呢,下面就来介绍一下

解决线程安全问题在Java中就是用锁,在Java中,实现锁这个功能,要用到synchronized 这个关键字,上面这个例子有点麻烦,我们举一个简单的例子,创建两个线程,然后写两个循环,创建一个变量,两个变量在这两个循环中都自增加加.代码如下

 private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        thread1.start();
        thread.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }

我们知道,这段代码按我们没接触多线程之前,觉得count的结果为10000,但是当我们运行程序会发现并不是我们想的那样.那这事为什那么呢,我们知道当一个变量要被修改时,首先要从内存中读取到寄存器,把这个过程我简称为(load),之后进行累加(add),最后一个步骤写入到内存中(save).

那么就能意识到问题了,当一个线程刚加了一次,并且写到了内存里面,但是另一个线程是之前就拿到了,也加加了,但是这个线程没有另一个线程执行的次数多,就导致,执行快的线程,刚累加完,慢线程,把一个小的值又给覆盖了.这就导致了程序出现了bug.那么解决方案就是加锁.代码如下

 private static int count=0;
    private static Object  object=new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized(object){
                    count++;
                }

            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized(object){
                    count++;
                }
            }
        });
        thread1.start();
        thread.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }

其实上面就改了一点点代码执行结果就正确了,由此可见,在多线程中加锁的重要性.那么,分析一下,程序为什么执行正确,看上面代码,我们给count++加了一层锁,两个线程,当有一个线程执行到count++时,另一个线程就不会执行这个操作,当这个操作执行完之后,锁打开了,那么另一个线程才会去执行,也就是当一个线程执行完一套完整的操作后,另外一个线程才会去执行一整套操作.

还有就是要注意,加的锁对象可以是任意的对象,但是必须是同一个对象,如果对象不相同,那么这个锁就相当于没加.还有就是可以是类对象,就是我们每写一个Java文件就是一个类对象,JVM,会将Java文件编译成以  .class为后缀的文件,就直接在这个Java文件加上  .class就可以,它就是一个类对象.

当一个类去点class时,这其实也是Java的一种反射操作,通过反射操作,我们可以拿到这类的所有东西,包括被private修饰的变量或者方法.

此处的synchronized是JVM提供的功能,而JVM是C++实现的.进一步也是通过操作系统api来实现的加锁功能,而系统api又是cup特殊的指令来实现的.

还有就是synchronized能是自己能加锁开锁的,像其他语言也有这种功能.

synchronized修饰方法
看下面的代码

class Func{
    public int count;
    synchronized public void add(){
        count++;
    }
}
public class demon1 {
    public static void main(String[] args) throws InterruptedException {
        Func func=new Func();
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        thread.start();
        thread1.start();
        thread1.join();
        thread.join();
        System.out.println(func.count);

    }
}

上面在方法前面加synchronized,和之前代码执行结果相同,哪个对象调用这个add方法就要将这个过程执行完,开锁后其他对象才能调用.当然还有另外一种简写的方式代码如下:
 



class Func1{
    public int count;
    public void add(){
        synchronized(this){
            count++;
        }
        count++;
    }
}
public class demon2 {
    public static void main(String[] args) throws InterruptedException {
        Func func=new Func();
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        thread.start();
        thread1.start();
        thread1.join();
        thread.join();
        System.out.println(func.count);

    }
}

这种方法就是在synchronized后面加this关键字,我们知道this关键字表示当前对象的引用.哪个对象调用这个方法都会对该方法上锁,其他对象调用不了.当然这种方法也有不是所有的场景都适用,看下面的代码



class Func3{
    public int count;
    public static void add(){
        synchronized(Func3.class){
        }

    }
}
public class demon3 {
    public static void main(String[] args) throws InterruptedException {
        Func func=new Func();
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                func.add();
            }
        });
        thread.start();
        thread1.start();
        thread1.join();
        thread.join();
        System.out.println(func.count);

    }
}

如果一个方法是一个静态方法,那么它没有实例,也就用不了this这个关键字.当然如果我们实在想用一个对象,那么可以直接随便new一个对象,但是要加static.

上面是面对线程安全问题的基础解决方案,但是我们在平时写代码的时候可能,会出现一种情况----------死锁.下面我们来看死锁问题.

死锁

所谓的死锁就是针对一把锁连续加锁两次.当你在外层加一层锁之后,在里面又加了一个锁,按道理来说,程序会陷入阻塞.看下面代码
 


class Func1{
    public int count;
    public void add(){
        synchronized(this){
            count++;
        }
        count++;
    }
}
public class demon2 {
    public static void main(String[] args) throws InterruptedException {
        Func func=new Func();
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (func){
                    func.add();
                }

            }
        });
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (func){
                    func.add();
                }
            }
        });
        thread.start();
        thread1.start();
        thread1.join();
        thread.join();
        System.out.println(func.count);

    }
}

上面代码加了两层锁,在我们分析看来,一定会陷入阻塞,但是实际上执行结果任然是10000,程序并没有出现bug,原因就是JVM对synchronized关键字做了优化,让其不会出现死锁现象,如果是C++或者Python就会出现死锁,Java为了减少程序员写出死锁的概率,引入了特殊机制,解决上述死锁问题,这种机制又叫可重入锁.

死锁是一个大的话题设计的东西非常多下面我会一一介绍,包括怎么处理死锁现象.

那么再说一下为什么会产生线程安全问题
1.操作系统对线程的调度是随机的(抢占式执行)
2.多个线程对同时对一个变量进行修改
3.修改操作不是原子的(所谓的原子性:是指一个操作或者一组操作要么完全执行,要么不执行,不被其他线程中断(原子操作不会看到执行的中间状态).
4.内存可见性
5.指令重排序
第四点和第五点后面会说

好那么下面来说可重入锁,可重入锁会判断当前锁所加的所是不是当前线程,如果是就不会进行任何加锁,也不会有任何的阻塞,而是直接放行.
好,那么当面对锁的多重嵌套,第一层锁是加锁,那么在什么时候释放锁呢,当然是最后一层,如果中间有其他代码,那就又有线程安全问题了,那么JVM是怎么来判断在最后一个大括号呢,其实是有一个程序计数器,比方说count,初始时让count等于0,然后遇见右括号,就加 1 ,遇见左括号就减 1,当count=0时,就可以判断出这是最后一个括号了,这时候,就要释放锁了.

那么有没有synchronized无法结局的死锁呢,当然有,就比如:

线程1现针对A加锁,线程2针对B加锁,线程1在不释放锁A的情况下,再针对B加锁.线程2在不释放B的情况下,再针对A加锁.看下面代码
 

public class demon4 {
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
           synchronized(locker1){
               System.out.println("t1加锁,locker1完成");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               synchronized(locker2){
                   System.out.println("t1加锁 locker2完成");
               }
           }


        });
        Thread thread=new Thread(()->{
           synchronized (locker2){
               System.out.println("t2 加锁locker2完成");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               synchronized (locker1){
                   System.out.println("t1加锁 locker1完成");
               }
           }


        });
        thread.start();
        thread1.start();

    }
}

运行结果:

我们发现线程停在这里不动了,就形成死锁.

解决方案也很简单,就是在给A加锁加锁之后释放锁,然后再给B加锁.也就是下面这段代码

package thread_learn2;

public class demon5 {
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            synchronized(locker1){
                System.out.println("t1加锁,locker1完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
               
            }
            synchronized(locker2){
                System.out.println("t1加锁 locker2完成");
            }


        });
        Thread thread=new Thread(()->{
            synchronized (locker2){
                System.out.println("t2 加锁locker2完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
               
            }
            synchronized (locker1){
                System.out.println("t1加锁 locker1完成");
            }


        });
        thread.start();
        thread1.start();

    }
}

这时一种解决死锁的一种方法.

还有一种方法,就是破除循环等待,啥意思呢,就比如一个程序员要修关于进出公司的bug,然后保安不让他进,必须要出示码,但是程序员不去修bug怎么出示码呢,把这种情况出现到代码上,也就构成了死锁.

那么解决上面问题也很简单,就是给每一个锁编号,然后约定每个线程都按一定顺序来进行加锁,看下面代码

package thread_learn2;



public class demon6 {
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            synchronized(locker1){
                System.out.println("t1加锁,locker1完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(locker2){
                    System.out.println("t1加锁 locker2完成");
                }


            }


        });
        Thread thread=new Thread(()->{
            synchronized (locker1){
                System.out.println("t2 加锁locker1完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1加锁 locker2完成");
                }

            }



        });
        thread.start();
        thread1.start();

    }
}


我们会发现,除了我们不可逆转的情况出现的死锁,其他就是代码结构了,代码结构涉及的问题就是我上述说的两个问题,
1.请求和保持(这个就是线程1在不释放锁A的情况下去拿锁B,线程2不能把锁B强过来(解决方法就是等锁A释放后再拿锁B).
2.循环等待(就是那个程序员进公司的问题)解决方案就是把锁编号,约定顺序加锁.其实这个解决方案就是看哪个线程先start,先start的这个线程会优先被执行完.但是注意上述代码都是建立在sleep的基础上.如果没有sleep代码可能会死锁,也可能不死锁,因为线程是抢占式执行,谁先抢到谁先执行,结果是未知的.

还有就是死锁的两个基本特征,一个是互斥性和不可抢占.

  • 互斥确保了同一时刻只有一个线程可以访问共享资源,从而避免数据竞争。
  • 不可抢占确保了持有锁的线程可以安全地完成其操作,而不会被其他线程中断或抢占。

volatile 关键字

volatile关键字是用来解决内存可见性的问题,我们在多线程中除了死锁问题,还有就是内存可见性的问题,这不是程序员逻辑问题导致出错的,而是JVM的问题,所以Java提供volatile关键字来解决问题,先看有问题的代码

package thread_learn2;

import java.util.Scanner;

public class demon7 {
    private static int n=0;
    public static void main(String[] args) {

        Thread thread=new Thread(()->{
           while(n==0){

           }
        });
        Thread thread1=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数:");
            n=scanner.nextInt();
        });
        thread.start();
        thread1.start();
    }
}

执行结果如下:多线程(基础知识)_第3张图片

发现程序并没有终止,这就是内存可见性的问题.那么为什么会有这种问题呢.原因如下:

线程thread一直在循环并没有打印,,首先要进行条件判断,就是n是否等于0.要进行判断,就要先把数据从内存读到寄存器中,然后在寄存器中比较.然而这两个过程速度差距非常大,相差几个数量级,也就是说在线程thread1中对n的更改,要经过这两个步骤,但是在JVM看来,几万次比较后的结果都是0,并且过程一相对于过程二开销比较大,所以JVM直接将过程一直接优化掉了,也就导致,就算我们去更改n的值程序也不能停下来.
关于JVM优化这件事,其实就是提高代码的执行效率,因为每个程序员写的代码不一样,有的运行速度慢,但是当JVM优化后,运行速度就差不多了.

还有就是JVM优化在单线程中是非常准确的不会出现内存可见性问题.但是多线程会出现.

还有就是JVM这里的优化和之前C语言Debug版本的Release版本不同,Dubge版本编译的时候,将中间的符号表也编译到exe文件中了,Release版本没有.而C语言的优化是要靠指令来完成,
-O0是不优化
-O3是优化最高级.

然后就是解决上面的问题,我们可以直接sleep,让线程thread休眠一下,这时候n就能改变值,但是这个方法很不好,程序运行速率会下降很多,所以就用上面说的关键字volatile关键字,.看下面代码

package thread_learn2;

import java.util.Scanner;

public class demon7 {
    private volatile static int n=0;
    public static void main(String[] args) {

        Thread thread=new Thread(()->{
           while(n==0){

           }
        });
        Thread thread1=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数:");
            n=scanner.nextInt();
        });
        thread.start();
        thread1.start();
    }
}

多线程(基础知识)_第4张图片

看运行结果,程序是没有错的.这也是volatile关键字的用法.

还有就是什么时候要加volatile这个关键字们就是在一个变量被一个线程读,一个线程写中情况.

下面我们还来谈内存可见性,按网上资料的说法,引入了两个概念
(1)工作内存
(2)主内存

整个Java程序都持有主内存,每个Java线程都会有一份工作内存.
就拿上面的代码来举例,变量n就在主内存中,当两个线程执行的时候,会将变量n加载到工作内存中,线程thread1修改了n,就会将n再写道主内存中.线程thread会将n从主内存读取到工作内存中,然后依照工作内存中n的值来进行判定的,而此时线程thread1修改了主内存n的值,但是,还是依据线程thread中的工作内存来进行判定的.也就出现了内存可见性问题.
我们此时类比一下,这里的主内存不就是内存吗,工作内存不就是cpu寄存器和cache吗,这其实就是换了个说法.站的角度不同.

好了关于内存可见性问题就说到这里,最后一点,volatile不能解决原子性问题,只能解决内存可见性问题.

wait和notify

上面其实已经提到了wait和notify这个方法,这里我们仔细来讨论一下.
wait是等待,notify是通知,上面我们说是唤醒,还是通知比较合适.
我们拿一个例子,来说一下这两个方法怎么用.
去ATM取钱,一群人去ATM取钱,第一个人进去之后,发现ATM没钱了,他前脚刚出去,门还没关上,要不我再去看看吧,重复这个过程.其他的人也进不去,这种现象叫做 "线程饿死".解决方案也很简单,就是让这个人去等待,等通知有钱了,再去通知他,让他去取钱,其他人也可以进ATM机,这就解决了.
所谓线程饿死这种现象,是概率问题,和调度器的策略相关.就好比上面的例子,程序不会一直重复这个过程,但是重复个几百次还是有的.
值得注意的是wait和notify是Object提供的方法,任意的object对象都可以用来wait,notify

好那么我们先简单使用一下wait这个方法看下面代码
 

package thread_learn2;

public class demon8 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait 之前");
        object.wait();
        System.out.println("wait 之后");
    }
}

运行结果如下

报错了,这是为什么呢
IllegalMonitorStateException  Illegal是非法的意思,Monitor是监视器的意思,但是这里的意思是锁的意思,是synchronized这个锁.State是状态的意思,Exception是异常的意思.连起来就是非法的锁状态.
这里object没有进行加锁.总的来说,wait会进行一个操作,就是进行解锁.所以使用wait要放在synchronized代码块里面.所以要先加锁,才能用wait这个方法.

只要改一下代码就行了,看下面代码

package thread_learn2;

public class demon8 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait 之前");
        synchronized (object){
            object.wait();
        }
        
        System.out.println("wait 之后");
    }
}

上述代码就是正确的代码.

还有wait这个方法,就是解锁和等待是同时的(打包成原子的).为什么要是打包成原子的呢,如果不是同时的可能会发生线程切换.这就可能导致线程不能及时被唤醒.

这里总结一下wait主要做的三件事:
(1)释放锁
(2)进入阻塞状态,准备接受通知
(3)接受到通知后,唤醒,并尝试获取锁.

还有就是,必须是同一个锁对象,才能被唤醒

notify也是这样,必须是同一个锁对象才能进行通知,同时notify也要确保先加锁,才能执行.

wait默认是死等,也是有参数的,这就和sleep就很相似.当wait等待时间到了,就不再进行等待,会尝试去获取锁,获取锁之后会执行下面的代码,出了synchronized代码块之后释放锁.
我们做一个练习,利用多线程来顺序打印ABC.代码如下:

package thread_learn2;

public class demon9 {
    static Object  object1=new Object();
   static Object object2=new Object();
    public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println("A");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object1){
                object1.notify();
            }
        });
        Thread thread1=new Thread(()->{
            synchronized (object1){
                try {
                    object1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("B");
            synchronized (object2){
                object2.notify();
            }


        });
        Thread thread2 =new Thread(()->{
            synchronized (object2){
                try {
                    object2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        thread.start();
        thread1.start();
        thread2.start();
    }
}

你可能感兴趣的:(java,jvm,开发语言)