Java多线程知识概括

363 阅读22分钟

@TOC

线程基本介绍

程序、进程、线程

  • 程序:指的是一些了用某种语言编写的指令的集合,也可以说是一段静态的代码。
  • 进程:指的是运行起来的程序,也就是动态程序。
  • 线程:指的是进程中某一个执行路径。

单核和多核

  • 单核:指的是单个cpu,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。但是因为CPU时间单元特别短,因此感觉不出来。
  • 多核:指的是多个cpu,自然可以处理多线程。

并行,串行,并发

  • 并行;指的是多个cpu(多核)同时处理多个任务。
  • 串行:指的是多个任务,执行时一个任务执行完再执行另一个任务。
  • 并发:指的是一个CPU(采用时间片)同时执行多个任务。

线程的创建和使用

线程创建有两种方式

  • 创建一个子类继承Thread父类,重写run()方法。然后实例化该子类,最后调用子类的start()方法。
  • 创建子类类实现Runnable接口,实现run()方法,然后通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。调用Thread类的start方法。
  • 注意: ①Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。

线程的有关方法

  • sleep()方法指的是使调用该方法处的代码所处的线程阻塞指定毫秒数,等时间过去后再返回执行。
  • join()方法指的是使调用方法的线程强制抢占调用该代码处的线程cpu时间。直至调用方法的线程执行结束时间结束。 ①join(); ②join(time);
  • yield()方法指的是使调用该方法的处的代码所处的线程返回到就绪状态,即让出cpu时间执行权。

线程的调度

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。对高优先级,使用优先调度的抢占式策略
  • 线程的优先级等级: ①MAX_PRIORITY:10 ②MIN _PRIORITY:1 ③NORM_PRIORITY:5
  • 线程创建时继承父线程的优先级。
  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的分类

  • Java中的线程分为两类: ①守护线程 ②用户线程。
  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
  • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
  • Java垃圾回收就是一个典型的守护线程。
  • 若JVM中都是守护线程,当前JVM将退出。
  • 形象理解:兔死狗烹,鸟尽弓藏

线程的生命周期

线程的生命周期:

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

线程的同步(加锁)

线程的同步问题:

  • 当两个线程访问同一数据(成为共享数据)时,如果某一线程通过判断条件进入访问数据代码块,然后第一个线程进入数据代码块后被阻塞了,这时共享数据还未被改变,另外一个新进程也可以通过判断条件进入访问数据代码块修改数据,然后第二个线程执行完,第一个线程阻塞结束,会从原来阻塞的位置继续执行。此时就会发生原来数据已经被第二个线程修改了。这样就发生了数据安全问题。
  • 这样的问题就是线程的安全问题,因此为了让线程安全,我们必须使线程同步进行。

线程的同步有三种方式

  • Synchronized代码块:使用Synchronized代码块时,我们要可以让任意一个对象或类充当锁。
  • Synchronized方法:Synchronized方法时,在静态方法中系统默认调用当前类充当锁,在非静态方法只能系统默认当前对象充当锁。
  • Lock锁:在Lock锁中我们会创建一个ReentrantLock 类实例来充当锁。通过lock()开启锁,unlock()关闭锁。

线程的同步详解

  • Synchronized代码块,Synchronized方法锁的开启和关闭是自动的 ,隐式的。
  • Lock是显式的。

死锁

  • 死锁解释:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃,自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 解决方法:专门的算法、原则,尽量减少同步资源的定义,尽量避免嵌套同步

线程同步的应用:单例设计模式之懒汉式:

  • 单例设计模式中懒汉模式是线程不安全的,因此创建实例的代码在方式中,如果一个线程通过判断条件后被阻塞了,这时另外一个线程可以创建多一个实例。
  • 因此我们可以用上面三种同步机制保证线程安全。

线程的通信

wait() 与 notify() 和 notifyAll()

  • wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()notifyAll()方法唤醒指定时间过去后,唤醒后等待重新获得对监视器的所有权后才能继续执行。 ①wait(); ②wait(time);
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
  • notifyAll():唤醒正在排队等待资源的所有线程结束等待
  • 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
  • 这三种方法都必须在Synchronized代码块或Synchronized方法中使用。

