初识JAVA多线程

目录

一. 多线程简述

二. 多线程注意事项

三. 多线程代码简单实现

         1.五种创建多线程的方法

四. Thread 类及常见方法(一部分介绍和使用)

1. 线程终止

 2.程序员决定如何让中断

3. 等待一个线程 ( join 控制线程结束的顺序)

 4.获取当前线程 

5.休眠线程

五.线程状态

1.观察线程的所有状态

六. 多线程安全问题

1.什么情况(常见的)会出现线程安全问题 

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

  2.1 synchronized 的使用(加锁)

2.2 加锁监视器 monitor lock

2.3  可重入

2.4 Java标准库中的线程安全类

3.内存可见性问题

4.volatile (解决内存可见性问题)

5. wait 和 notify (协调多个线程之间的执行先后顺序)


一. 多线程简述

        1. 随cpu的进入多核时代,为提高程序的执行速度,就要充分利用cpu的多核资源,也就是是实现多进程。但多进程消耗资源大,速度慢(创建,销毁,调度的开销都挺大,即多进程对“资源的分配/回收”消耗过大)。所以,多线程(轻量级进程)也就应运而生。多线程将“资源的分配/回收”省略掉,复用前有的资源,即共用。

二. 多线程注意事项

        1.一个进程包含一个或多个线程(注:一个线程不能属于多个进程)

        2.同一个进程的多个线程之间,共用同一份资源(主要指 内存 和 文件)

        3.操作系统 实际调度(调度资源)是时,是以线程为单位调度。(每个线程在cpu上都是独立调度)

        4.进程是操作系统分配资源的基本单位

        5.设置线程数量时,因cpu核心数量有限,线程过多,不仅不能提高效率,反而浪费开销在线程的调度上,设置线程数量时应根据需求,合理设置。

        6.多线程安全问题:(下面有具体介绍)

        7.若某一个线程抛出异常,处理不善,很可能会影响它所属于的整个进程。(原因:多线程共用同一份资源。 对比:多进程因资源独立,则 不容易 触发这个问题)

三. 多线程代码简单实现

     1.Java多线程核心操作的类(Thread)

     2.五种创建多线的方式 

        继承 Thread 类,重写run方法

        使用 Runnable接口,创建多线程

        使用匿名内部类实现创建多线程

        使用匿名内部类,实现Runnable方法

        使用Lambda表达式 (最简单,推荐)

         1.五种创建多线程的方法

                   1.1继承 Thread 类,重写run方法

package thread;
//继承 Thread 类,重写run方法

class MyThread extends Thread {         //使线程成为独立的执行流
    
    @Override
    public void run() {
        while(true)      //若没有多线程,程序在此就陷入死循环,只有一个打印。
        {                //有多线程,就会和下面的打印交替出现
            System.out.println("hello world");
            try {
                Thread.sleep(1000);  //休眠1s
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo1 {
    //Thread类在java.lang下,不用手动导入包
    public static void main(String[] args) {
        Thread t= new MyThread();
        t.start();      //多线程的一个特殊方法,作用:创建一个新线程,新线程执行t.run
                        //主线程调用t.start()方法,创建出一个新线程负责run方法,并且当ruan执行完毕时,新线程自然销毁
        // t.run()  若直接调运,则等价于单线程
        while(true)
        {
            System.out.println("hello 123456");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                1.2.使用 Runnable接口,创建多线程 

package thread;
//使用Runnable接口创建多线程
//Runnnable:描述一个需要执行的任务,run方法是实现任务的方式。

class MyRunnale implements Runnable{
    @Override
    public void run() {
        System.out.println("hell word");
    }
}
public class ThreadDemo2 {

    public static void main(String[] args) {
        Runnable runnable =new MyRunnale();
        //任务描述
        Thread t=new Thread(runnable);
        t.start();
        //将任务交给一个新线程执行
    }
}
//该方法实现了代码的解耦合,使线程与线程之间所需执行的任务分开
//方便与后续改动代码

                1.3. 使用匿名内部类实现创建多线程

package thread;
//使用匿名内部类实现创建多线程

public class ThreadDemo3 {

    public static void main(String[] args) {
        Thread t = new Thread(){
            public void run(){
                System.out.println("hello word");
            }
        };
        t.start(); //创建新线程并执行run方法
    }
}

               1. 4.使用匿名内部类,实现Runnable方法 

package thread;
//使用匿名内部类,实现Runnable方法

public class ThreadDEmo4 {
    public static void main(String[] args) {
        Thread t= new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello word");
            }
        });
        t.start();
    }
}

                1.5.使用Lambda表达式 (最简单,推荐) 

package thread;
//使用Lambda表达式,最简单,推荐

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            System.out.println("hello word");
        });
        t.start();
    }

}

