多线程

1. 1 并发与并行

  • 并发:指两个或多个事件在同一时间段内发生。
  • 并行:指的是两个或多个事件在同一时刻发生(同时发生)。

1.2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序

java 运行超时 java运行一段时间变慢_线程池

线程的调度

  • 分时调度
    所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
  • 抢占式调度
    优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度

1.3创建线程类

Java中使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建线程对象
  3. 调用线程对象的start()方法来启动该线程

实现步骤

  1. 创建一个Thread类的子类
  2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务
  3. 创建Thread类的子类对象
  4. 调用Thread类中的方法start方法,开启新的线程,执行run方法
    void start() 使该线程开始执行。Java虚拟机调用该线程的run方法。
    结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)。
    多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
    java程序属于抢占式调度,哪个线程的优先级别高,哪个线程优先执行;同一个优先级,随机选择一个执行

Thread类

有关线程的一些方法;

  1. public Thread():分配一个新的线程对象
  2. public Thread (String name):分配一个指定的名字的新的线程对象
  3. public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  4. public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字

常用方法:

  1. public String getName(): 获取当前线程名称
  2. public void start(): 导致此线程开始执行;Java虚拟机调用此线程的run方法
  3. public void run():此线程要执行的任务在此处定义代码。
  4. public static void sleep(long mills):使当前正在执行的线程以指定的毫秒数暂停
  5. public static Thread currentThread()返回对当前正在执行的线程对象的引用。

1.4 创建线程方式二

步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象。该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法来启动线程

实现步骤:

  1. 创建一个Runnable接口的实现类
  2. 在实现类中重写Runnable接口的run 方法,设置线程任务
  3. 创建一个Runnable接口的实现类对象
  4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
  5. 调用Thread类中的start方法,开启新的线程执行run方法

1.5 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源
  2. 可以避免java中的单继承的局限性
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类

ps: 在java中,每次程序运行至少启动2个线程,一个是main线程,一个垃圾回收的线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM就是一个进程。

线程安全原理图

java 运行超时 java运行一段时间变慢_线程池_02

1.6 线程的同步技术

有以下三种实现方式

  1. 同步代码块
  2. 同步方法
  3. 锁机制

1.6.1 同步代码块

解决线程安全问题的一种方案:使用同步代码块

synchronized(锁对象){
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

PS:

  1. 通过代码块中的锁对象,可以使用任意的对象
  2. 但是必须保证多个线程使用的锁对象是同一个
  3. 锁对象作用:
    把同步代码块锁住,只让一个线程在同步代码块中执行

同步技术的原理图

java 运行超时 java运行一段时间变慢_线程池_03

1.6.2 同步方法

使用步骤:

  1. 把访问了共享数据的代码抽取出来,放到一个方法中
  2. 在方法上添加synchronized修饰符
修饰符 synchronized 返回值 方法名(参数列表){
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

ps:同步方法也会把方法内部的代码锁住

只让一个线程执行

同步方法的锁对象就是实现类对象 new RunnableImpl也就是this

1.6.3 Lock锁对象

Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。

Lock接口中的方法:

void lock()获取锁

void unlock()释放锁

使用步骤:

  1. 成员位置创建一个ReentrantLock对象
  2. 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
  3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

1.7 线程的状态

1.7.1线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中有以下六种状态

线程状态

导致状态发生的条件

NEW(新建)

线程刚被创建,但是并未启动。还没调用start方法。

Runnable(可运行)

线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器

Blocked(锁阻塞)

当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

Waiting(无限等待)

一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同waiting状态,有几个方法超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、Object.wait

Terminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡,

线程状态图

java 运行超时 java运行一段时间变慢_编程语言_04

Timed Waiting线程状态图

java 运行超时 java运行一段时间变慢_java_05

锁阻塞状态图

java 运行超时 java运行一段时间变慢_编程语言_06

无限等待状态图

java 运行超时 java运行一段时间变慢_多线程_07

无限等待的状态的,等待和唤醒需用公用同一个锁对象,才能实现。

进入到TimeWaiting(计时等待)有两种方式

  1. 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
  2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

唤醒的方法:

  1. void notify() 唤醒在此对象监视器上等待的单个线程。
  2. void notifyAll()唤醒在此对象监视器上等待的所有线程

1.8等待唤醒机制

1.8.1 线程间的通信
概念:

多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

为什么要处理线程间的通信:

多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同是,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即–等待唤醒机制

1.8.2 等待唤醒机制
什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但线程间也会有协作机制。

等待唤醒机制就是一个线程在进行了规定操作后,就进入等待状态wait(),等待其他线程执行完他们的指定代码过后再将其唤醒notify()在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程wait/notify就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这是的线程状态即是WAITING。它还要等着别的线程执行一个特别的动作,即是通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的wait set 中的一个线程释放;等候时间最长的,优先释放。
  3. notifyAll:则释放所通知对象的wait set 上的全部线程

ps:哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行

总结如下:

  • 如果能获取锁,线程就从WAITING状态变成RUNNABLE 状态;
  • 否则,从wait set 出来,又进入entry set,线程就从WAITING状态又变成BLOCKED状态
调用wait和notify方法需要注意的细节
  1. wait方法的notify方法必须要由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

1.9 线程池

1.9.1 线程池思想概述

我们使用线程的时候就去创建一个线程,这样实现起来非常方便,但是会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间

而线程池就是一种可以复用线程的方法,就是执行完一个任务之后,并不销毁,可以继续执行其他的任务。

底层原理图

java 运行超时 java运行一段时间变慢_java_08

1.9.2 线程池概念
  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源。

java 运行超时 java运行一段时间变慢_多线程_09

合理利用线程池能够带来的好处:

  1. 降低资源消耗。减少创建和销毁线程的此时,每个工作线程都可以被重复利用,可执行多个任务
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而使服务器宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存就越大,最后死机)
1.9.3线程池的使用
在java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executor类中的静态方法:
 static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
 参数:
	int nThreads:创建线程池中包含的线程数量
 返回值:
	ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收
    java.util.concurrent.ExecutorService:线程池接口
    用来从线程池中获取线程,调用start方法,执行线程任务
    	submit(Runnable task) 提交一个Runnable任务用于执行
    关闭/销毁线程池的方法
        void shutdown()

线程池的使用步骤:

  1. 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
  2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务
  3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
  4. 调用ExecutorService中的方法shutdown销毁线程池