【JAVA入门】Day47 - 线程

news/2024/9/20 7:23:57 标签: java, 开发语言

【JAVA入门】Day47 - 线程


文章目录

  • 【JAVA入门】Day47 - 线程
    • 一、并发和并行
    • 二、多线程的实现方式
      • 2.1 继承 Thread 类的方式
      • 2.2 实现 Runnable 接口的方式
      • 2.3 利用 Callable 接口实现
    • 三、Thread 类中常见的成员方法
    • 四、线程的调度和优先级
      • 4.1 抢占式调度
      • 4.2 优先级
      • 4.3 守护线程
      • 4.4 出让线程 / 礼让线程
      • 4.5 插入线程 / 插队线程
    • 五、线程的生命周期
    • 六、线程安全
      • 6.1 同步代码块
      • 6.2 同步方法
      • 6.3 Lock 锁
    • 七、死锁
    • 八、等待唤醒机制
      • 8.1 等待唤醒机制的基本实现
      • 8.2 阻塞队列实现等待唤醒机制
    • 九、线程的状态


        线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
        进程是程序的基本执行实体。一个软件运行以后,它就是一个进程。线程进程的组成部分,它是应用软件中互相独立,可以同时运行的功能。
        多线程程序的最大特点就是计算机可以在运行时在多个线程之间同时切换,最大限度运用计算机的算力,达到最大的代码利用率和运行效率。

一、并发和并行

  • 并发是指在同一时刻,有多个指令在单个CPU上交替执行。
  • 并行是指在同一时刻,有多个指令在多个CPU上同时执行。

        在计算机之中,并发和并行是很有可能同时发生的,在同一时刻,不但有多个指令在交替执行,而且它们还会在多核CPU上同时执行。

二、多线程的实现方式

        多线程在 Java 中有三种实现方式:
① 继承 Thread 类的方式进行实现。
② 实现 Runnable 接口的方式进行实现。
③ 利用 Callable 接口和 Future 接口方式实现。

2.1 继承 Thread 类的方式

        继承 Thread 类的方式代码如下:
① 自己写一个类继承 Thread 类。
② 重写 run() 方法。

java">package myThread;

public class MyThread extends Thread {
    @Override
    public void run() {
        //线程要执行的代码
        for(int i = 0; i < 100; i++) {
            System.out.println(getName() + ":HelloWord");
        }
    }
}

③ 测试类中创建子类对象,调用 start() 方法,开启线程。

java">package myThread;

public class ThreadDemo1 {
    public static void main(String[] args) {
         /*
            继承 Thread 类方法:
            1.自己定义一个类继承Thread
            2.重写run方法
            3.创建子类对象,并启动线程
          */

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //赋予名字
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.start();
        t2.start();
    }
}

2.2 实现 Runnable 接口的方式

        实现 Runnable 接口的方法:
① 写一个自定义类实现 Runnable 接口。
② 重写 run() 方法。

java">package myThread;

public class MyRun implements Runnable {
    @Override
    public void run() {
        //线程内部执行代码
        for(int i = 0; i < 100; i++){
            //获取到当前线程的对象
        /*  Thread t = Thread.currentThread();
            System.out.println(t.getName() + ":HelloWord");*/
            System.out.println(Thread.currentThread().getName() + "HelloWord!");
        }
    }
}

③ 在测试类中创建自定义对象,然后创建线程对象(将自定义对象作为参数传递给线程)。
④ 调用 run() 方法,开启线程。

java">package myThread;

public class ThreadDemo2 {
    public static void main(String[] args) {
        /*
            多线程第二种启动方式:实现 Runnable 接口
            1.自己定义一个类实现Runnable接口
            2.重写里面的run方法
            3.创建自己的类对象
            4.创建一个Thread类方法,将自己创建的对象作为参数传递过去
         */

        //创建自定义对象(已实现Runnable接口)
        MyRun mr = new MyRun();

        //创建线程对象
        //给两个线程传递同一个自定义对象,表明两个线程要执行的任务一样
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //设定线程名
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.run();
        t2.run();
    }
}

2.3 利用 Callable 接口实现

        利用实现 Callable 接口的方式实现多线程:
① 自定义一个实现类对象,实现 Callable 接口,泛型为我们想得到的线程运行结果的数据类型,重写 call() 方法。

java">package myThread;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {

    //重写call方法,求1~100的整数和
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1 ; i <= 100; i++){
            sum = sum + i;
        }
        return sum;
    }
}

② 在测试类中,创建自定义 MyCallable 对象,然后创建 FutureTask<> 对象,泛型要和重写的 call() 方法的返回值保持一致,将 MyCallable 对象作为参数传递,表示用这个 FutureTask<> 对象来管理 MyCallable 对象的执行结果。
③ 创建 Thread 对象,将 FutureTask<> 对象作为参数传递,表示用它来管理该线程,调用 start() 方法,启动线程。
④ 利用 FutureTask<> 对象的 get() 方法获取线程执行的结果。