wait()方法和 sleep()方法的区别:

  • 由于sleep()方法是Thread类的方法,因此它不能改变对象的锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程仍然无法访问这个对象。而wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。
  • sleep(): ①属于Thread类,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态 ②sleep方法没有释放锁 ③sleep必须捕获异常 ④sleep可以在 任何地方使用
  • wait(): ①属于Object,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程 ②wait方法释放了锁 ③wait不需要捕获异常 ④wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用
  • 链接:java 中的 wait()方法和sleep()方法的区别

await()与signal()与signalAll():

  • Condition是在java1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
  • Condition是个接口,基本的方法就是await()和signal()方法。
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。 ①Conditon中的await()对应Object的wait(); ②Condition中的signal()对应Object的notify(); ③Condition中的signalAll()对应Object的notifyAll()。

中断线程

停止线程方式:

  • 使用标志在这里插入图片描述
  • stop方法终止线程,但是由于该方法过于暴力已经被弃用!
  • 使用中断线程
  • 注意: ①suspend() 和 resume() 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。 ②suspend和resume就是废的。

中断线程简介:

  • 线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。
  • 线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
  • 判断线程是否被中断: ①判断某个线程是否已被发送过中断请求。 ②可以使用isInterrupted()方法。因为调用interrupt()方法将线程中断标示位设置为true后,再调用isInterrupted()方法不会立刻清除中断标示位,即不会将中断标设置为false。 ③也可以使用interrupted()静态方法,不过这一方法会产生副作用,如果当前中断标示位为true,它会将当前线程的中断状态重置为false。

如何中断线程:

  • 调用interrupte()方法之后,则根据线程当前的状态进行不同的后续操作。如果线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作: ①如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException; ②如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证) ③如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)
  • 注意: ①如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。 ②通过上面的分析,我们可以总结,调用线程类的interrupt方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupt方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
  • 总结:没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求。
  • 链接: ①线程中断详解线程中断分析
  • 演示: 在这里插入图片描述 在这里插入图片描述

Fork-Join框架

Fork-Join框架:

  • Fork和Join是java1.7提供的用于定型执行的框架,将大任务切分成若干个小任务执行,小任务执行结果汇总成大任务的框架。从字面上理解就是Fork把大任务切分成若干个小任务Join就是把小任务合并得到大任务结果。使用工作窃取算法。
  • 工作窃取算法: ①从其他线程里获取工作任务得一种算法。使用工作窃取算法可以方便我们将大任务切分成多个小任务。为了减少线程间的竞争,我们为每个任务分别放入不同的队列里,线程和队列一一对应,但是有些线程会先把任务做完,这些做完了自己任务的线程就去帮助其他线程进行任务,这是他们会访问同一个队列。 ②为了减少窃取线程和别窃取线程之间的竞争我们通常使用双端队列。被窃取线程永远从双端队列的头部获取任务,窃取线程永远从双端队列的尾部获取内容。
  • 优点: ①就是充分利用线程进行并行计算,减少线程间的竞争。缺点是还是存在竞争比如在队列中只有一个任务时,同时也消耗了更多的系统资源创建更多的线程。
  • 局限: ①在使用Fork/Join只能使用Fork和Join进行同步操作,如果在使用了其他机制时工作线程就不能进行其他操作了。比如在Fork/Join框架中使用了失眠,那么在睡眠过程中就不能执行其他操作了。 ②使用Fork/Join操作的线程不能执行io操作。 ③不能抛出检查异常,必须使用必要的代码来检查他们。

JDK5.0新增线程创建方式

实现Callable接口

  • Callable接口:实现Callable接口和实现Runnbale接口类似但不同的是实现Callable接口是的run方法是有一个返回值的,这个返回值要用一个FutureTask类接受,然后调用FutureTask类的get()方法获取返回值。

使用线程池:

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor.

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable.

  • Runnable Future submit(Callable task):执行任务,有返回值,一般又来执行Callable.

  • void shutdown() :关闭连接池

  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池

  • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池

  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。

多线程总结

多线程总结:

  • 线程创建方式: ①继承Thread父类 ②实现Runnable接口 ③实现Callable接口 ④使用线程池
  • 同步方式: ①Synchronized代码块 ②Synchronized方法 ③Lock锁
  • 通信方式: ①Synchronized相关的wait() 与 notify() 和 notifyAll() ②Lock相关的await()与signal()与signalAll()

