【Java编程】04_多线程

1. 概述

  在日常生活中,很多事情都是同时进行的。例如,人可以同时进行呼吸、血液循环、思考问题等活动。在使用计算机的过程中,应用程序也可以同时运行,用户可以使用计算机一边听歌,一边玩游戏。在应用程序中,不同的程序块也是可以同时运行的,这种多个程序块同时运行的现象被称作并发执行多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此间可以进行通信

1.1 程序

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

1.2 进程

  进程(process)是程序的一次执行过程,也就是“正在运行的程序”,它代表了程序所占用的内存区域。一个操作系统中,每个独立执行的程序都可称为一个进程。是一个动态的过程:有它自身的产生、存在和消亡的过程。——存在生命周期
如:运行中的QQ,运行中的MP3播放器

在这里插入图片描述

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

  • 在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天。但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。

1.2.1 进程的特点

  1. 独立性
      进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  2. 动态性
      程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的。程序是静态的,进程是动态的
  3. 并发性
      多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响。

1.2.2 并行与并发

在这里插入图片描述

1)单核CPU与多核CPU的理解

  单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)

2)并行与并发的理解

  并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事
  并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

HA(High Availability)高可用:指在高并发的情景中,尽可能的保证程序的可用性,减少系统不能提供服务的时间。

1.3 线程

  线程(thread)线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

  当一个Java程序启动时,就会产生一个进程。该进程会默认创建一个线程,在这个线程上会运行main()方法中的代码。代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序

一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

1.3.1 进程与线程的关系

  进程可进一步细化为线程,是一个程序内部的一条执行路径。每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。

在这里插入图片描述
  一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)。当程序开启多个线程,其中有一个主线程来调用本进程中的其他线程,我们看到的进程的切换,切换的也是不同进程的主线程多线程扩展了多进程的概念,使的同一个进程可以同时并发处理多个任务。
在这里插入图片描述

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的。
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间。它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

何时需要多线程?

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

  多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉

Tips: 使用多线程的优点
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
  
1)多线程程序的优点:

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

2)何时需要多线程

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

1.3.2 CPU分时调度

  时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间。

  • 如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起。

  • 如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。

注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:

  1. FCFS(First Come First Service 先来先服务算法)
  2. SJS(Short Job Service短服务算法)
    在这里插入图片描述

2. 线程的创建和使用

Java提供了四种多线程实现方式。

  • 继承java. lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码。
  • 实现java. lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。

JDK 5.0新增了两种实现方式

  • 方式一:实现Callable接口。
  • 方式二:使用线程池

2.1 Thread类

Thread类的特性

  • 每个线程都是通过某个特定Thread对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体
  • 通过该Thread对象的 start() 方法来启动这个线程,而非直接调用 run()。
// java API 中Thread类 和 Runnable 接口关于 run()方法
public class Thread implements Runnable{
private Runnable target;
   ...
   public void run() {
       if (target != null) {
           target.run();
       }
   }
}

@FunctionalInterface
public interface Runnable {
   public abstract void run();
}

2.1.1 构造方法

Thread():创建新的Thread对象


Thread(String threadname):创建线程并指定线程实例名


Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接中的run方法


Thread(Runnable target, String name):创建新的Thread对象

2.1.2普通方法

static Thread currentThread( ) : 静态方法,返回对当前正在执行的线程对象的引用


static void yield() :线程让步

  • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
  • 若队列中没有同优先级的线程,忽略此方法

long getId() : 返回该线程的标识


String getName() : 返回该线程的名称


void run() : 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法


static void sleep(long millions) : 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)


void start() : 使该线程开始执行:Java虚拟机调用该线程的run()。


join() : 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。


static void sleep(long millis) : (指定时间:毫秒) : 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。抛出InterruptedException异常


stop() : 强制线程生命期结束,不推荐使用


boolean isAlive() :返回boolean,判断线程是否还活着