java">package myThread;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
            多线程第三种实现方式:
            特点:可以获取到多线程运行的结果
            1.创建一个类MyCallable实现Callable接口
            2.重写call(有返回值,表示多线程运行的结果)
            3.创建MyCallable的对象(表示多线程要执行的任务)
            4.创建FutureTask的对象(管理多线程运行的结果)
            5.创建Thread类的对象并启动(表示线程)
         */

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        //创建FutureTask的对象(用来管理多线程运行的结果)
        //传递mc,表示用ft管理mc的运行结果
        FutureTask<Integer> ft = new FutureTask<>(mc);
        //创建线程对象
        //传递ft,表示该线程的运行结果会由ft来管理
        Thread t1 = new Thread(ft);

        //启动线程
        t1.start();

        //获取多线程运行结果
        Integer result = ft.get();

        System.out.println(result);

    }
}

三、Thread 类中常见的成员方法

        以下是 Thread 类中常见的成员方法。
在这里插入图片描述
        以下代码演示了 Thread 类中前四个相对简单的常见方法的使用:
        首先自定义一个对象继承Thread类。然后可以在MyThread1类中调用Thread类的构造方法进行构造方法的重载,重载后的构造方法可以将字符串传递进去,作为线程的名字。

java">package myThread;

public class MyThread1 extends Thread{
    public MyThread1() {
    }

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

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println(getName() + '@' + i);
        }
    }
}

        这里我们直接用一个Thread对象调用currentThread()方法,但是我们并没有启动这个线程,结果发现返回的线程名字是 main,这是因为JVM虚拟机启动后,会自动启动多条线程,其中有一条就叫做main线程,main线程的作用就是调用main方法,执行里面的代码,我们在以前写的所有测试类代码,其实都是运行在main这条线程上。

java">package myThread;

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        /*
            线程类中的方法
         */

        //1.返回此线程的名称
        //线程默认的名字就是 Thread-x,这里的x从0开始
/*        MyThread1 mt1 = new MyThread1();
        MyThread1 mt2 = new MyThread1();

        mt1.start();
        mt2.start(); */

        //2.设置线程名字
        //我们也可以用setName()方法设置线程的名字
        //Thread的构造方法也是可以设置名字的
        //其中一种方法重载:Thread(String name)
/*        MyThread1 mt3 = new MyThread1("我是线程");
        mt3.start();*/

        //3.获取当前线程对象
        //这里我们直接用一个Thread对象调用currentThread()方法,但是我们并没有启动这个线程,结果发现返回的线程名字是 main
        //这是因为JVM虚拟机启动后,会自动启动多条线程,其中有一条就叫做main线程
        //main线程的作用就是调用main方法,执行里面的代码
        //我们在以前写的所有测试类代码,其实都是运行在main这条线程上
/*        Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name);*/

        //4.让线程休眠指定的时间,以毫秒为单位
        //在这里Thread就是指main线程
/*        System.out.println("11111111");
        Thread.sleep(5000);
        System.out.println("22222222");*/
    }
}

四、线程的调度和优先级

4.1 抢占式调度

        线程的抢占式调度是指多个线程抢夺CPU的执行权,CPU在什么时候执行哪条线程是不确定的,执行多长时间也是不确定的,因此这种调度方式具有随机性。

4.2 优先级

        线程能否抢占CPU资源,体现在这个线程的优先级上。Java 采取的调度方式就是抢占式调度,它的线程调度也依据线程的优先级,且提供了线程优先级的内置方法。
在这里插入图片描述
        如何设置线程优先级,线程优先级体现在哪里,如下代码所示:

java">package myThread;

public class ThreadDemo4 {
    public static void main(String[] args) {
        /*
            setPriority(int newPriority)            设置线程优先级
            final int getPriority()                 获取线程的优先级
         */

        //创建线程要执行的参数对象
        MyRun2 mr = new MyRun2();

        //创建线程对象
        Thread t1 = new Thread(mr,"线程A");
        Thread t2 = new Thread(mr,"线程B");

        System.out.println(t1.getPriority());           
        System.out.println(t2.getPriority());
        System.out.println(Thread.currentThread().getPriority());
    }
}

        通过以上代码我们可以发现,创建的线程默认优先级都是5。

java">        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();

        经过我们设置完优先级后,线程 t2 的优先级远大于 t1,此时它抢占CPU的概率也大大提高,理论上 t2 会先于 t1 运行完毕。
在这里插入图片描述
        但实际上,这里的优先级大小只是概率提升,并不意味着绝对,实际上还是有可能发生 t1 先运行完毕的情况,这就是 Java 抢占式调度的特点。

4.3 守护线程

        守护线程的执行逻辑是:当非守护线程执行完毕之后,守护线程会陆续迅速地结束。
        守护线程就好比非守护线程的“保镖”,如果非守护线程都结束了,“保镖”也就没有存在的必要了。
        看下面的例子。

我们定义了两个不同的线程,一个执行输出1到10,一个执行输出1到100。两个线程同时运行的话,一定是MyThread1先结束。