四. Thread 类及常见方法(一部分介绍和使用)

构造方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnablle 对象创建线程对象
Thread(String name)创建线程对象,并命名        
Thread(Runnable target , String name)使用Runnablle 对象创建线程对象,并命名   

 
属性获取方法备注
IDgetif()获取线程的身份标识
名称getName()获取构造方法中对线程的命名(与上面对应)
状态getSate()        获取线程状态(java线程状态箱较操作系统更丰富,这里不详述)
优先级getPriority()        可获取,也可设置(设置效果影响不大,前文有描述)
是否守护线程isDameon()        守护线程(不管它是否结束)不会阻止进程结束,非守护线程(未结束)会阻止进程结束。我们手动创建的线程,默认为非守护线程
是否存活isAlive()

        首先在调用 start() 方法之后,系统才会在内核中创建一个 pcb ,这时 pcb 才代表一个真正的线程。所以,在调用之前,  isAlive() 是f alse , 调用之后才是 true 。是判断线程是否真的存在。

        其次,当线程中的 run() 执行结束,此时线程销毁,pcb随之销毁。这时,isAlive() 也是 false

package thread;

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            System.out.println("hello word");
        });
        t.start();

        t.getId();           //ID
        t.getName();         //名称
        t.getState();        //状态
        t.getPriority();     //优先级
        t.isDaemon();        //是否守护线程
        t.isAlive();         //是否存活
        t.isInterrupted();   //是否中断
    }

}

1. 线程终止

注意!!! 

        中断一个线程,不是让线程立即停止,而是 ‘通知’ 线程应该停止,至于是否真的停止,取决于线程的代码如何实现的(三种情况:不中断,等一会中断,立即中断)

        1.1 通过标志位中断

public class ThreadDemo {

    private static boolean flage=true;     //定义一个静态变量 flage 作为标志位

    public static void main(String[] args) {
        Thread t= new Thread(()->{
            while(flage){       //标志位
                System.out.println("hello word");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });
        t.start();
        flage = false;    //主线程main 可随时通过修改 flage 的值来做到中断线程 t

//注意!!!这里之所以做到中断线程,完全是由于线程内部代码的设计
    }

}

        1.2调用 interrupt() 方法

public class ThreadDemo5 {
//此代码为 调用后,但不中断的情况。具体解释见下文
 
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){     //Thread.currentThread().isInterrupted()  Thread自带的标志位
                System.out.println("hello word");               //Thread.currentThread() Thread 的一个静态方法,作用是获取当前线程
                try {                                           //那个线程调用这个方法,它就代表那个线程,类似于 .this
                    Thread.sleep(1000);                         //isInterrupted() 若它为true 表示被终止
                } catch (InterruptedException e) {              //相反则表示未被终止。
                    throw new RuntimeException(e);
                }
            }

        });
        t.start();
        t.interrupt(); //在主线程main中 t 线程调用interrupt()这个方法后,t 线程被终止

    }

}

 interrupt 还有一个功能,在上述代码中,如果t 线程在sleep中休眠中,此时调用interrupt(),会通过触发sleep内部的异常,从而提前唤醒线程。

注意!!!  如果运行上述代码,会发现 t 被中断了,但依旧会持续输出 hell word ?

        解析:

       调用 interrupt() 方法后会做两件事。(1.将标志位的值 置为true。2.触发sleep的异常,唤醒线程。)

        但是,当sleep的内部异常被触发后,线程被唤醒,但标志位却被sleep清除,也就是标志位的值再次被置为 false。 所以就会while循环会继续执行。线程未中断 (这里,也就再次说明了,中断线程,只是通知它应该中断了,是否真的中断,取决于代码的设计)

        此时,sleep清除标志位,就让线程何时中断可由程序员控制三种情况,不中断,等一会中断,立即中断)。

 2.程序员决定如何让中断