2.2 继承Thread类创建多线程

  1. 创建一个继承于Thread类的子类。
  2. 子类中重写Thread类中的run方法 —— 将此线程执行的操作声明在run()中。
  3. 创建Thread类的子类的对象,即创建了线程对象。
  4. 通过此对象调用start():① 启动当前线程 ② 调用当前线程的run()
public class TestThread {

    public static void main(String[] args) {
   		//3. 创建Thread类的子类的对象,即创建了线程对象。
        MyThread myThread1 = new MyThread("明日花绮罗");
        MyThread myThread2 = new MyThread("三上悠亚");
        MyThread myThread3 = new MyThread("深田咏美");
        //4. 通过此对象调用start():① 启动当前线程 ② 调用当前线程的run()
        myThread1.start();
        myThread2.start();
        myThread3.start();
        while (true){
            System.out.println("main方法在执行");
        }
    }
}
//1. 创建一个继承于Thread类的子类。
class MyThread extends Thread{

    public MyThread() {
    }
	//4.创建多参构造方法
    public MyThread(String name) {
        super(name);
    }
	//2. 子类中重写Thread类中的run方法 —— 将此线程执行的操作声明在run()中。
    @Override
    public void run() {
        while (true){
            System.out.println("线程"+ getName() + "在执行");
        }
    }
}

2.3 实现Runnable接口创建多线程

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:① 启动当前线程 ② 调用Runnable子类接口的run方法。
public class TestThread1 {
    public static void main(String[] args) {
        //3. 通过Thread类含参构造器创建线程对象。
        MyThread1 myThread1 = new MyThread1();
        //4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
        Thread thread1 = new Thread(myThread1,"明日花绮罗");
        Thread thread2 = new Thread(myThread1,"三上悠亚");
        Thread thread3 = new Thread(myThread1,"深田咏美");
        //5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
        thread1.start();
        thread2.start();
        thread3.start();

        System.out.println("main程序在执行!");
    }
}
//1. 定义子类,实现Runnable接口。
class MyThread1 implements Runnable{
	//2. 子类中重写Runnable接口中的run方法。
    @Override
    public void run() {
        while (true){
            System.out.println("线程" + Thread.currentThread().getName() +"在执行");
        }
    }
}

Tips:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定
  3. 想要启动多线程,必须调用start方法
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”
  5. 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()

2.4 继承方式和实现方式的联系与区别

public class Thread extends Object implements Runnable

继承Thread类

  • 优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
  • 缺点: 自定义的线程类已继承了Thread类,所以后续无法再继承其他的类。

实现Runnable接口

  • 优点: 自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类(避免了单继承的局限性)。在这种方式下,多个线程可以共享同一个接口实现类的对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想。
  • 缺点: 编程稍微复杂,如想访问当前线程,则需使用 Thread.currentThread() 方法。

区别

  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。

3. 线程的生命周期及状态转换

  在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当 Thread 对象创建完成时,线程的生命周期便开始了。当 run() 方法中代码正常执行完毕或者线程抛出一个未捕获的异常 (Exception) 或者错误 (Error) 时,线程的生命周期便会结束

  线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态( Running)、阻塞状态(Blocked)和死亡状态(Terminated) ,线程的不同状态表明了线程当前正在进行的活动。在程序中,通过一些操作,可以使线程在不同状态之间转换。
在这里插入图片描述

3.1 线程生命周期

1)新建状态(New): 创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。如:Thread t = new MyThread();

2)就绪状态(Runnable): 当调用线程对象的start()方法,线程即为进入就绪状态(也称可运行状态)。

处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,随时等待CPU调度执行,能否获得CPU的使用权开始运行,还需要等待系统的调度。并不是执行了t.start()此线程立即就会执行

3)运行状态(Running): 当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态。

如果处于就绪状态的线程获得了CPU的使用权,开始执行 run() 方法中的线程执行体,则该线程处于运行状态。

当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,就绪状态是进入运行状态的唯一入口,也就是只有处于就绪状态的线程才可能转换到运行状态