java">package a06Thread3;

public class MyThread1 extends Thread{

    public MyThread1() {
    }

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

    @Override
    public void run() {
        for(int i = 1; i <= 10; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
java">package a06Thread3;

public class MyThread2 extends Thread{
    public MyThread2() {
    }

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

    @Override
    public void run() {
        for(int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}

在测试类中,我们把MyThread2设置为守护线程,然后先后启动MyThread1和MyThread2。

java">package a06Thread3;

public class ThreadDemo {
    public static void main(String[] args) {
        /*
            final void setDaemon(boolean on) 设置为守护线程
            当其他的非守护线程执行完毕后,守护线程会陆续结束
         */

        MyThread1 t1 = new MyThread1("被守护者");
        MyThread2 t2 = new MyThread2("守护者");

        //把第二个线程设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start();
    }
}

根据下面的控制台输出语句我们可以看出,非守护线程结束以后,守护线程陆陆续续迅速结束了。

守护者@1
被守护者@1
守护者@2
被守护者@2
守护者@3
被守护者@3
守护者@4
被守护者@4
守护者@5
守护者@6
被守护者@5
守护者@7
被守护者@6
被守护者@7
守护者@8
被守护者@8
守护者@9
被守护者@9
被守护者@10
守护者@10
守护者@11
守护者@12
守护者@13
守护者@14
守护者@15

        守护线程在 Java 开发中有一定的应用场景,比如:Java 的垃圾回收线程就是特殊的守护线程。

4.4 出让线程 / 礼让线程

        通过 yield 方法可以把当前线程编程一个礼让线程,在执行时尽可能出让自己的CPU执行权。

java">package a06Thread2;

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

        //礼让当前CPU的执行权
        Thread.yield();
    }
}

        当两个进程相互交叉运行时,如果相互出让执行权,运行的结果就会变得相对均匀。

java">package a06Thread2;

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

        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();

        mt1.setName("线程A");
        mt2.setName("线程B");

        mt1.start();
        mt2.start();
    }
}

线程B@1 线程A@1 线程B@2 线程A@2 线程B@3 线程A@3 线程B@4 线程A@4 线程A@5 线程B@5 线程A@6
线程B@6 线程A@7 线程B@7 线程A@8 线程B@8 线程A@9 线程B@9 线程B@10 线程A@10 线程B@11 线程A@11
线程B@12 线程B@13 线程A@12 线程B@14 线程A@13 线程B@15 线程A@14 线程B@16 线程A@15 线程B@17
线程B@18 线程A@16 线程B@19 线程B@20 线程A@17 线程B@21 线程A@18 线程B@22 线程A@19 线程A@20
线程A@21 线程A@22 线程A@23 线程A@24 线程A@25 线程A@26 线程A@27 线程A@28 线程A@29 线程B@23
线程B@24 线程A@30 线程B@25 线程A@31 线程B@26 线程A@32 线程B@27 线程A@33 线程B@28 线程A@34
线程B@29 线程B@30 线程A@35 线程B@31 线程A@36 线程A@37 线程A@38 线程A@39 线程B@32 线程A@40
线程B@33 线程A@41 线程B@34 线程B@35 线程B@36 线程A@42 线程B@37 线程B@38 线程A@43 线程A@44
线程B@39 线程A@45 线程B@40 线程A@46 线程A@47 线程B@41 线程A@48 线程B@42 线程A@49 线程B@43
线程A@50 线程B@44 线程A@51 线程A@52 线程A@53 线程A@54 线程B@45 线程A@55 线程B@46 线程B@47
线程A@56 线程B@48 线程B@49 线程B@50 线程A@57 线程B@51 线程A@58 线程B@52 线程A@59 线程A@60
线程B@53 线程A@61 线程B@54 线程B@55 线程B@56 线程B@57 线程B@58 线程B@59 线程B@60 线程A@62
线程B@61 线程B@62 线程B@63 线程B@64 线程A@63 线程A@64 线程B@65 线程A@65 线程B@66 线程B@67
线程A@66 线程B@68 线程A@67 线程B@69 线程B@70 线程A@68 线程B@71 线程B@72 线程A@69 线程A@70
线程B@73 线程B@74 线程A@71 线程A@72 线程A@73 线程A@74 线程A@75 线程A@76 线程B@75 线程A@77
线程A@78 线程A@79 线程B@76 线程A@80 线程B@77 线程A@81 线程B@78 线程B@79 线程A@82 线程B@80
线程B@81 线程B@82 线程A@83 线程B@83 线程A@84 线程B@84 线程B@85 线程A@85 线程B@86 线程A@86
线程B@87 线程B@88 线程A@87 线程B@89 线程B@90 线程B@91 线程A@88 线程B@92 线程B@93 线程A@89
线程B@94 线程B@95 线程A@90 线程A@91 线程A@92 线程B@96 线程B@97 线程A@93 线程A@94 线程B@98
线程B@99 线程A@95 线程B@100 线程A@96 线程A@97 线程A@98 线程A@99 线程A@100