标志位被sleep清除,就让程序员对线程的中断有了可操作性,或者叫可选择性。从上文可知线程中断可分为(三种情况,不中断,等一会中断,立即中断,而sleep的清除标志位,让我们可以根据需求,选择对应的中断情况  

        1.1.不中断

                上一个代码

        1.2 立即中断

public class ThreadDemo5 {

    public static void main(String[] args) {
        Thread t= new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello word");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break; //跳出循环。
                }
            }
        });
        t.start();
        t.interrupt(); //在主线程main中 t 线程调用interrupt()这个方法后,t 线程被终止
    }

        1.3 稍后中断

public class ThreadDemo5 {

    public static void main(String[] args) {
        Thread t= new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello word");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //在第一个sleep触发异常后,在添加一个sleep.就是等待一会,在执行break;
                    //在这个catch下,可以写任何代码
                    e.printStackTrace();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException ex) {
                        e.printStackTrace();
                    }
                    break;
                }
            }
        });
        t.start();
        t.interrupt(); //在主线程main中 t 线程调用interrupt()这个方法后,t 线程被终止

}

3. 等待一个线程 ( join 控制线程结束的顺序)

        以下面代码为例,本身在调用 t.start 后,主线程main和 t 线程并发执行。但在调用 t.join后 会让 t 线程执行完毕,在执行主线程main。 这也叫阻塞(block)

 Join方法的两种(无参数,有参数)

        1. public void join(); 这个方法,会一直等待线程结束

        2.public void join(long minllis); 这个方法,有一个最大等待时间的参数。到时间就不等

public class ThreadDemo5 {

    public static void main(String[] args) {
        Thread t= new Thread(()->{
            for(int i=0;i<10;i++)
            {
                System.out.println("t线程在执行"); //输出
            }
        });
        t.start();
        System.out.println("join 开始阻塞");
        try {
            t.join();  //阻塞
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("t 执行完毕");
    }

}

 4.获取当前线程 

方法:public static  Thread currendThread();

说明:返回当前线程的引用对象(谁调用,返回谁的实例)

用法:类名.currendThread

5.休眠线程

方法:public static void sleep(long millis); (参数:/毫秒)

说明:本质上是让线程不参与调度,休眠时间到达之后,才参加调度

用法:类名.sleep(millis);

五.线程状态

1.观察线程的所有状态

1.状态是针对 调度 描述的,而线程是调度的基本单位。

2.java对线程状态的描述,进行了细化

      (1).NEW:创建了Thread 对象,但是还未调用 start

      (2).TERMINATED:  线程执行完(内核中pcb执行完毕),但是Thread对象还存在

      (3).RUNNABLE: 可运行状态(a.正在cpu上运行   b.准备就绪,随时可以去cpu上运行)

      (4).WAITING:  阻塞状态                          <——

      (5).TIMED_WAITING:  阻塞状态                       |   都表示阻塞,但阻塞原因不同和        

      (6).BLOCKED: 阻塞状态                         <——

3.方法:Thread.State.valuse()(线程状态是一个 枚举 类型)

六. 多线程安全问题

 为什么会有多线程安全问题?万恶之源--->抢占式执行,带来的随机性

     抢占式执行:(在操作系统调用多线程 时,会“抢占式执行”,具体那一个线程先调用,是不确定的,取决于操作系统调度器的具体实现策略。代码的执行顺序固定,结果固定。但线程的调度充满随机性了,那代码执行顺序随机了,结果也就从固定,变成多种可能性)

        

        注:从代码角度来看,线程之间的调度是 “随机” 的,但这里的随机是:内核本身是非随机的,但因为干预因素太多,并且应用程序无法感知这一细节,所以造成 “随机”的假象。(多线程的执行顺序又是由内核实现的,无解。可以通过api进行一些 有限 干预)

1.什么情况(常见的)会出现线程安全问题 

        1.1【根本原因】抢占式执行,随机调度(这个原因,目前无能无力)