4) 阻塞状态(Blocked): 处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。直到其进入就绪状态才有机会被CPU选中再次执行。

一些线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。

  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
  • 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的方法返回。
  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
  • 当线程调用了Thread的sleep( long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
      

需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度

根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:

  • 等待阻塞: 运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态。

  • 同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。

  • 其他阻塞: 调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时。join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态。

5) 死亡状态(Dead): 线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,该线程的生命周期就结束了,线程将不再拥有运行的资格,也不能再转换到其他状态。

3.2 线程的状态

1)由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :

  • 就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可立即执行。
  • 执行(运行)状态:线程已经获得CPU,其程序正在运行的状态。
  • 阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞。
    在这里插入图片描述
  • 就绪 → 执行:为就绪线程分配CPU即可变为执行状态。
  • 执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态。
  • 执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞。
    (例如线程正在访问临界资源,而资源正在被其他线程访问),反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行。

2)我们可以再添加两种状态:

  • 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中。
  • 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统。
    在这里插入图片描述

PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程。

3)线程的七态模型
在这里插入图片描述

4. 线程的调度

  程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

  在计算机中,线程调度有两种模型,分别是分时调度模型抢占式调度模型

  • 分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。
  • 抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。

  Java虚拟机默认采用抢占式调度模型,大多数情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。

4.1 线程的优先级

  在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小
  线程的优先级用1~10之间的整数来表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。

Thread功能描述
stantic int MIN_PRIORITY表示线程的最高优先级,想当于值 10
MAX_PRIORITY表示线程的最低优先级,相当于1
NORM_PRIORITY表示线程的普通优先级,相当于5

  程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。
  然而线程优先级不是固定不变的,可以通过Thread类的setPriority (intnewPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

//定义类MaxPriority实现Runnable接口
class MaxPriority implements Runnable {
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
    }
}
//定义类MinPriority实现Runnable接口
class MinPriority implements Runnable {
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
    }
}

public class TestThread{
    public static void main(String[] args) {
        Thread minPriority = new Thread(new MaxPriority(),"优先级较低的线程");
        Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程");
        minPriority.setPriority(Thread.MIN_PRIORITY);//设置线程优先级为1
        maxPriority.setPriority(10);                 //设置线程优先级为10
        minPriority.start();
        maxPriority.start();
    }
}

在这里插入图片描述
  需要注意的是,虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不能很好的和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段

4.2 线程休眠

  如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis) ,该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

  当前线程调用sleep( long millis)方法后,在指定时间(参数millis)内是不会执行的,这样其他的线程就可以得到执行的机会了。

  sleep(long millis)方法声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