        但是我们一定要注意:即使出让了自己的执行权的线程,还是有可能抢夺到CPU,因此这个出让只是相对的。

4.5 插入线程 / 插队线程

        当我们将一个线程定义为插入线程时,它可以插队在当前执行代码的线程前优先执行。
        下面我们定义了一个输出0~99的线程 t 。

java">package a06Thread1;

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

        然后我们在 main 方法下启动该线程,同时在 main 线程中运行一个输出 0~10 的循环代码。

java">package a06Thread1;

public class ThreadDemo {
    /*
        插入线程 / 插队线程
     */
    public static void main(String[] args) throws InterruptedException {

        MyThread t = new MyThread();
        t.setName("插队线程");
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main线程@" + i);
        }
    }
}

执行结果:

main线程@0 main线程@1 main线程@2 main线程@3 main线程@4 main线程@5 main线程@6
main线程@7 main线程@8 main线程@9 t线程@0 t线程@1 t线程@2 t线程@3 t线程@4 t线程@5 t线程@6
t线程@7 t线程@8 t线程@9 t线程@10 t线程@11 t线程@12 t线程@13 t线程@14 t线程@15 t线程@16
t线程@17 t线程@18 t线程@19 t线程@20 t线程@21 t线程@22 t线程@23 t线程@24 t线程@25 t线程@26
t线程@27 t线程@28 t线程@29 t线程@30 t线程@31 t线程@32 t线程@33 t线程@34 t线程@35 t线程@36
t线程@37 t线程@38 t线程@39 t线程@40 t线程@41 t线程@42 t线程@43 t线程@44 t线程@45 t线程@46
t线程@47 t线程@48 t线程@49 t线程@50 t线程@51 t线程@52 t线程@53 t线程@54 t线程@55 t线程@56
t线程@57 t线程@58 t线程@59 t线程@60 t线程@61 t线程@62 t线程@63 t线程@64 t线程@65 t线程@66
t线程@67 t线程@68 t线程@69 t线程@70 t线程@71 t线程@72 t线程@73 t线程@74 t线程@75 t线程@76
t线程@77 t线程@78 t线程@79 t线程@80 t线程@81 t线程@82 t线程@83 t线程@84 t线程@85 t线程@86
t线程@87 t线程@88 t线程@89 t线程@90 t线程@91 t线程@92 t线程@93 t线程@94 t线程@95 t线程@96
t线程@97 t线程@98 t线程@99

        我们可以看到 main 线程迅速运行完了10次循环,之后线程 t 才缓缓运行完。
        接下来,我们把 t 线程设置为插队线程:

java">package a06Thread1;

public class ThreadDemo {
    /*
        插入线程 / 插队线程
     */
    public static void main(String[] args) throws InterruptedException {

        MyThread t = new MyThread();
        t.setName("t线程(插队版)");
        t.start();

        //把t线程插入到当前线程(main线程)之前
        t.join();

        for (int i = 0; i < 10; i++) {
            System.out.println("main线程@" + i);
        }

    }
}

        再次执行,我们可以看到,t 线程插队在 main 线程之前运行完了,main 线程才继续运行完。

t线程(插队版)@0 t线程(插队版)@1 t线程(插队版)@2 t线程(插队版)@3 t线程(插队版)@4 t线程(插队版)@5
t线程(插队版)@6 t线程(插队版)@7 t线程(插队版)@8 t线程(插队版)@9 t线程(插队版)@10 t线程(插队版)@11
t线程(插队版)@12 t线程(插队版)@13 t线程(插队版)@14 t线程(插队版)@15 t线程(插队版)@16
t线程(插队版)@17 t线程(插队版)@18 t线程(插队版)@19 t线程(插队版)@20 t线程(插队版)@21
t线程(插队版)@22 t线程(插队版)@23 t线程(插队版)@24 t线程(插队版)@25 t线程(插队版)@26
t线程(插队版)@27 t线程(插队版)@28 t线程(插队版)@29 t线程(插队版)@30 t线程(插队版)@31
t线程(插队版)@32 t线程(插队版)@33 t线程(插队版)@34 t线程(插队版)@35 t线程(插队版)@36
t线程(插队版)@37 t线程(插队版)@38 t线程(插队版)@39 t线程(插队版)@40 t线程(插队版)@41
t线程(插队版)@42 t线程(插队版)@43 t线程(插队版)@44 t线程(插队版)@45 t线程(插队版)@46
t线程(插队版)@47 t线程(插队版)@48 t线程(插队版)@49 t线程(插队版)@50 t线程(插队版)@51
t线程(插队版)@52 t线程(插队版)@53 t线程(插队版)@54 t线程(插队版)@55 t线程(插队版)@56
t线程(插队版)@57 t线程(插队版)@58 t线程(插队版)@59 t线程(插队版)@60 t线程(插队版)@61
t线程(插队版)@62 t线程(插队版)@63 t线程(插队版)@64 t线程(插队版)@65 t线程(插队版)@66
t线程(插队版)@67 t线程(插队版)@68 t线程(插队版)@69 t线程(插队版)@70 t线程(插队版)@71
t线程(插队版)@72 t线程(插队版)@73 t线程(插队版)@74 t线程(插队版)@75 t线程(插队版)@76
t线程(插队版)@77 t线程(插队版)@78 t线程(插队版)@79 t线程(插队版)@80 t线程(插队版)@81
t线程(插队版)@82 t线程(插队版)@83 t线程(插队版)@84 t线程(插队版)@85 t线程(插队版)@86
t线程(插队版)@87 t线程(插队版)@88 t线程(插队版)@89 t线程(插队版)@90 t线程(插队版)@91
t线程(插队版)@92 t线程(插队版)@93 t线程(插队版)@94 t线程(插队版)@95 t线程(插队版)@96
t线程(插队版)@97 t线程(插队版)@98 t线程(插队版)@99 main线程@0 main线程@1 main线程@2
main线程@3 main线程@4 main线程@5 main线程@6 main线程@7 main线程@8 main线程@9

        这就是插入线程的作用。