        1.2 代码结构:避免多个线程修改同一个变量

        1.3 修改操作的对象是非元原子性的(原子性:单个指令,无法拆分。)

        1.4 内存可见性问题(一个线程读,一个线程修改,也可能会出现问题)                 

        1.5 指令重排序(本质上是编译器在自动优化代码的时候,在逻辑上不变的前提下会对单个线程内 代码的执行顺序进行调整) 

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

  2.1 synchronized 的使用(加锁)

假如 线程1 和线程2 修改同一个 非原子性变量 ,且加锁(线程对 某一个对象加锁对象为同一个发生锁竞争。那么,当线程1 先加锁到这个对象(也可以理解为,先调用到这个对象),那么线程2 就会处于 阻塞状态,直到线程1 的加锁对象 执行完毕。

      

synchronized 的使用方法(明确加锁对象):

        (1)修饰方法        (注意:加锁对象不是被修饰的方法,而是所属的对象

                a.修饰普通方法  

                b.修饰静态方法     (加锁对象是 类)

        (2) 修饰代码块         (手动指定加锁对象)

synchronized 修饰方法 

class Counter{
  public int count=0;
    public synchronized void add()  // synchronized: 对 Counter这个对象 加锁
    {                               //注意!!!! 不是对方法加锁
        count++;  // ++操作,本质上分为三步(内存->寄存器; 寄存器+1; 寄存器->内存)
    }             // 所以,++操作为 非原子性!!!!
}

public class ThreadDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{        //线程1: t1
            for(int i=0;i<10000;i++)
            {
                counter.add();
            }
        });

        Thread t2 = new Thread(()->{        //线程t2: t2
            for(int j=0;j<10000;j++)
            {
                counter.add();
            }
        });

        t1.start();   //创建t1线程,执行。
        t2.start();   //创建t2线程,执行。

        try {
            t1.join();  //等待线程
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(counter.count);
    }
}

 synchronized 修饰方法 

class Counter{
  public int count=0;
    public void add()  
    {                               
       synchronized(this){    //synchronized 修饰代码块,this 这的对象可自定义。
             count++;         //进入代码块加锁,出代码块解锁
        }
} 

public class ThreadDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{        //线程1: t1
            for(int i=0;i<10000;i++)
            {
                counter.add();
            }
        });

        Thread t2 = new Thread(()->{        //线程t2: t2
            for(int j=0;j<10000;j++)
            {
                counter.add();
            }
        });

        t1.start();   //创建t1线程,执行。
        t2.start();   //创建t2线程,执行。

        try {
            t1.join();  //等待线程
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(counter.count);
    }
}

2.2 加锁监视器 monitor lock

       jvm 给synchronized 起的另外一个名字。加锁和解锁是两个操作,所以有可能出现某一个线程加锁后,忘记解锁,那么和其 发生锁冲突 的线程就一直处于阻塞状态,所以为避免出现这种状况,synchronized就基于代码块的形式,解决了这一情况

2.3  可重入

        一个线程对同一个对象,连续进行两次加锁。如果没有问题,就叫可重入。发生错误,就叫不可重入

class Counter{
  public int count=0;
    public synchronized void add()  //对 Counter 加锁,线程调用add,第一次加锁
    {                               
       synchronized(this){    //synchronized 修饰代码块,this 指向Counter对象。
             count++;         //进入代码块,第二次加锁
        }
} 

public class ThreadDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{        //线程1: t1
            for(int i=0;i<10000;i++)
            {
                counter.add();
            }
        });


        t1.start();   //创建t1线程,执行。

        try {
            t1.join();  //等待线程

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(counter.count);
    }

         从被加锁的对象(this)角度来看,线程第一次调用加锁后。进入方法,遇到代码块,进行第二次加锁。此时 该对象认为自己被线程占用,第二此加锁,是否需要进入阻塞状态,等待?

        此时,又是一个特殊的情况,两次调用同为一个线程,那么是否允许这样操作呢???

如果允许,就是可重入;不允许,就是不可重入,线程陷入 '死锁' 状态

 结论 :

但在java语言中,不可避免出现上述写法。为了避免出现死锁的情况,java,对 synchronized 设定是可重入的