class SleepThread implements Runnable{
    @Override
    public void run(){
        for (int i=1;i<10;i++){
            if(i == 3){
                try {
                    Thread.sleep(2000);//当前线程休眠2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程一正在输出:"+i);
                try {
                    Thread.sleep(500);//当前线程休眠500毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestThread {
    public static void main(String[] args) throws Exception{
        //创建一个线程
        new Thread(new SleepThread()).start();
        for(int i=1;i<10;i++) {
            if (i == 5) {
                Thread.sleep(2000);
            }
            System.out.println("主线程正在输入:"+i);
        }
        Thread.sleep(500);
    }
}

在这里插入图片描述
注意,sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。

4.3 线程让步

  在校园中,我们经常会看到同学互相抢篮球,当某个同学抢到篮球后就可以拍一会,之后他会把篮球让出来,大家重新开始抢篮球,这个过程就相当于Java程序中的线程让步。

  线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。接下来通过一个案例来演示一下yield()方法的使用。

package com.jt.thread;

public  class YieldThread extends Thread{
    public YieldThread(String name){
        super(name);//调用父类的构造方法
    }

    public  void run(){
        for (int i=0;i<6;i++){
            System.out.println(Thread.currentThread().getName()+ "---" +i);
            if(i == 3){
                System.out.println("线程让步:");
                Thread.yield();//线程运行到此,做出让步
            }
        }
    }
}

class Example{
    public static void main(String[] args) {
        Thread t1 = new YieldThread("线程A");
        Thread t2 = new YieldThread("线程B");
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
上面创建了两个线程t1和t2,它们的优先级相同。两个线程在循环变量i=3时,都会调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行。

4.4 线程插队

现实生活中经常能碰到 “插队” 的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行

package com.jt.thread;

public class JoinThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i<6;i++){
            System.out.println(Thread.currentThread().getName()+ "输入:" +i);
            try{
                Thread.sleep(500);  //线程休眠500毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class ExampleJoin{
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new JoinThread(),"线程一");
        t.start();

        for (int i = 0; i<6;i++){
            System.out.println(Thread.currentThread().getName()+ "输入:" +i);
            if(i == 2){
                t.join();
            }
            Thread.sleep(500);
        }
    }
}

在这里插入图片描述
上面main线程中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法。

这时,t 线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。

5. 多线程同步

5.1 同步与异步

同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。坏处就是效率会降低,不过保证了安全

同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。坏处就是有安全隐患,效率要高一些

5.2 线程安全

  异步线程会带来安全隐患问题。如线程异步模拟售票程序,开启四个窗口售票。

public class Test {
    
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket,"一号黄牛");
        Thread thread2 = new Thread(ticket,"二号黄牛");
        Thread thread3 = new Thread(ticket,"三号黄牛");
        Thread thread4 = new Thread(ticket,"四号黄牛");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Ticket implements Runnable {

    @Override
    public void run() {
        int tickets = 100;

        while (true){

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(tickets <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName()
                    +"卖出一张票,余"+ --tickets +"张票");
        }
    }
}

在这里插入图片描述
执行结束 ① 产生超卖,0 张 、-1张、-2张。② 产生重卖,同一张票卖给多人。
多线程出现了安全问题:

1)问题的原因:

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

2) 解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

Tips:
1)多线程安全问题是如何出现的?

  • 常见情况是由于线程的随机性+访问延迟。

2)以后如何判断程序有没有线程安全问题?

  • 在多线程程序中 + 有共享数据 + 多条语句操作共享数据就一定会存在线程安全问题。

5.3 同步代码块

  多线程的场景和共享数据的条件是改变不了的(就像4个窗口一起卖100张票,这个是业务),所以思路可以从第3点"多条语句操作共享数据"入手,既然是在这多条语句操作数据过程中出现了问题

那我们可以把有可能出现问题的代码都包裹起来,一次只让一个线程来执行。

  为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被锁住的代码就是同步的,被称作同步代码块,其语法格式如下:

synchronized (锁对象){
	需要被同步的代码(也就是可能出现问题的操作共享数据的多条语句);
}

  锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞。

  等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。

public class Test {

    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        Thread thread1 = new Thread(tickets,"一号黄牛");
        Thread thread2 = new Thread(tickets,"二号黄牛");
        Thread thread3 = new Thread(tickets,"三号黄牛");
        Thread thread4 = new Thread(tickets,"四号黄牛");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Tickets implements Runnable {
    Object ticketLock = new Object();
    int tickets = 100;

    @Override
    public void run() {
        while (true){

            synchronized (ticketLock){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(tickets <= 0){
                    break;
                }
                
                System.out.println(Thread.currentThread().getName()
                        +"卖出一张票,余"+ --tickets +"张票");
            }
        }
    }
}

同步效果的使用有两个前提:
前提1:同步需要两个或者两个以上的线程 (单线程无需考虑多线程安全问题)
前提2:多个线程间必须使用同一个锁(我上锁后其他人也能看到这个锁,不然我的锁锁不住其他人,就没有了上锁的效果)

5.4 同步方法

  当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:

public synchronized void show (String name){ 
...
}

