【JAVA入门】Day47 - 线程

【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() 方法。

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

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

package myThread;public class ThreadDemo1 {public static void main(String[] args) {/*继承 Thread 类方法:1.自己定义一个类继承Thread2.重写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() 方法。

package myThread;public class MyRun implements Runnable {@Overridepublic 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() 方法,开启线程。

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() 方法。

package myThread;import java.util.concurrent.Callable;public class MyCallable implements Callable<Integer> {//重写call方法,求1~100的整数和@Overridepublic 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() 方法获取线程执行的结果。

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类的构造方法进行构造方法的重载,重载后的构造方法可以将字符串传递进去,作为线程的名字。

package myThread;public class MyThread1 extends Thread{public MyThread1() {}public MyThread1(String name) {super(name);}@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println(getName() + '@' + i);}}
}

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

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 采取的调度方式就是抢占式调度,它的线程调度也依据线程的优先级,且提供了线程优先级的内置方法。
在这里插入图片描述
        如何设置线程优先级,线程优先级体现在哪里,如下代码所示:

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。

        t1.setPriority(1);t2.setPriority(10);t1.start();t2.start();

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

4.3 守护线程

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

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

package a06Thread3;public class MyThread1 extends Thread{public MyThread1() {}public MyThread1(String name) {super(name);}@Overridepublic void run() {for(int i = 1; i <= 10; i++) {System.out.println(getName() + "@" + i);}}
}
package a06Thread3;public class MyThread2 extends Thread{public MyThread2() {}public MyThread2(String name) {super(name);}@Overridepublic void run() {for(int i = 1; i <= 100; i++) {System.out.println(getName() + "@" + i);}}
}

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

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执行权。

package a06Thread2;public class MyThread extends Thread {@Overridepublic void run() {for(int i = 1; i <= 100; i++) {System.out.println(getName() + "@" + i);}//礼让当前CPU的执行权Thread.yield();}
}

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

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 。

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

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

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 线程设置为插队线程:

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并处理掉。换句话说,我们不能捕获从线程中逃逸的异常。

package a06Thread4;public class MyThread extends Thread{/*需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票*///static表示这个类中所有的对象都共享ticket这一个变量static int ticket = 0;@Overridepublic void run() {while(true){if(ticket < 100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票!");}else{break;}}}}

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

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 同步代码块

        同步代码块的格式如下:

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

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

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

package a06Thread4;public class MyThread extends Thread{/*需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口一起卖票*///static表示这个类中所有的对象都共享ticket这一个变量static int ticket = 0;//锁对象,一定要保证是唯一的,要加staticstatic Object obj = new Object();@Overridepublic 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 同步方法

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

格式:

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

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

代码实现:

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();}
}
package a06Thread5;public class MyRunnable implements Runnable{//共享变量//由于MyRunnable对象只需要创建一次,因此这个ticket变量不需要加staticint ticket = 0;@Overridepublic 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中提供了获得锁和释放锁的方法:

void lock():获得锁
void unlock():释放锁

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

ReentrantLock():创建一个ReentrantLock的实例

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

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();@Overridepublic 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 等待唤醒机制的基本实现

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

package a07ThreadWaitNotify;public class Table {/*作用:控制生产者和消费者的执行*///foodFlag表示桌子上是否有食物,如果有就是1,没有就是0public static int foodFlag = 0;//食物总个数,表示消费者最多可以吃多少食物public static int count = 10;//锁对象public static Object lock = new Object();
}

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

package a07ThreadWaitNotify;public class Cook extends Thread{@Overridepublic void run() {/*1.循环2.同步代码块3.判断共享数据是否到了末尾?先写到了末尾的情况再写没有到末尾的情况(执行核心逻辑)*/while(true){synchronized (Table.lock){if(Table.count == 0){break;}else{//先判断桌子上是否有食物,没有就等待,有则开吃//吃完之后,要唤醒厨师继续做,然后把能吃的个数减一,然后修改foodFlag为0if(Table.foodFlag == 0){//如果没有就等待,用锁对象调用wait()方法,让锁对象调用目的是让当前线程跟锁进行绑定try {Table.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}else{//如果有食物就开吃// 能吃总数减1Table.count--;System.out.println("消费者在吃食物,还能再吃" + Table.count + "份!!!");//吃完后唤醒厨师继续做Table.lock.notifyAll();         //唤醒这条锁绑定的所有线程//修改桌子状态Table.foodFlag = 0;}}}}}
}

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

package a07ThreadWaitNotify;public class Foodie extends Thread{/*1.循环2.同步代码块3.判断共享数据是否到了末尾?先写到了末尾的情况再写没有到末尾的情况(执行核心逻辑)*/@Overridepublic 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();         //唤醒锁绑定的所有线程}}}}}
}

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

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 语句就能搞定。

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;}@Overridepublic void run() {while(true){//不断把食物放入阻塞队列try {queue.put("食物");} catch (InterruptedException e) {e.printStackTrace();}System.out.println("生产者放入了一份食物");}}
}

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

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;}@Overridepublic void run() {while(true){//不断从阻塞队列中获取食物try {String food = queue.take();System.out.println(food);} catch (InterruptedException e) {e.printStackTrace();}}}
}

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

package a07ThreadWaitNotifyQueue;import java.util.concurrent.ArrayBlockingQueue;public class ThreadDemo {public static void main(String[] args) {/*需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)代码细节:生产者和消费者必须使用同一个阻塞队列*///创建阻塞队列对象,大小设置为1ArrayBlockingQueue<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.xdnf.cn/news/1539671.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

如何不终止容器退出Docker Bash会话

如何不终止容器退出Docker Bash会话 💖The Begin💖点点关注,收藏不迷路💖 当通过docker exec进入Docker容器的bash会话后,如果想退出但不停止容器,可以使用快捷键组合: 按下Ctrl+P然后紧接着按下Ctrl+Q。 这个操作会让你从bash会话中“分离”出来,但容器会继续运行…

Zabbix 部署----安装 Zabbix(监控服务器)

目录 zabbix 官网: 1、准备一台虚拟机 1.整理配置yum源(192.xx.xx.10) 2.设置主机名(192.xx.xx.10) 3.防火墙 4.selinux 2、准备Zabbix-repo 使用阿里提供的zabbixYUM源 3、安装Zabbix服务器 4、初始化数据库 1.安装数据库 2.启动数据库 3.授权zabbix账号 4.初始化…

解决使用nvm ls命令没有出现*的问题

一、引言 在输命令的时候不知道手误写了什么导致node命令用不了&#xff0c;查看环境变量配的nvm对应的路径没问题&#xff0c;试过网上说的修改文件夹名字但是没有用&#xff01;&#xff01; 输入 nvm ls 显示已下载的node版本&#xff0c;发现前面没有* 输入nvm use 使用其中…

【macOS】【zsh报错】zsh: command not found: python

【macOS】【zsh Error】zsh: command not found: python 本地已经安装了Python&#xff0c;且能在Pycharm中编译Python程序并运行。 但是&#xff0c;在macOS终端&#xff0c;运行Python&#xff0c;报错。 首先要确认你在macOS系统下&#xff0c;是否安装了Python。 如果安…

去噪扩散隐式模型

dataset_name "datasets/oxford-102-flowers/" dataset_repetitions 2 # 数据集重复 num_epochs 25 image_size 64 # 模型训练和生成图像的大小 # KID 内核初始距离 kid_image_size 75 # 从噪声中逐步“去噪”或“扩散”到最终图像所需的步骤数。 kid_diffusi…

NEMESIS: NORMALIZING THE SOFT-PROMPT VECTORS OF VISION-LANGUAGE MODELS

文章汇总 发现的现象 动机的描述 Norm增加会导致性能下降&#xff0c;Norm降低会导致性能上升。于是作者提出&#xff1a; 我们需要规范化VLMs中的软提示吗? 实验验证 在左图中的紫色块中可以看到&#xff0c;随着模型性能的上升&#xff0c;Norm value会不断下降。 解决…

C语言 | Leetcode C语言题解之第419题棋盘上的战舰

题目&#xff1a; 题解&#xff1a; int countBattleships(char** board, int boardSize, int* boardColSize){int row boardSize;int col boardColSize[0];int ans 0;for (int i 0; i < row; i) {for (int j 0; j < col; j) {if (board[i][j] X) {if (i > 0 &…

ant vue3 datePicker默认显示英文

改前&#xff1a; 改后&#xff1a; 处理方法&#xff1a; 在App.vue页加上以下导入即可 import dayjs from dayjs; import dayjs/locale/zh-cn dayjs.locale(zh-cn); 如图&#xff1a;

电力电塔电线缺陷检测数据集 voc yolo

电力 电塔电线缺陷检测数据集 10000张 带标注 voc yolo 电力电塔电线缺陷检测数据集 数据集描述 该数据集旨在用于电力电塔和电线的缺陷检测任务&#xff0c;涵盖多种常见的缺陷类型。数据集包含了大量的图像及其对应的标注信息&#xff0c;可用于训练计算机视觉模型&#x…

如何搭建虚拟机Ubuntu?

1.创建新虚拟机 2.进入新建虚拟机向导&#xff0c;选择稍后安装 3. 选择Linux中的Ubuntu 4.为虚拟机命名设置路径 5. 设置磁盘容量大小 6.选择自定义硬件 7.设置设备状态 8.完成Ubuntu虚拟机创建

【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL24

边沿检测 有一个缓慢变化的1bit信号a&#xff0c;编写一个程序检测a信号的上升沿给出指示信号rise&#xff0c;当a信号出现下降沿时给出指示信号down。 注&#xff1a;rise,down应为单脉冲信号&#xff0c;在相应边沿出现时的下一个时钟为高&#xff0c;之后恢复到0&#xff0…

Pandas_数据结构详解

1.创建DataFrame对象 概述 DataFrame是一个表格型的结构化数据结构&#xff0c;它含有一组或多组有序的列&#xff08;Series&#xff09;&#xff0c;每列可以是不同的值类型&#xff08;数值、字符串、布尔值等&#xff09;。 DataFrame是Pandas中的最基本的数据结构对象&am…

【kafka-04】kafka线上问题以及高效原理

Kafka系列整体栏目 内容链接地址【一】afka安装和基本核心概念https://zhenghuisheng.blog.csdn.net/article/details/142213307【二】kafka集群搭建https://zhenghuisheng.blog.csdn.net/article/details/142253288【三】springboot整合kafka以及核心参数详解https://zhenghui…

中间件安全(二)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 前言: 前文链接&#xff1a;中间件安全&#xff08;一&#xff09; 本文主要讲解Couchdb数据库未授权越权漏洞&#xff08;CVE-2017-12635&#xff09;。 靶场链接&#xff1a;Vu…

【内网渗透】最保姆级的春秋云镜Privilege打靶笔记

目录 flag1 flag2 flag3 flag4 flag1 fscan扫外网 访问./www.zip拿到源码 tools/content-log.php存在任意文件读取 根据提示读到Jenkins初始管理员密码 ./tools/content-log.php?logfile../../../../../../../../../ProgramData/Jenkins/.jenkins/secrets/initialAdminP…

Docker UI强大之处?

DockerUI是一款由国内开发者打造的优秀Docker可视化管理工具。它拥有简洁直观的用户界面&#xff0c;使得Docker主机管理、集群管理和任务编排变得轻松简单。DockerUI不仅能展示资源利用率、系统信息和更新日志&#xff0c;还提供了镜像管理功能&#xff0c;帮助用户高效清理中…

Spring Boot 整合 MyBatis 的详细步骤(两种方式)

1. Spring Boot 配置 MyBatis 的详细步骤 1、首先&#xff0c;我们创建相关测试的数据库&#xff0c;数据表。如下&#xff1a; CREATE DATABASE springboot_mybatis USE springboot_mybatisCREATE TABLE monster ( id int not null auto_increment, age int not null, birthda…

Vue 实现高级穿梭框 Transfer 封装

文章目录 01 基础信息1.1. 技术栈1.2. 组件设计a. 竖版设计稿b. 横版设计稿 02 技术方案&#xff08;1&#xff09;初定义数据&#xff08;2&#xff09;注意事项&#xff08;3&#xff09;逻辑草图 03 代码示例3.1. 组件使用3.2. 组件源码./TransferPlus/index.vue./TransferP…

动物识别系统Python+卷积神经网络算法+TensorFlow+人工智能+图像识别+计算机毕业设计项目

一、介绍 动物识别系统。本项目以Python作为主要编程语言&#xff0c;并基于TensorFlow搭建ResNet50卷积神经网络算法模型&#xff0c;通过收集4种常见的动物图像数据集&#xff08;猫、狗、鸡、马&#xff09;然后进行模型训练&#xff0c;得到一个识别精度较高的模型文件&am…

开源项目 GAN 漫画风格化 UGATIT

开源项目&#xff1a;DataBall / UGATIT GitCode * 数据集 * [该项目制作的训练集的数据集下载地址(百度网盘 Password: gxl1 )](https://pan.baidu.com/s/1683TRcv3r3o7jSitq3VyYA) * 预训练模型 * [预训练模型下载地址(百度网盘 Password: khbg )](https://pan.ba…