五、线程的生命周期

        线程的生命周期如下图所示:
在这里插入图片描述
        值得注意的是,这里的线程阻塞是直接剥夺了线程的执行资格和执行权,这样它会跑到后台原地等待,直到阻塞结束后,才会重新参与抢夺CPU的执行权,因此一旦一个线程被阻塞,它脱离阻塞后不会立刻执行下面的代码,而是会经历再次抢夺CPU的执行权,才会重新开始执行。

六、线程安全

        要解释什么是线程安全问题,我们可以先引入一个例子:
        假设某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票。
        我们通过一个自定义线程来模拟这个卖票的逻辑。将 ticket 变量设定为静态,表示所有线程对象共享这一个变量。
        细节:sleep方法需要抛出一个异常,但是 Thread 的 run 方法是不能抛出异常的,因此只能用 try-catch 语句包裹 sleep 方法。线程是独立执行的代码片断,线程的问题应该由线程自己来解决,而不要委托到外部。基于这样的设计理念,在Java中,线程方法的异常都应该在线程代码边界之内(run方法内)进行try catch并处理掉。换句话说,我们不能捕获从线程中逃逸的异常。

java">package a06Thread4;

public class MyThread extends Thread{
    /*
        需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票
     */

    //static表示这个类中所有的对象都共享ticket这一个变量
    static 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(getName() + "正在卖第" + ticket + "张票!");
            }else{
                break;
            }
        }
    }

}

        经过测试类的三个线程并行,我们发现卖票时出现了奇怪的问题。

java">package a06Thread4;

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

        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

        同一张票居然被多个窗口售卖!且最后竟然有第101、102张票!

窗口3正在卖第1张票! 窗口1正在卖第1张票! 窗口2正在卖第1张票! 窗口3正在卖第3张票! 窗口1正在卖第3张票!
窗口2正在卖第4张票! 窗口1正在卖第5张票! 窗口2正在卖第5张票! 窗口3正在卖第5张票! 窗口2正在卖第6张票!
窗口1正在卖第7张票! 窗口3正在卖第8张票! 窗口2正在卖第9张票! 窗口1正在卖第10张票! 窗口3正在卖第11张票!
窗口2正在卖第12张票! 窗口1正在卖第13张票! 窗口3正在卖第14张票! 窗口2正在卖第15张票! 窗口1正在卖第16张票!
窗口3正在卖第17张票! 窗口2正在卖第18张票! 窗口1正在卖第19张票! 窗口3正在卖第20张票! 窗口2正在卖第21张票!
窗口1正在卖第22张票! 窗口3正在卖第23张票! 窗口2正在卖第24张票! 窗口1正在卖第25张票! 窗口3正在卖第26张票!
窗口2正在卖第27张票! 窗口1正在卖第28张票! 窗口3正在卖第29张票! 窗口2正在卖第30张票! 窗口1正在卖第31张票!
窗口3正在卖第32张票! 窗口2正在卖第33张票! 窗口1正在卖第34张票! 窗口3正在卖第35张票! 窗口2正在卖第36张票!
窗口1正在卖第37张票! 窗口3正在卖第38张票! 窗口2正在卖第39张票! 窗口1正在卖第40张票! 窗口3正在卖第41张票!
窗口2正在卖第42张票! 窗口1正在卖第43张票! 窗口3正在卖第44张票! 窗口2正在卖第45张票! 窗口1正在卖第46张票!
窗口3正在卖第47张票! 窗口2正在卖第48张票! 窗口1正在卖第49张票! 窗口3正在卖第50张票! 窗口2正在卖第51张票!
窗口1正在卖第52张票! 窗口3正在卖第53张票! 窗口2正在卖第54张票! 窗口1正在卖第55张票! 窗口3正在卖第56张票!
窗口2正在卖第57张票! 窗口1正在卖第58张票! 窗口3正在卖第59张票! 窗口2正在卖第60张票! 窗口1正在卖第61张票!
窗口3正在卖第62张票! 窗口2正在卖第63张票! 窗口1正在卖第64张票! 窗口3正在卖第65张票! 窗口2正在卖第66张票!
窗口1正在卖第67张票! 窗口3正在卖第68张票! 窗口2正在卖第69张票! 窗口1正在卖第70张票! 窗口3正在卖第71张票!
窗口2正在卖第72张票! 窗口1正在卖第73张票! 窗口3正在卖第74张票! 窗口2正在卖第75张票! 窗口1正在卖第76张票!
窗口3正在卖第77张票! 窗口2正在卖第78张票! 窗口1正在卖第79张票! 窗口3正在卖第80张票! 窗口2正在卖第81张票!
窗口1正在卖第82张票! 窗口3正在卖第83张票! 窗口2正在卖第84张票! 窗口1正在卖第85张票! 窗口3正在卖第86张票!
窗口2正在卖第87张票! 窗口1正在卖第88张票! 窗口3正在卖第89张票! 窗口2正在卖第90张票! 窗口1正在卖第91张票!
窗口3正在卖第92张票! 窗口2正在卖第93张票! 窗口1正在卖第94张票! 窗口3正在卖第95张票! 窗口2正在卖第96张票!
窗口1正在卖第97张票! 窗口3正在卖第98张票! 窗口2正在卖第99张票! 窗口1正在卖第100张票! 窗口3正在卖第101张票!
窗口2正在卖第102张票!

        这一问题的原因还是:线程执行时,有随机性。
        当一个线程被执行到某一行代码时,随时有可能被另一个线程把CPU的执行权抢走,因此可能导致一个变量同时被多个线程操作。这就是线程的安全问题
        要解决这个问题,我们就需要把操作共享数据的代码“”起来。