多线程注意的点:

  • 进行线程同步是因为有多个线程需要对公共资源进行操作,因此需要加锁。 ①而这个锁一般不是公共资源本身,因为只需要做到不让方法进行到操作公共资源那一步即可。 ②因此可以把对象充当锁,但这个多个线程加的锁必须是同一把锁。 <1>Synchronized的锁可以是任意对象。 <2>Lock的锁是ReentrantLock类。 ③因此从竞争公共资源就是竞争锁。
------------------如下:公共资源是tickts ,充当锁的是当前对象------------------
class MyRunnable implements Runnable {
	private int tickts = 10;
	@Override
	public void run() {
	// 同步方法:同步的是当前对象
	private synchronized void saleTickts() {}
	}
}	
  • 为什么线程需要通信? ①可以大致认为线程进程就是你们家小区一个个房间。平时大家都是各自住各自的对吧。不然就乱套了。 ②但总有时候: <1>需要串串门:“老李啊,我们家今天装修,要楼道里放一钢琴,您走路留意着点啊。““好嘞,我就呆屋里,不碍的“ <2>或者“张阿姨,我下午有点事,麻烦您帮我幼儿园接下明明。今天我刚包了饺子,您尝尝““嗨,客气嘛。下午没问题。“这种行为我们称为跨进程通信。 ③为啥要通信,因为不通信办不成事啊。上面两个例子代表了进程间协作的两个典型场景, <1>第一个叫做“互斥”,就是有一个进程/线程要独占一个资源; <2>第二个叫做“同步”,就是排好接下来的进程/线程执行的顺序。
  • 该章节主要针对两种资源:CPU资源公共资源(锁资源)。
  • 死锁是这样一种情形: ①多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