  被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

public class Test {

    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        Thread thread1 = new Thread(tickets,"一号黄牛");
        Thread thread2 = new Thread(tickets,"二号黄牛");
        Thread thread3 = new Thread(tickets,"三号黄牛");
        Thread thread4 = new Thread(tickets,"四号黄牛");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Tickets implements Runnable {
    Object ticketLock = new Object();
    int tickets = 100;

    @Override
    public void run() {
        while (true){
            if(tickets <= 0){
                break;
            }
            saleTickets();
        }
    }

    public synchronized void saleTickets(){//同步方法
       if(tickets > 0){
           try {
               Thread.sleep(10);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           System.out.println(Thread.currentThread().getName()
                   +"卖出一张票,余"+ --tickets +"张票");
       }
    }
}

5.5 同步锁机制

  在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

5.5.1 synchronized的锁是什么?

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

  • 同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class
    如果使用Thread继承的方式来实现多线程必须使用类名.class的方法上锁,因为每开启一个线程就会创建一个对象实例。

Tips:

  • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全。
  • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
  • 如果是继承的方式的话,锁对象最好用"类名.class",否则创建自定义线程类多个对象时,无法保证锁的唯一

1)为什么同步代码块的锁对象可以是任意的同一个对象,但是同步方法使用的是this呢?

  • 因为同步代码块可以保证同一个时刻只有一个线程进入但同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步

2)同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?

  • 同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。

3)如果不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?

  • Java中静态方法的锁是该方法所在类的class对象,该对象可以直接用 “ 类名. class " 的方式获取,有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用 “ 类名.方法名() ” 的方式调用。

5.5.2 同步的范围

1)如何找问题,即代码是否存在线程安全?(非常重要)

  1. 明确哪些代码是多线程运行的代码
  2. 明确多个线程是否有共享数据
  3. 明确多线程运行代码中是否有多条语句操作共享数据

2)如何解决呢?(非常重要)

  对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

3)切记:

范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。

5.5.3 释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

5.5.4 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
  • 应尽量避免使用suspend()和resume()来控制线程。

5.6 死锁问题

有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休。

中国人:“你先给我筷子,我再给你刀叉!”,美国人: “你先给我刀叉,我再给你筷子!”结果可想而知,两个人都吃不到饭。

这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

public class DeadLock {
    public static void main(String[] args) {
        DeadLockThread d1 = new DeadLockThread(true);
        DeadLockThread d2 = new DeadLockThread(false);
        new Thread(d1,"Chinese").start();
        new Thread(d2,"American").start();
    }
}

class DeadLockThread implements Runnable{
    static Object chopsticks = new Object();
    static Object knifeAndFork = new Object();

    private boolean flag;

    public DeadLockThread(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            while (true){
                synchronized (chopsticks){
                    System.out.println(Thread.currentThread().getName()
                    +"---if---chopsticks");
                }
                synchronized (knifeAndFork){
                    System.out.println(Thread.currentThread().getName()
                            +"---if---knifeAndFork");
                }
            }
        }else {
            while (true){
                synchronized (knifeAndFork){
                    System.out.println(Thread.currentThread().getName()
                            +"---else---knifeAndFork");
                }
                synchronized (chopsticks){
                    System.out.println(Thread.currentThread().getName()
                            +"---else---chopsticks");
                }
            }
        }
    }
}

上面,创建了Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。

Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕,而American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕,两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于了挂起状态,从而造成了死锁。

6. 线程池

在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗

线程池的创建

那么,我们应该如何创建一个线程池呐?

Java中已经提供了创建线程池的一个类:Executor

而我们创建时,一般使用它的子类:ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:

Java线程池构成分析

在这里插入图片描述