6.1 同步代码块

        同步代码块的格式如下:

java">synchronized(){
	操作共享数据的代码
}

锁的特点是:
1.锁默认是打开状态,一旦有一个线程进去了,锁就会自动关闭。
2.锁中的代码全部执行完毕后,线程会出来,然后锁会自动打开。

        通过给刚才的线程共享数据的部分(操作 ticket 的部分)加锁,就可以保证线程的安全。

java">package a06Thread4;

public class MyThread extends Thread{
    /*
        需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票
     */

    //static表示这个类中所有的对象都共享ticket这一个变量
    static int ticket = 0;

    //锁对象,一定要保证是唯一的,要加static
    static Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized(obj) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                } else {
                    break;
                }
            }
        }
    }
}

        注意:锁对象一定要是唯一的,否则加锁将变得没有意义。

6.2 同步方法

        同步代码块是把一段代码“锁”起来,解决多线程带来的数据安全问题。但是,如果我们的代码块包含了整个方法,就没有必要采取代码块的形式了,我们有同步方法

格式:

java">修饰符 synchronized 返回值类型 方法名(方法参数){...}

特点:
1.同步方法是锁住方法里面所有的代码。
2.锁对象是不能自己指定的,如果是非静态方法,锁对象就是 this;如果是静态对象,锁对象是当前类的字节码文件对象。

代码实现:

java">package a06Thread5;

public class ThreadDemo {
    public static void main(String[] args) {
        /*
            利用同步方法实现需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票
         */

        //创建Runnable对象
        MyRunnable mr = new MyRunnable();

        //创建线程对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        //设置线程名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

java">package a06Thread5;

public class MyRunnable implements Runnable{

    //共享变量
    //由于MyRunnable对象只需要创建一次,因此这个ticket变量不需要加static
    int ticket = 0;

    @Override
    public void run() {
        while(true){
            //同步方法
            if(sellTicket()) break;
        }
    }

    private synchronized boolean sellTicket() {
        if(ticket < 100){
            ticket++;
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            return false;       //如果卖票没达到100,返回false,循环继续
        }else{
            return true;        //如果卖票达到100,返回true,执行break语句
        }
    }
}

        同步方法在线程安全中经常用到。我们常用的字符串拼接类 StringBuilder 其实是线程不安全的,如果我们想在多线程中保持数据的安全,就需要用到 StringBuffer 类,它是一个和 StringBuilder 方法几乎一致的类,但是它的方法全都是用 synchronized 定义的,它是线程安全的。

6.3 Lock 锁

        虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
        Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock中提供了获得锁和释放锁的方法:

java">void lock():获得锁
void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法

java">ReentrantLock():创建一个ReentrantLock的实例

        通过加 Lock 锁改写同步代码块的方法如下:

java">package a06Thread4;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread{
    /*
        需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票
     */

    //static表示这个类中所有的对象都共享ticket这一个变量
    static int ticket = 0;

    //Lock锁
    //加static关键字保证lock锁只创建一次,是唯一的
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true) {
            //加锁
            lock.lock();
            try {
                if (ticket < 100) {
                    Thread.sleep(1);
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                //将释放锁的语句写在finally语句中,让锁一定能被释放
                lock.unlock();
            }
        }
    }
}