2.4 Java标准库中的线程安全类

        多线程调用同一个集合类,也需要考虑到线程安全问题 

  • 内置synchronized 加锁的,相对来说安全(不是绝对)

        Vector(不推荐使用)

        HashTable(不推荐使用)

        ConcurrentHasMap

        StringBuffer

  • 没有内置synchronized 加锁的,多线程使用时需多加注意!遇到线程安全问题,需要手动加锁

        ArrayList

        LinkedList

        HashMap

        TreeMap

        HashSet

        TreeSet

        StringBuilder

  • 特殊的一个类:String 类

        String 没有内置加锁,但是线程安全的。因为它是不可修改对象。多线程对String读取,没有线程安全问题

3.内存可见性问题

        内存可见性问题:假设线程A对一个变量进行读操作。同时线程B对变量进行修改操作。此时,线程A读取到变量的值,不一定是线程修改后的值。

        举例:

class MyCounter{
    public int flage=0;
}
public class Test {
    public static void main(String[] args) {
        MyCounter my = new MyCounter();

        Thread A =new Thread(()->{
           while (my.flage==0){                 //线程A 只进行了对flage的读操作
               ;
           }
            System.out.println("线程A结束运行");
        });

        Thread B =new Thread(()->{            //线程B 对flage进行修改
            Scanner s =new Scanner(System.in);
            System.out.println("请输入一个整数:");
            my.flage=s.nextInt();
        });
                    //代码预期: 线程A,B,并发执行,所以一旦线程B对flage进行修改为非0,线程A结束运行
        A.start();
        B.start();
    }
}

代码预期功能:线程A,B,并发执行,所以一旦线程B对flage进行修改为非0,线程A结束运行,输出 “线程A结束运行”

实际效果:

         输入5之后,程序并未停止运行。

 分析:为什么程序未达到预期效果,出现BUG

         1. 上述程序,从汇编来看,针对线程A的 while()循环, 分为两个操作

                  load:   将flage的值,从内存中读取到寄存器中,并进行判断

                  cmp:    将寄存器的值,与0比较。根据比较结果,判断程序下一步执行方向

        2. 上述循环,执行速度非常非常快。一方面,在线程B真正修改 flage的值之前,循环已执行很多次。另一方面,load 操作相比 cmp 操作 慢非常非常多!!!,且load的值每次读取结果一样。

        3. 所以,在第 2条的基础上。编译器对程序做出了优化,即JVM,不再真正重复 load 操作,假设 flage 是没有程序对它进行修改的操作。只进行读取一次。

        4.实际上,是由其他线程(线程B)对它进行修改操作。而 JVM 对于这种(对同一个变量,一个线程读操作,一个线程修改操作)是会存在判定误差的!所以就出现BUG。

        5. 结果:面对编译器JVM优化的,“好心办坏事”,需要程序员进行手动干预——volatile

4.volatile (解决内存可见性问题)

        对于会出现 “内存可见性,线程不安全” 的变量(例如上述 flage)加上volatile 关键字,进行修饰。告诉编译器,这个变量是 “易变的” ,每一次都要重新读取变量的值。

        volatile只能修饰成员变量(局部变量的生命周期定义,天然就规避了线程安全问题)

        回到程序:可见程序达到预期效果。

  注意 !!!!:

        类似于上述编译器优化出现的问题,并不是始终都会出现的。例如,上述的代码,即使 flage 没有 volatile 进行修饰。但如果对线程A,在while()循环里,添加了 sleep 操作(休眠线程),控制了循环速度,那么,就可以避免内存可见性问题。

        但是,编译器的优化,无法从代码层面感知,所以,建议,对有可能出现线程安全问题的 变量,应该都加上 volatile 修饰。

举例:代码达到预期效果

        注意:volatile不保证原子性,它只针对一个线程读,一个线程修改的并发执行带来的线程安全问题。并不能解决两个线程对同一个变量进行修改操作的并发放执行,带来的安全问题

