一文搞懂Java多线程

一文搞懂Java多线程

  • 1. 基本概念
  • 2. 线程的创建和启动
    • 2.1. 多线程实现的原理
    • 2.2.多线程的创建,方式一:继承于Thread类
    • 2.3.多线程的创建,方式一:创建Thread匿名子类(也属于方法一)
    • 2.4. 多线程的创建,方式二:实现Runnable接口
      • 2.4.1. 比较创建线程的两种方式
    • 2.5. 多线程的创建,方式三:实现Callable接口
    • 2.6. 多线程的创建,方式四:线程池
  • 3. Thread类的常用方法
  • 4. 线程的调度
    • 4.1. cpu的调度策略
    • 4.2. Java的调度算法:
  • 5. 线程的生命周期
  • 6. 线程的同步
    • 6.1. 多线程的安全性问题解析
    • 6.2. 多线程安全性问题的解决
      • 6.2.1. 多线程安全问题的解决方式一:同步代码块
      • 6.3.2. 多线程安全问题的解决方式二:同步方法
      • 6.2.3. 多线程安全问题的解决方式二:Lock锁 -JDK5.0新特性
    • 6.3. 线程同步的死锁问题
  • 7. 线程的通信
  • 练习

1. 基本概念

  • 程序(program)

    程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(还没有运行起来),静态对象。

  • 进程(process)

    进程是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。

    进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

  • 线程(thread)

    进程可进一步细化为线程,是一个程序内部的执行路径。

    若一个进程同一时间并行执行多个线程,那么这个进程就是支持多线程的。

    线程是cpu调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。

    一个进程中的多个线程共享相同的内存单元/内存地址空间——》他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得相乘间通信更简便、搞笑。但索格线程操作共享的系统资源可能就会带来安全隐患(隐患为到底哪个线程操作这个数据,可能一个线程正在操作这个数据,有一个线程也来操作了这个数据v)。

    • 配合JVM内存结构了解(只做了解即可)

      class文件会通过类加载器加载到内存空间。

      其中内存区域中每个线程都会有虚拟机栈和程序计数器。

      每个进程都会有一个方法区和堆,多个线程共享同一进程下的方法区和堆。

  • CPU单核和多核的理解

    单核的CPU是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。同时间段内有多个线程需要CPU去运行时,CPU也只能交替去执行多个线程中的一个线程,但是由于其执行速度特别快,因此感觉不出来。

    多核的CPU才能更好的发挥多线程的效率。

    对于Java应用程序java.exe来讲,至少会存在三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如过发生异常时会影响主线程。

  • Java线程的分类:用户线程 和 守护线程

    • Java的gc()垃圾回收线程就是一个守护线程
    • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以吧一个用户线程变成一个守护线程。
  • 并行和并发

    • 并行:多个cpu同时执行多个任务。比如,多个人做不同的事。
    • 并发:一个cpu(采用时间片)同时执行多个任务。比如,渺少、多个人做同一件事。
  • 多线程的优点

    1. 提高应用程序的响应。堆图像化界面更有意义,可以增强用户体验。
    2. 提高计算机系CPU的利用率。
    3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
  • 何时需要多线程

    • 程序需要同时执行两个或多个任务。
    • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
    • 需要一些后台运行的程序时。

2. 线程的创建和启动

2.1. 多线程实现的原理

  • Java语言的JVM允许程序运行多个线程,多线程可以通过Java中的java.lang.Thread类来体现。
  • Thread类的特性
    • 每个线程都是通过某个特定的Thread对象的run()方法来完成操作的,经常吧run()方法的主体称为线程体。
    • 通过Thread方法的start()方法来启动这个线程,而非直接调用run()。

2.2.多线程的创建,方式一:继承于Thread类

  1. 创建一个继承于Thread类的子类。
  2. 重写Thread类的run()方法。
  3. 创建Thread类的子类的对象。
  4. 通过此对象调用start()来启动一个线程。

**代码实现:**多线程执行同一段代码