七、死锁

        死锁说白了就是在 Java 多线程运行中出现了锁的嵌套,导致程序不能继续执行。死锁是一种错误,是我们在编写代码时必须避免的。
        死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的相互等待的现象,如果没有外力作用,它们都将无法推进下去。
在这里插入图片描述
        要避免死锁也很简单,我们在开发时,尽可能避免锁的嵌套,就能防止死锁发生。

八、等待唤醒机制

        生产者和消费者模式是一个十分经典的多线程协作模式。在 Java 中,线程默认的执行顺序是随机的,谁先抢占CPU资源都是不确定的。而等待唤醒机制可以把线程的这种随机打破,让它们变成实打实的“交替执行”。
        我们把交替执行的两个线程分别叫做生产者,负责生产数据;消费者,负责消费数据。
        生产者和消费者模式有两种执行情况,分别为:消费者等待生产者等待。消费者负责消耗数据,生产者负责生产数据,如果消费者没有看到数据,就会一直等待,等到数据出现就开始消耗;反之,如果生产者生产好了数据,却没有等到消费者来吃,它也会继续等待,直到消费者来把数据消费,它才会继续生产新的数据。
        生产者和消费者模式一般需要用到这三个方法:
在这里插入图片描述

8.1 等待唤醒机制的基本实现

        代码实现如下所示:
① 创建一个桌子类,负责控制生产者和消费者的执行。

java">package a07ThreadWaitNotify;

public class Table {
    /*
        作用:控制生产者和消费者的执行
     */

    //foodFlag表示桌子上是否有食物,如果有就是1,没有就是0
    public static int foodFlag = 0;

    //食物总个数,表示消费者最多可以吃多少食物
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}

② 创建一个Cook类表示生产者。

java">package a07ThreadWaitNotify;

public class Cook extends Thread{
    @Override
    public void run() {
        /*
            1.循环
            2.同步代码块
            3.判断共享数据是否到了末尾?
                先写到了末尾的情况
                再写没有到末尾的情况(执行核心逻辑)
         */

        while(true){
            synchronized (Table.lock){
                if(Table.count == 0){
                    break;
                }else{
                    //先判断桌子上是否有食物,没有就等待,有则开吃
                    //吃完之后,要唤醒厨师继续做,然后把能吃的个数减一,然后修改foodFlag为0
                    if(Table.foodFlag == 0){
                        //如果没有就等待,用锁对象调用wait()方法,让锁对象调用目的是让当前线程跟锁进行绑定
                        try {
                            Table.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //如果有食物就开吃
                        // 能吃总数减1
                        Table.count--;
                        System.out.println("消费者在吃食物,还能再吃" + Table.count + "份!!!");

                        //吃完后唤醒厨师继续做
                        Table.lock.notifyAll();         //唤醒这条锁绑定的所有线程

                        //修改桌子状态
                        Table.foodFlag = 0;

                    }
                }
            }
        }
    }
}

③ 创建一个Foodie类表示消费者。

java">package a07ThreadWaitNotify;

public class Foodie extends Thread{
    /*
    1.循环
    2.同步代码块
    3.判断共享数据是否到了末尾?
        先写到了末尾的情况
        再写没有到末尾的情况(执行核心逻辑)
    */
    @Override
    public void run() {
        while(true){
            synchronized (Table.lock){
                if(Table.count == 0){
                    break;
                }else{
                    //先判断桌子上是否有食物,如果有食物,说明消费者没吃完,就等待
                    if(Table.foodFlag == 1){
                        try {
                            Table.lock.wait();      //用锁调用,把锁和线程绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //如果没有食物,就做一碗食物
                        System.out.println("生产者做了一份食物。");
                        //修改桌子上的食物状态
                        Table.foodFlag = 1;
                        //然后唤醒消费者开吃
                        Table.lock.notifyAll();         //唤醒锁绑定的所有线程

                    }
                }
            }
        }
    }
}

④ 创建一个测试类,用来运行线程。

java">package a07ThreadWaitNotify;

public class ThreadDemo {
    public static void main(String[] args) {
        /*
            需求:完成生产者和消费者(等待唤醒机制)的代码
            实现线程轮流交替执行的效果
         */

        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();

        //启动线程
        c.start();
        f.start();
    }
}

        以上就是生产者和消费者模式用最基本的代码实现的写法。
        实际上在 Java 中,我们还有另一种实现方式,那就是阻塞队列

8.2 阻塞队列实现等待唤醒机制

        阻塞队列其实是一个有上限的数据传输管道。
在这里插入图片描述
        Java 中阻塞队列的继承结构如下图所示:
在这里插入图片描述
        我们利用阻塞队列可以将刚才的生产者消费者模式用精简的代码表示:
① 创建一个 Cook 类,利用构造方法传递阻塞队列(为了保证是同一条阻塞队列)。然后编写业务逻辑,一个 put 语句就能搞定。

java">package a07ThreadWaitNotifyQueue;

import java.util.concurrent.ArrayBlockingQueue;

public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;