5. wait 和 notify (协调多个线程之间的执行先后顺序)

        由于线程之间,是抢占式执行。线程执行顺序是不可预估的。但在实际开发中,更希望合理的协调多个线程之间的执行顺序

        所以,为了完成协调工作。就有三个方法

        wait() / wait(long timeout)  让当前线程进入等待状态。

        notify () / notifyAll()    唤醒在当前对象上等待的线程。

        注:wait() , notify() , notifyAll() , 三个方法都属于Object类。

        wait 方法不带有参数,表示一直处于等待,当前线程处于阻塞状态,直到被其他线程唤醒。

        而带有参数 wait(long timeout), 表示有个最大等待时间,参数就为最大等待时间。到时间了,就不等了。

     (1) wait  操作流程       

  1.  先释放锁
  2. 进入阻塞等待状态
  3. 收到通知后,在尝试重新获取锁,,并在获取锁后,继续往下执行。

     (2) 代码演示,直观了解 

class Myclass{
    int a=0;
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Myclass myclass =new Myclass();

        Thread thread1 = new Thread(()->{
            System.out.println("线程1:使用wait之前:");

            try {
                synchronized (myclass){     //给Myclass这个对象加锁。
                    myclass.wait();         //给Myclass这个对象释放锁,但让线程进入阻塞状态。
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1:使用wait之后");
        });

        Thread thread2 = new Thread(()->{
            System.out.println("线程2:使用notify之前");

                synchronized (myclass){     //因为,notify务必先获取锁,才能进行通知
                myclass.notify();           //而 线程1,已经对Myclass 这个对象释放锁了,所以再次加锁
            }
            System.out.println("线程2:使用notify之后");
        });

        thread1.start();
        Thread.sleep(500);
        thread2.start();
    }

        运行结果

         注意!!!

        (3)总结:

1,上述代码执行过程:

                线程1执行,调用 wait ------->线程1阻塞------>线程2执行,调用notify,唤醒线程1------>线程1继续执行。

 2,wait() 无参数版,有缺陷。如果没有notify唤醒,wait 将会一直死等下去。相应的线程会一直陷入阻塞状态。

 3,wait(long timeout)  有参数版 与sleep 功能貌似很像,但本质上有区别:

                wait 和 sleep 都可被提前唤醒,但wait是被 notify 唤醒,属于程序的正常执行。但sleep被提前唤醒,是因为触发了异常处理,说明程序出现异常

 4,如果有多组线程处于等待状态(wait), notify唤醒线程时,不能指定唤醒,只能随机唤醒某一线程

       如果,要处理多组线程,可以将其分组确定顺序——例如:

                假设有:线程1,线程2,线程3

                需求:执行顺序:1,3,2

                分组:(1,3),(3,2)这样就可以使用配套的wait 和 notify 先确定第一组的顺序,在确定第二组顺序。达到需求。

        (4) notifyAll()

                        notifyAll() 和 notify 的区别是,在多个线程时,前者只唤醒一个线程。而后者是唤醒所有等待的线程,然后被唤醒的线程在一起竞争锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
多线程可以通过以下三种方式创建: 1. 实现Runnable接口,并实现run()方法。首先,自定义一个类并实现Runnable接口,然后在该类中实现run()方法。接下来,创建Thread对象,并将实现了Runnable接口的对象作为参数实例化该Thread对象。最后,调用Thread的start()方法来启动线程。这种方式的代码示例可以参考引用\[1\]中的代码。 2. 继承Thread类,重写run()方法。Thread类本质上也是实现了Runnable接口的一个实例,它代表了一个线程的实例。通过继承Thread类并重写run()方法,然后调用start()方法来启动线程。这种方式的代码示例可以参考引用\[2\]中的代码。 3. 使用匿名内部类实现多线程。可以直接在创建Thread对象时使用匿名内部类来实现多线程。这种方式可以简化代码,但只能使用一次。具体的代码示例可以参考引用\[3\]中的代码。 总结起来,多线程的创建方式有实现Runnable接口、继承Thread类和使用匿名内部类。每种方式都有其适用的场景,可以根据具体需求选择合适的方式来创建多线程。 #### 引用[.reference_title] - *1* *2* [初识多线程——创建多线程的三种方式](https://blog.csdn.net/pizssn/article/details/106382025)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [多线程创建的三种方式](https://blog.csdn.net/qq_31780525/article/details/54338341)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值