package com.broky.multiThread;public class ThreadTest extends Thread{@Override//线程体,启动线程时会运行run()方法中的代码public void run() {//输出100以内的偶数for (int i = 0; i < 100; i++) {if (i % 2 == 0){System.out.println(Thread.currentThread().getName()+":\t"+i);}}}public static void main(String[] args) {//创建一个Thread类的子类对象ThreadTest t1 = new ThreadTest();//通过此对象调用start()启动一个线程t1.start();//注意:已经启动过一次的线程无法再次启动//再创建一个线程ThreadTest t2 = new ThreadTest();t2.start();//另一种调用方法,此方法并没有给对象命名new ThreadTest().start();System.out.println("主线程");}
} 

多线程代码运行图解

多线程执行多段代码

package com.broky.multiThread.exer;public class ThreadExerDemo01 {public static void main(String[] args) {new Thread01().start();new Thread02().start();}
}class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);}}
}class Thread02 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (i % 2 != 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);}}
} 

2.3.多线程的创建,方式一:创建Thread匿名子类(也属于方法一)

package com.broky.multiThread;public class AnonymousSubClass {public static void main(String[] args) {new Thread(){@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);}}}.start();}
} 

2.4. 多线程的创建,方式二:实现Runnable接口

  1. 创建一个实现Runnable接口的类。
  2. 实现类去实现Runnable接口中的抽象方法:run()。
  3. 创建实现类的对象。
  4. 将此对象作为参数传到Thread类的构造器中,创建Thread类的对象。
  5. 通过Thread类的对象调用start()方法。
package com.broky.multiThread;public class RunnableThread {public static void main(String[] args) {//创建实现类的对象RunnableThread01 runnableThread01 = new RunnableThread01();//创建Thread类的对象,并将实现类的对象当做参数传入构造器Thread t1 = new Thread(runnableThread01);//使用Thread类的对象去调用Thread类的start()方法:①启动了线程 ②Thread中的run()调用了Runnable中的run()t1.start();//在创建一个线程时,只需要new一个Thread类就可,不需要new实现类Thread t2 = new Thread(runnableThread01);t2.start();}
}//RunnableThread01实现Runnable接口的run()抽象方法
class RunnableThread01 implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);}}
} 

2.4.1. 比较创建线程的两种方式

  • Java中只允许单进程,以卖票程序TiketSales类来说,很有可能这个类本来就有父类,这样一来就不可以继承Thread类来完成多线程了,但是一个类可以实现多个接口,因此实现的方式没有类的单继承性的局限性,用实现Runnable接口的方式来完成多线程更加实用。
  • 实现Runnable接口的方式天然具有共享数据的特性(不用static变量)。因为继承Thread的实现方式,需要创建多个子类的对象来进行多线程,如果子类中有变量A,而不使用static约束变量的话,每个子类的对象都会有自己独立的变量A,只有static约束A后,子类的对象才共享变量A。而实现Runnable接口的方式,只需要创建一个实现类的对象,要将这个对象传入Thread类并创建多个Thread类的对象来完成多线程,而这多个Thread类对象实际上就是调用一个实现类对象而已。实现的方式更适合来处理多个线程有共享数据的情况。
  • 联系:Thread类中也实现了Runnable接口
  • 相同点两种方式都需要重写run()方法,线程的执行逻辑都在run()方法中

2.5. 多线程的创建,方式三:实现Callable接口

与Runnable相比,Callable功能更强大

  1. 相比run()方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果
