多线程(基础知识)

目录

并发和并行

多线程的实现方式

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

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<Integer> future=new FutureTask<>(myCallable);
        //Thread thread=new Thread((Runnable) future);
        FutureTask<Integer> 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<Integer> {
    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()方法

就是强线程的执行权.

线程的生命周期

看下面这个图

线程的安全问题

一般线程安全

以买票问题为例子,有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;
            }
        }
    }
}

执行结果如下

我们就发现不对劲,三个窗口同时在卖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();
    }
}

执行结果如下:

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

线程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();
    }
}

看运行结果,程序是没有错的.这也是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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值