参数 :

  • corePoolSize 核心线程数
    核心线程会一直存活,即使没有任务需要执行
    当线程数小于核心线程数时,即使核心线程池中有线程空闲,线程池也会优先创建新线程处理,直到池中的线程达到核心线程数为止
    设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
  • maximumPoolSize 允许在池中的最大线程数
    当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
    当线程数=maxPoolSize,且任务队列已满时。线程池会拒绝处理任务而抛出异常
  • keepAliveTime 线程空闲时间
    当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
    如果allowCoreThreadTimeout=true,则会直到线程数量=0
  • TimeUnit 时间单位
    TimeUnit是枚举实例,指定颗粒度单位
    TimeUnit提供了可读性更好的线程暂停操作,通常用来替换Thread.sleep()。
    记住TimeUnit.sleep()内部调用的Thread.sleep()也会抛出InterruptException。
  • allowCoreThreadTimeOut 允许核心线程超时
  • RejectedExecutionHandler 任务拒绝处理器
    达到两种情况会拒绝处理任务:当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。

    当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown
    如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务,线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常

线程池任务执行过程

在这里插入图片描述

总结

线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收。

maximumPoolSize就是线程池中可以容纳的最大线程的数量。

keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间。

util,就是计算这个时间的一个单位。

workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。

threadFactory,就是创建线程的线程工厂。

最后一个handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。

ThreadPoolExecutor类有几个内部实现类来处理这类情况:

  • AbortPolicy 不执行新任务,直接抛出异常,提示线程池已满
  • CallerRunsPolicy 直接调用execute来执行当前任务
  • DiscardPolicy 忽视,不执行新任务,也不抛出异常
  • DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务实现RejectedExecutionHandler接口,可自定义处理器

ThreadPoolExecutor参数默认值

corePoolSize=1
queueCapacity=Integer.MAX_VALUE
maxPoolSize=Integer.MAX_VALUE
keepAliveTime=60s
allowCoreThreadTimeout=false
rejectedExecutionHandler=AbortPolicy()

线程拒绝执行异常

在这里插入图片描述

四种常见的线程池:

CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class ThreadPoolTests {
    public static void main(String[] args)
    {
        //1.核心线程数
        int corePoolSize = 2;
        //2.最大线程数
        int maximumPoolSize = 3;
        //3.线程最大空闲时间
        long keepAliveTime = 60;
        //4.时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        //5.任务队列(阻塞式队列)
        BlockingQueue<Runnable> workQueue =
                new ArrayBlockingQueue<>(1);
        //6.定义拒绝策略(可选,常用有四种)
        RejectedExecutionHandler handler =
                //当池无法处理这个任务时,由任务启动方去执行任务
                new ThreadPoolExecutor.CallerRunsPolicy();
        //7.构建线程工厂(可选,最关键是要给线程一个友好的名字)
        ThreadFactory factory = new ThreadFactory() {
            //线程名前缀
            private String namePrefix = "cgb-2015-thread-";
            //构建一个线程安全的原子自增自减对象
            private AtomicLong atomicLong = new AtomicLong(1);

            @Override
            public Thread newThread(Runnable r)
            {
                //r 为任务
                return new Thread(r, namePrefix + atomicLong.getAndIncrement());
            }
        };

        //8.创建线程池
        ThreadPoolExecutor pool =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        keepAliveTime,
                        unit,
                        workQueue,
                        factory,
                        handler);
        //7.将任务交给池中对象去执行
        pool.execute(() -> {
            String tName =
                    Thread.currentThread().getName();
            System.out.println(tName + "->任务1");
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
            }
        });

        pool.execute(() -> {
            String tName =
                    Thread.currentThread().getName();
            System.out.println(tName + "->任务2");
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
            }

        });

        pool.execute(() -> {
            String tName =
                    Thread.currentThread().getName();
            System.out.println(tName + "->任务3");
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
            }
        });

        pool.execute(() -> {
            String tName =
                    Thread.currentThread().getName();
            System.out.println(tName + "->任务4");
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
            }

        });

        pool.execute(() -> {
            String tName =
                    Thread.currentThread().getName();
            System.out.println(tName + "->任务5");

        });

        pool.shutdown();//不再接收任务,等现有任务执行结束将池关掉
        // pool.shutdownNow();//不再接收任务,但有可能会将现有任务也会停止
    }
}

7. 多线程通信

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值