package com.broky.multiThread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** 创建线程的方式三:实现Callable接口。 ---JDK5新特性* 如何理解Callable比Runnable强大?* 1.call()可以有返回值* 2.call()可以抛出异常被外面的操作捕获*///1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer>{//2.实现call方法,将此线程需要执行的操作声明在call()中@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i < 100; i++) {if(i%2==0){System.out.println(i);sum += i;}}return sum;}
}public class ThreadNew {public static void main(String[] args) {//3.创建Callable接口实现类的对象NumThread numThread = new NumThread();//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象FutureTask<Integer> futureTask = new FutureTask(numThread);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()new Thread(futureTask).start();try {//6.获取Callable中Call方法的返回值Integer sum = futureTask.get();System.out.println("总和为"+sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
} 

2.6. 多线程的创建,方式四:线程池

背景:

​ 经常创建和销毁、使用量特别大的资源、比如并发情况下的线程、对性能影响很大。

思路:

​ 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

优点:

​ 提高响应速度(减少了创建新线程的时间)

​ 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

​ 便于线程管理

package com.broky.multiThread;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;/*** 创建线程的方式四:使用线程池* <p>* 面试题:创建多线程有几种方式*/class NumberThread implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":\t" + i);}}}
}public class ThreadPool {public static void main(String[] args) {//1.提供指定线程数量的线程池ExecutorService service = Executors.newFixedThreadPool(10);ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;//设置线程池的属性//        System.out.println(service.getClass());//        service1.setCorePoolSize(15);//        service1.setKeepAliveTime();//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。service.execute(new NumberThread()); //适合用于Runnable//        service.submit(); 适合适用于Callable//关闭线程池service.shutdown();}
} 

3. Thread类的常用方法

  • start() : 启动当前线程, 调用当前线程的run()方法
  • run() : 通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中
  • currentThread() : 静态方法, 返回当前代码执行的线程
  • getName() : 获取当前线程的名字
  • setName() : 设置当前线程的名字
  • yield() : 释放当前CPU的执行权
  • join() : 在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态
  • stop() : 已过时. 当执行此方法时,强制结束当前线程.
  • sleep(long militime) : 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态
  • isAlive() :判断当前线程是否存活

4. 线程的调度

4.1. cpu的调度策略

  • **时间片:**cpu正常情况下的调度策略。即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

  • **抢占式:**高优先级的线程抢占cpu。

4.2. Java的调度算法:

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
  • 堆高优先级,使用优先调度的抢占式策略。

线程的优先级等级(一共有10挡)

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5 (默认优先级)

获取和设置当前线程的优先级

  • getPriority(); 获取
  • setPriority(int p); 设置

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有高优先级的线程执行完成以后,低优先级的线程才执行。

5. 线程的生命周期

  • JDk中用Thread.State类定义了线程的几种状态

想要实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在他的一个完整的生命周期中通常要经历如下的五种状态

  1. 新建:当一个Thread类或其子类的对象被声明并创建时,新的线程对象处于新建状态。
  2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
  4. 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  5. 死亡:线程完成了它的全部工作或线程被提前强制性的中止或出现异常倒置导致结束。

6. 线程的同步

6.1. 多线程的安全性问题解析

  • 线程的安全问题
    • 多个线程执行的不确定性硬气执行结果的不稳定性
    • 多个线程对账本的共享, 会造成操作的不完整性, 会破坏数据.
    • 多个线程访问共享的数据时可能存在安全性问题
  • 线程的安全问题Demo: 卖票过程中出现了重票和错票的情况 (以下多窗口售票demo存在多线程安全问题)
package com.broky.multiThread.safeThread;public class SafeTicketsWindow {public static void main(String[] args) {WindowThread ticketsThread02 = new WindowThread();Thread t1 = new Thread(ticketsThread02);Thread t2 = new Thread(ticketsThread02);Thread t3 = new Thread(ticketsThread02);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}class WindowThread implements Runnable {private int tiketsNum = 100;public void run() {while (true) {if (tiketsNum > 0) {try {//手动让线程进入阻塞,增大错票概率Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum);/*try {//手动让线程进入阻塞,增大重票的概率Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}*/tiketsNum--;} else {break;}}}
} 

错票分析:

当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这是其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。

极端情况为,当票数为1时,三个线程同时判断通过,进入阻塞,然后多执行两侧卖票操作。

重票分析

如果t1在输出票号22和票数-1的操作之间被阻塞,这就导致这时候t1卖出了22号票,但是总票数没有减少。在t1被阻塞期间,如果t2运行到输出票号时,那么t2也会输出和t1相同的票号22.

通过以上两种情况可以看出,线程的安全性问题时因为多个线程正在执行代码的过程中,并且尚未完成的时候,其他线程参与进来执行代码所导致的。

6.2. 多线程安全性问题的解决

原理:

当一个线程在操作共享数据的时候,其他线程不能参与进来。知道这个线程操作完共享数据的时候,其他线程才可以操作。即使当这个线程操作共享数据的时候发生了阻塞,依旧无法改变这种情况。

在Java中,我们通过同步机制,来解决线程的安全问题。

6.2.1. 多线程安全问题的解决方式一:同步代码块

synchronized(同步监视器){需要被同步的代码}

说明:

  1. 操作共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码。 不能多包涵代码(效率低,如果包到while前面就变成了单线程了),也不能少包含代码
  2. 共享数据:多个线程共同操作的变量。
  3. 同步监视器:俗称,锁。任何一个类的对象都可以充当锁。但是所有的线程都必须共用一把锁,共用一个对象。

锁的选择:

  1. 自行创建,共用对象,如下面demo中的Object对象。

  2. 使用this表示当前类的对象

    继承Thread的方法中的锁不能使用this代替,因为继承thread实现多线程时,会创建多个子类对象来代表多个线程,这个时候this指的时当前这个类的多个对象,不唯一,无法当作锁。

    实现Runnable接口的方式中,this可以当作锁,因为这种方式只需要创建一个实现类的对象,将实现类的对象传递给多个Thread类对象来当作多个线程,this就是这个一个实现类的对象,是唯一的,被所有线程所共用的对象。

  3. 使用类当作锁,以下面demo为例,其中的锁可以写为WindowThread.class, 从这里可以得出结论,类也是一个对象

优点:同步的方式,解决了线程安全的问题

缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率低。

Demo

package com.broky.multiThread.safeThread;public class SafeTicketsWindow {public static void main(String[] args) {WindowThread ticketsThread02 = new WindowThread();Thread t1 = new Thread(ticketsThread02);Thread t2 = new Thread(ticketsThread02);Thread t3 = new Thread(ticketsThread02);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}class WindowThread implements Runnable {private int tiketsNum = 100;//由于,Runnable实现多线程,所有线程共用一个实现类的对象,所以三个线程都共用实现类中的这个Object类的对象。Object obj = new Object();//如果时继承Thread类实现多线程,那么需要使用到static Object obj = new Object();public void run() {//Object obj = new Object();//如果Object对象在run()方法中创建,那么每个线程运行都会生成自己的Object类的对象,并不是三个线程的共享对象,所以并没有给加上锁。while (true) {synchronized (obj) {if (tiketsNum > 0) {try {//手动让线程进入阻塞,增大安全性发生的概率Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum + "\t剩余票数:" + --tiketsNum);} else {break;}}}}
} 

6.3.2. 多线程安全问题的解决方式二:同步方法

将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。

要点:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
  2. 非静态的同步方法,同步监视器是:this。
  3. 静态的同步方法,同步监视器是:当前类本身。

Demo

package com.broky.multiThread.safeThread;public class Window02 {public static void main(String[] args) {Window02Thread ticketsThread02 = new Window02Thread();Thread t1 = new Thread(ticketsThread02);Thread t2 = new Thread(ticketsThread02);Thread t3 = new Thread(ticketsThread02);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}class Window02Thread implements Runnable {private int tiketsNum = 100;@Overridepublic void run() {while (tiketsNum > 0) {show();}}private synchronized void show() { //同步监视器:thisif (tiketsNum > 0) {try {//手动让线程进入阻塞,增大安全性发生的概率Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum + "\t剩余票数:" + --tiketsNum);}}
} 
package com.broky.multiThread.safeThread;public class Window03 {public static void main(String[] args) {Window03Thread t1 = new Window03Thread();Window03Thread t2 = new Window03Thread();Window03Thread t3 = new Window03Thread();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.setPriority(Thread.MIN_PRIORITY);t3.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();t3.start();}
}class Window03Thread extends Thread {public static int tiketsNum = 100;@Overridepublic void run() {while (tiketsNum > 0) {show();}}public static synchronized void show() {//同步监视器:Winddoe03Thread.class  不加static话同步监视器为t1 t2 t3所以错误if (tiketsNum > 0) {try {//手动让线程进入阻塞,增大安全性发生的概率Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum + "\t剩余票数:" + --tiketsNum);}}
} 

使用同步解决懒汉模式的线程安全问题

package com.broky.multiThread.safeThread;public class BankTest {
}class Bank {private Bank() {}private static Bank instance = null;public static Bank getInstance() {//方式一:效率性差,每个等待线程都会进入同步代码块//        synchronized (Bank.class) {//            if (instance == null) {//                instance = new Bank();//            }//        }//方式二:在同步代码块外层在判断一次,就防止所有线程进入同步代码块。if (instance == null) {synchronized (Bank.class) {if (instance == null) {instance = new Bank();}}}return instance;}
} 

6.2.3. 多线程安全问题的解决方式二:Lock锁 -JDK5.0新特性

JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁

synchronized和lock的异同:(面试题)

1. Lcok是显式锁(需要手动开启和关闭锁),synchronized是隐式锁,除了作用域自动释放。
2. Lock只有代码块锁,synchronized有代码块锁和方法锁。
3. 使用Lcok锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类) 

建议使用顺序:Lock—》同步代码块(已经进入了方法体,分配了相应的资源)—》同步方法(在方法体之外)

Demo:

package com.broky.multiThread.safeThread;import java.util.concurrent.locks.ReentrantLock;public class SafeLock {public static void main(String[] args) {SafeLockThread safeLockThread = new SafeLockThread();Thread t1 = new Thread(safeLockThread);Thread t2 = new Thread(safeLockThread);Thread t3 = new Thread(safeLockThread);t1.start();t2.start();t3.start();}
}class SafeLockThread implements Runnable{private int tickets = 100;private ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (tickets>0) {try {//在这里锁住,有点类似同步监视器lock.lock();if (tickets > 0) {Thread.sleep(100);System.out.println(Thread.currentThread().getName() + ":\t票号:" + tickets + "\t剩余票数:" + --tickets);}} catch (InterruptedException e) {e.printStackTrace();} finally {//操作完成共享数据后在这里解锁lock.unlock();}}}
} 

6.3. 线程同步的死锁问题

原理:

​ 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。

​ 出现死锁后,并不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

​ 使用同步时应避免出现死锁。

Java中死锁最简单的情况:

​ 一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。

​ 从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的

死锁的解决方法:

1. 专门的算法、原则。
2. 尽量减少同步资源的定义。
3. 尽量避免嵌套同步。 
package com.broky.multiThread.safeThread;public class DeadLock {public static void main(String[] args) {StringBuffer s1 = new StringBuffer();StringBuffer s2 = new StringBuffer();new Thread() {public void run() {synchronized (s1) {s1.append("a");s2.append("1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2) {s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();new Thread(new Runnable() {public void run() {synchronized (s2) {s1.append("c");s2.append("3");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1) {s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}).start();}
} 

7. 线程的通信

很多情况下,尽管我们创建了多个线程,也会出现几乎一个线程执行完所有操作的时候,这时候我们就需要让线程间相互交流。

原理:

​ 当一个线程执行完成其所应该执行的代码后,手动让这个线程进入阻塞状态,这样一来,接下来的操作只能由其他线程来操作。当其他线程执行的开始阶段,再手动让已经阻塞的线程停止阻塞,进入就绪状态,虽说这时候阻塞的线程停止了阻塞,但是由于现在正在运行的线程拿着同步锁,所以停止阻塞的线程也无法立马执行。如此操作就可以完成线程间的通信。

所用的到方法:

​ wait():一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。

​ notify():一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。

​ notifyAll() :一旦执行此方法,就会唤醒所有被wait的线程

​ 说明:

​ 这三个方法必须在同步代码块或同步方法中使用。

​ 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

​ 这三个方法并不时定义在Thread类中的,而是定义在Object类当中的。因为所有的对象都可以作为同步监视器,而这三个方法需要由同步监视器调用,所以任何一个类都要满足,那么只能写在Object类中。

sleep()和wait()的异同:(面试题)

  1. 相同点:两个方法一旦执行,都可以让线程进入阻塞状态。

  2. 不同点:1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()

    ​ 2) 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。

    ​ 2) 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。

Demo:

package com.broky.multiThread;public class Communication {public static void main(String[] args) {CommunicationThread communicationThread = new CommunicationThread();Thread t1 = new Thread(communicationThread);Thread t2 = new Thread(communicationThread);Thread t3 = new Thread(communicationThread);t1.start();t2.start();t3.start();}
}class CommunicationThread implements Runnable {int Num = 1;@Overridepublic void run() {while (true) {synchronized (this) {notifyAll();if (Num <= 100) {System.out.println(Thread.currentThread().getName() + ":\t" + Num);Num++;try {wait();} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
} 

练习

  • 练习1:

银行有一个账户。

有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

package com.broky.multiThread.exer;/*** 练习1* 银行有一个账户* 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。* 分析:* 1.是否有多个线程问题? 是,有两个储户线程。* 2.是否有共享数据? 是,两个储户向同一个账户存钱* 3.是否有线程安全问题: 有*/
public class AccountTest {public static void main(String[] args) {Account acct = new Account();Customer c1 = new Customer(acct);Customer c2 = new Customer(acct);c1.setName("储户1");c2.setName("储户2");c1.start();c2.start();}
}class Account {private double accountSum;public Account() {this.accountSum = 0;}public Account(double accountSum) {this.accountSum = accountSum;}//存钱public void deppsit(double depositNum) {synchronized (this) {if (depositNum > 0) {accountSum = accountSum + depositNum;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ": 存钱成功,当前余额为:\t" + accountSum);}}}}class Customer extends Thread {private Account acct;public Customer(Account acct) {this.acct = acct;}@Overridepublic void run() {for (int i = 0; i < 3; i++) {acct.deppsit(1000);}}
} 
  • 经典例题:生产者和消费着问题

生产者( Productor)将产品交给店员( Clerk),而消费者( (Customer)从店员处取走产品, 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产; 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

package com.broky.multiThread.exer;/*** - 经典例题:生产者和消费着问题* 生产者( Productor)将产品交给店员( Clerk),而消费者( (Customer)从店员处取走产品,* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,* 如果店中有空位放产品了再通知生产者继续生产; 如果店中没有产品了,店员会告诉消费者等一下,* 如果店中有产品了再通知消费者来取走产品。** 分析:* 1.是多线程问题,可以假设多个消费这和多个生产者是多线程的* 2.存在操作的共享数据,生产和购买时都需要操作经销商的库存存量。* 3.处理线程安全问题。* 4.三个类:生产者,经销商,消费者。经销商被生产者和消费者共享。生产者读取经销商库存,当库存不够时,生产产品* 并发给经销商,操作经销商库存+1。消费者读取经销商库存,当有库存时,方可进行购买,购买完成后,经销商库存-1.*/
public class ProductTest {public static void main(String[] args) {Clerk clerk = new Clerk();Producer p1 = new Producer(clerk);Producer p2 = new Producer(clerk);p1.setName("生产者1");p2.setName("生产者2");Consumer c1 = new Consumer(clerk);Consumer c2 = new Consumer(clerk);c1.setName("消费者1");c2.setName("消费者2");p1.start();c1.start();}
}class Clerk {private int productNum;public Clerk() {this.productNum = 0;}public int getProductNum() {return productNum;}public void setProductNum(int productNum) {this.productNum = productNum;}
}class Producer extends Thread {private Clerk clerk;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始生产......");while(true){try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}produce();}}public Producer(Clerk clerk) {if (clerk != null) {this.clerk = clerk;}}private void produce() {synchronized (ProductTest.class) {ProductTest.class.notify();if (clerk.getProductNum() < 20) {clerk.setProductNum(clerk.getProductNum() + 1);System.out.println(Thread.currentThread().getName() + ":\t生产完成第 " + clerk.getProductNum() + " 个产品");}else {try {ProductTest.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}class Consumer extends Thread {private Clerk clerk;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始消费......");while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}buy();}}public Consumer(Clerk clerk) {if (clerk != null) {this.clerk = clerk;}}private void buy(){synchronized (ProductTest.class) {ProductTest.class.notify();if (clerk.getProductNum() > 0) {System.out.println(Thread.currentThread().getName() + ":\t购买完成第 " + clerk.getProductNum() + " 个产品");clerk.setProductNum(clerk.getProductNum() - 1);}else {try {ProductTest.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/388304.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【屏驱MCU】RT-Thread 文件系统接口解析

本文主要介绍【屏驱MCU】基于RT-Thread 系统的文件系统原理介绍与代码接口梳理 目录 0. 个人简介 && 授权须知1. 文件系统架构1.1 虚拟文件系统目录架构 2. menuconfig 分析3. 代码接口分析3.1 DFS框架挂载目录3.2 【FAL抽象层】分区表和设备表3.3 如何将【文件路径】挂…

多任务协程处理的流程,看看是否和你想像的一样

import time import asyncioasync def func1():print("你好&#xff0c;我是第一个任务")await asyncio.sleep(3)print("你好&#xff0c;我是第二个任务")async def func2():print("你好&#xff0c;我是第3个任务")await asyncio.sleep(2)…

GNSS形变监测系统

TH-WY1 GNSS形变监测系统采用扼流圈设计有以下几个优势&#xff1a; 高精度测量&#xff1a;扼流圈是一种高精度的传感器&#xff0c;可以提供非常精确的测量结果。这使得GNSS形变监测系统能够准确地测量结构物的形变变化。 高稳定性&#xff1a;扼流圈设计使得传感器具有良好…

第33篇 计算数据中最长的连续1的个数<三>

Q&#xff1a;如何将计算出的结果&#xff08;最长的连续1的个数&#xff09;显示在DE2-115开发板的HEX上&#xff1f; A&#xff1a;基本原理&#xff1a;DE2-115_Computer_System中的HEX并行端口作为内存映射设备连接到DE2-115开发板的七段数码管&#xff0c;每个端口都对应…

大模型提示工程(Prompt),让LLM自己优化提示词

前言 随着大家对于prompt提问的研究以及对于高质量回答的追求&#xff0c;现在有一个比较热的词叫做prompt creator。 Prompt Creator 实际上是使得 ChatGPT 更好的引导你去完善自己的提问&#xff0c;同时也完善自己的回答&#xff0c;更好地指导自己回答出更加令使用者满意…

win10桌面任务栏美化(不用软件)(任务栏应用居中,透明任务栏)

透明任务栏 1、打开设置——个性化——颜色&#xff0c;打开透明效果&#xff1b; 2、在搜索框搜索注册表编辑器&#xff1b; 3、找如下路径&#xff1a;计算机\HKEY-CURRENT-USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced&#xff1b; 4、寻找文件&a…

【TS】TypeScript类型断言:掌握类型转换的艺术

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 TypeScript类型断言&#xff1a;掌握类型转换的艺术1. 引言2. 什么是类型断言&a…

链表的实现(C++版)

对于链表的学习,之前在C语言部分的时候就已经有学习过,也学会了使用C语言来打造一个链表.如今学了C 则想通过C来打造一个链表,以达到锻炼自己的目的. 1.链表的初步实现 1.节点模板的设置 template <class T> struct ListNode{ListNode <T>* _next;ListNode <T…

k8s学习--使用kubepshere部署devops项目时遇到的报错(无法找到gitee仓库)

今天在kubesphere部署devops项目&#xff0c;编辑流水线的时候&#xff0c;发现怎么也访问不到gitee仓库 报错的流水线位置 报错日志 报错原因 变量问题 因为看见了csy/sangomall&#xff0c;所以理所当然的把路径变量GITEE_ACCOUNT写成了用户名 解决方法 结果发现仓库…

可靠的图纸加密软件,七款图纸加密软件推荐

大家好啊,我是小固,今天跟大家聊聊图纸加密软件。 作为一名设计师,我深知保护自己的知识产权有多重要。曾经就因为图纸泄露,差点血本无归,那个教训可真是惨痛啊!所以我今天就给大家推荐几款靠谱的图纸加密软件,希望能帮到你们。 固信软件https://www.gooxion.com/ 首先要隆重…

Java语言程序设计——篇十一(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是CSDN&#xff0c;我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f33…

Vue 的安装与配置

今天是八月一日&#xff0c;我也开启了Vue的学习&#xff0c;希望和大家一起学编程&#xff0c;相互督促&#xff0c;相互进步&#xff01; 安装vscode 安装Node.js 官网&#xff1a;https://nodejs.org/zh-cn 下载完正常安装就行 可以winr输入cmd&#xff0c;也可以vscod…

springboot智能健康管理平台-计算机毕业设计源码57256

摘要 在当今社会&#xff0c;人们越来越重视健康饮食和健康管理。借助SpringBoot框架和MySQL数据库的支持&#xff0c;开发智能健康管理平台成为可能。该平台结合了小程序技术的便利性和SpringBoot框架的快速开发能力&#xff0c;为用户提供了便捷的健康管理解决方案。 通过智能…

【多线程】单例模式

&#x1f3c0;&#x1f3c0;&#x1f3c0;来都来了&#xff0c;不妨点个关注&#xff01; &#x1f3a7;&#x1f3a7;&#x1f3a7;博客主页&#xff1a;欢迎各位大佬! 文章目录 1. 什么是单例模式1.1 理解单例模式1.2 单例模式的特点 2. 饿汉模式3. 懒汉模式3.1 单线程下的懒…

中国人民解放军建军97周年

缅怀先烈&#xff0c;砥砺前行 付吾辈之韶华&#xff0c;耀吾辈之中华! 万里河山&#xff0c;有您心安!

Django REST Framework(十五)路由Routes

如何在Django REST framework中利用SimpleRouter和DefaultRouter来高效生成视图集的路由信息,并详细解释如何使用action装饰器为视图集中的自定义方法生成路由 1.路由的定义规则 路由称为URL(Uniform Resource Locator,统一资源定位符),也可以称为URLconf,是对可以从互联…

【Java题解】杨辉三角—力扣

&#x1f389;欢迎大家收看&#xff0c;请多多支持&#x1f339; &#x1f970;关注小哇&#xff0c;和我一起成长&#x1f680;个人主页&#x1f680; ⭐目前主更 专栏Java ⭐数据结构 ⭐已更专栏有C语言、计算机网络⭐ 题目链接&#xff1a;杨辉三角 目录&#x1f451; ⭐题…

the request was rejected because no multipart boundary was found

文章目录 1. 需求描述2. 报错信息3. 探索过程1. 使用postman 排除后端错误2. 搜索网上的解决方法3. 解决方法 1. 需求描述 想要在前端上传一个PDF 发票&#xff0c;经过后端解析PDF之后&#xff0c;将想要的值自动回填到对应的输入框中 2. 报错信息 org.apache.tomcat.util.…

2024年有哪些开放式耳机值得入手?值得关注的开放式耳机评测大赏

如今&#xff0c;开放式耳机越来越受到人们的关注。2024 年更是涌现出了众多优秀的开放式耳机产品。但在众多选择面前&#xff0c;哪一款耳机的音质更出色&#xff1f;哪一款佩戴起来更舒适&#xff1f;又有哪一款在通话质量和连接性能上表现更优异呢&#xff1f;接下来我将详细…