class LockA implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockA 开始执行");
         while(true){
            synchronized (LockTest.obj1) {
               System.out.println(new Date().toString() + " LockA 锁住 obj1");
               Thread.sleep(3000); // 此处等待是给B能锁住机会
               synchronized (LockTest.obj2) {
                  System.out.println(new Date().toString() + " LockA 锁住 obj2");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
class LockB implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockB 开始执行");
         while(true){
            synchronized (LockTest.obj2) {
               System.out.println(new Date().toString() + " LockB 锁住 obj2");
               Thread.sleep(3000); // 此处等待是给A能锁住机会
               synchronized (LockTest.obj1) {
                  System.out.println(new Date().toString() + " LockB 锁住 obj1");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

Synchronized线程同步与通信:

  • Synchronized通信机制简介: ①因为任何对象都可以充当Synchronized的锁,因此wait() 与 notify() 和notifyAll()的方法必须由Object类实现。且调用 wait()/notify()/notifyAll() 方法的线程必须是持有该对象的锁的线程。
  • Synchronized通信机制详解: ①Java中创建的每个对象都有一个关联的监视器Monitor(也就是互斥锁)。在任何给定时间,只有一个线程可以拥有该监视器。 ②在Java中使用此监视器来实现同步。当任何线程进入同步方法/块时,它要获取指定对象的锁。当任何线程获得锁时,也就是说它已获取了该监视器。所有其他需要执行相同同步代码(锁定监视器)的线程将被挂起,直到最初获取锁的线程释放它为止。 ③wait方法告诉当前线程(在同步方法或块内执行代码的线程)放弃监视器并进入等待状态(waiting state)。 ④notify方法唤醒正在此对象监视器上等待的单个线程。 ⑤notifyAll方法唤醒在同一对象上调用wait()的所有线程。 ⑥任何方法或代码块,如果没有使用关键字synchronized限定,可以在任何时间由多个线程执行,因为对象的监视器(锁)没有被使用。在同步方法(或存在同步块)的情况下,只有获取对象监视器的单个线程才能访问该代码。
  • Monitor监视器: ①每一个对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。 ②java对象头里的Mark Word默认数据是存储对象的HashCode或等信息。但是在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。 在这里插入图片描述

Lock线程同步与通信:

  • Monitor监视器与Condition接口的区别: 在这里插入图片描述
  • Condition的实现: ①主要包括等待队列,等待和通知。 ②等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程。
  • 链接: ①Java中的Condition类ReentrantLock和Synchronized的区别和原理

操作系统与Java中线程的生命周期和线程状态:

  • 链接:线程的生命周期及五种基本状态

  • 操作系统中的线程状态: ①创建:线程从创建到被cpu执行之前的这个阶段。 ②就绪:指线程已具备各种执行条件,一旦获取cpu便可执行。 ③运行:表示线程正获得cpu在运行。 ④阻塞:指线程在执行中因某件事而受阻,处于暂停执行的状态,阻塞的线程不会去竞争cpu。 ⑤终止:线程执行完毕,接下来会释放线程占用的资源。

  • 操作系统中线程的生命周期图如下(进程与线程生命周期一样): 在这里插入图片描述

  • Java中线程的状态:Thread类中维护类一个内部枚举类State,里面是线程的状态,属性如下: ①NEW:表示未启动的线程。 ②RUNNABLE:表示线程正在JVM中执行,但执行的动作也可能是“等待”:等待操作系统的某些资源如处理器。 ③BLOCKED:表示线程正在等待一个锁去进入某个同步方法或同步代码块。 ④WAITING:表示因为调用接下来的方法,线程正在等待。包括:Object.wait()、Thread.join()和LockSupport.part()。直到另外一个线程执行一个特殊的动作该线程才会退出等待状态。如:该线程调用Object.wait(),需要另外一个线程调用Object.notify()或Object.notifyAll()。 ⑤TIMED_WAITING:表示线程因为调用接下来的方法,线程正在等待,等待时间最多为一个具体的值。包括:Thread.sleep(time)、Object.wait(time)、Thread.join(time)、LockSupport.parkNanos(time)和LockSupport.parkUntil(time)。直到另外一个线程执行一个特殊的动作该线程才会退出等待状态。如:该线程调用Object.wait(3000),另外一个线程调用Object.notify()或Object.notifyAll()可以使该线程退出等待状态。 ⑥TERMINATED:表示一个终止的线程,这个线程的执行已经完成。

  • 生命周期图如下,下面红色字体是操作系统中对应Java中的线程状态: ①这幅图有个地方不对:notify被唤醒后会进入锁池等待,所以WAITING和TIMED_WAITING状态到RUNNBALE的箭头不应该有notify()方法。 在这里插入图片描述正确示范图: 在这里插入图片描述 ③在同步代码中唤醒必须要去争夺锁资源。

  • 注意:Java中,当线程执行到同步代码区,若线程被执行等待方法,则线程会释放锁并进入等待状态,等待状态的线程不会参与锁的竞争,即若其它持有锁的线程执行完成后不会唤醒处于等待状态的线程,而阻塞状态的线程可能会被唤醒。等待状态的线程被唤醒后会从等待队列移除,加入到阻塞队列中,参与锁的竞争。类似于记录型信号量,信号量维持了int变量(锁状态)和阻塞队列(获取该信号量失败的线程队列),java中Block、waitng、timed_waitng都是阻塞,这些线程都会挂到阻塞队列上。

锁池和等待池:

  • 链接:详解Java锁池和等待池
  • 在java中,每个对象都有两个池,锁(monitor)池(Entry Set)等待池(Wait Set)
  • 锁池: ①假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池: ①假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

sleep与wait的区别? 以及wait(long timeout)中timeout的含义:

  • sleep是Thread的静态native方法,可随时调用,会使当前线程休眠,并释放CPU资源,但不会释放对象锁;
  • wait方法是Object的方法,只能在同步方法或同步代码块中使用,调用会进入休眠状态,并释放CPU资源与对象锁,需要我们调用notify/notifyAll方法唤醒指定或全部的休眠线程,进入锁池,再次竞争CPU资源.
  • timeout:最大等待时间(毫秒),超过会被唤醒,再次进入锁池.
  • 总结:sleep方法到时后会进入就绪态,而wait方法到时或在等待池中唤醒后还要去锁池中竞争锁再进入到就绪态。

Condition.await()和LockSupport.park():

  • 链接:Condition.await()、LockSupport.park()、Object.wait()、Thread.sleep()之间的区别
  • 详解: ①LockSupport.park()进入等待状态,可以在任意地方执行,不会释放锁。ReentrentLock.lock()底层判断逻辑中可能会执行LockSupport.park(),因此也是进入等待状态。因为在同步代码外,唤醒后立即执行。Condition.await()底层包括执行LockSupport.park(),进入等待状态,并释放锁资源,因为在同步代码中执行,因此唤醒后不一定立即执行。 在这里插入图片描述