    //在构造方法中用参数传递的方式赋予队列地址值
    //保证Cook对象和Foodie对象绑定的阻塞队列是同一个
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            //不断把食物放入阻塞队列
            try {
                queue.put("食物");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("生产者放入了一份食物");
        }
    }
}

② 创建一个 Foodie 类,利用构造方法传递阻塞队列。写业务逻辑,一个 take 方法就能搞定。

java">package a07ThreadWaitNotifyQueue;

import java.util.concurrent.ArrayBlockingQueue;

public class Foodie extends Thread {

    ArrayBlockingQueue<String> queue;

    //在构造方法中用参数传递的方式赋予队列地址值
    //保证Cook对象和Foodie对象绑定的阻塞队列是同一个
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            //不断从阻塞队列中获取食物
            try {
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

③ 在测试类中创建阻塞队列,大小设置为1,表示一次只能传递一份食物。创建两条线程并执行。

java">package a07ThreadWaitNotifyQueue;

import java.util.concurrent.ArrayBlockingQueue;

public class ThreadDemo {
    public static void main(String[] args) {
        /*
            需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)代码
            细节:生产者和消费者必须使用同一个阻塞队列
         */

        //创建阻塞队列对象,大小设置为1
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //创建线程对象
        //两个线程传递同一条阻塞队列,保证线程安全
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        //开启线程
        c.start();
        f.start();
    }
}

九、线程的状态

        一个线程其实有七大状态。
在这里插入图片描述
        其实在 Java 中,只定义了线程的6种状态,唯独没有运行状态,这是因为,一旦线程抢夺到CPU执行权后,就会把线程交出去给OS运行,就不管线程的运行状态了。因此 Java 中有定义的线程状态只有以下6种,且右边是进入这种状态的前置条件。
在这里插入图片描述


http://www.niftyadmin.cn/n/5666809.html

相关文章

VM16安装macOS11

注意&#xff1a; 本文内容于 2024-09-17 12:08:24 创建&#xff0c;可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容&#xff0c;请访问原文地址&#xff1a;VM16安装macOS11。感谢您的关注与支持&#xff01; 使用 Vmware Workstation Pro 16 安装 macOS…

C++学习笔记 —— 内存分配 new

//创建数值 int *pi new int; //pi指向动态分配的&#xff0c;未初始化的无名对象 delete pi; int *pi new int(10); //pi指向动态分配的&#xff0c;初始化10 delete pi;//创建数组 int *a new int[5]; //创建一个数组&#xff0c;未初始化数值 delete []a; // new 和 de…

大数据时代:历史、发展与未来

文章目录 引言1980年&#xff1a;大数据的先声2006年&#xff1a;云计算与大数据的诞生2008年&#xff1a;大数据的科学探索2009年&#xff1a;大数据成为行业热词2011年&#xff1a;大数据的商业价值2013年&#xff1a;世界大数据元年结语 引言 在信息技术飞速发展的今天&…

linux-软件包管理-包管理工具(Debian 系)

Linux 软件包管理概述 在Linux系统中&#xff0c;软件包管理是系统维护的核心部分之一。通过软件包管理器&#xff0c;用户可以方便地安装、更新、删除和查询系统中的软件包。每个Linux发行版通常都有自己专属的包管理工具&#xff0c;这些工具基于不同的包格式。例如&#xf…

Springboot的三层架构

package com.wzb.ThreeLevelsExercise20240919;public class Exercise {// 内聚&#xff1a;内聚是指一个模块或内部各元素的紧密程度。高内聚则是一个模块或类中的所有功能都是紧密相关的&#xff0c;专注于完成单一任务// 高内聚的好处&#xff1a;// 1.易于维护&#xff1a;…

优化算法(四)—蚁群算法(附MATLAB程序)

蚁群算法&#xff08;Ant Colony Optimization, ACO&#xff09;是一种模拟蚂蚁觅食行为的优化算法&#xff0c;由Marco Dorigo于1990年提出。它利用了蚂蚁在寻找食物的过程中通过释放信息素来相互影响的机制&#xff0c;以找到最优解或接近最优解。蚁群算法特别适用于解决组合…

C++ : 继承问题 [virtual函数调用,为什么禁止在virtual使用默认参数]

文章目录 子类指针&#xff0c;父类指针分别调用virtual函数&#xff0c;与非virtual函数虚函数中尽量不要使用默认参数&#xff01;&#xff01;&#xff01; 子类指针&#xff0c;父类指针分别调用virtual函数&#xff0c;与非virtual函数 virtual函数&#xff0c;通过指针调…

有毒有害气体检测仪的应用和性能_鼎跃安全

随着现代工业的不断发展和扩张&#xff0c;越来越多的企业涉及到有毒有害气体的生产、使用和处理。工业规模的扩大导致有毒有害气体的排放量增加&#xff0c;同时也增加了气体泄漏的风险。在发生火灾、爆炸或危险化学品泄漏等紧急事件时&#xff0c;救援人员需要迅速了解现场的…