第8章 多线程基础

简介: 建立多线程基础,了解基本知识。

8.1 基本概念

8.1.1 程序、进程、线程

  1. 程序(program):完成特定任务、用某种语言编写的指令集合。是一段静态的代码。
  2. 进程(process):程序的一次执行过程,或者正在运行的一个程序。
  • 进程是系统分配资源的基本单位,根据进程执行的生命周期,系统会为不同时期的进程分配不同的内存空间。
  1. 线程(thread):程序内部的一条执行路径,一个进程可以含有多个进程。
  • 如果一个进程可以并行执行多个线程,则进程支持多线程。
  • 每个线程拥有独立的运行栈和程序计数器。
  • 同一进程的多个线程共享相同的堆空间(对象、属性共享)、方法区,优点是线程间通信更便捷、高效,但多个线程同时操作公共资源会有安全隐患。
  1. Java中线程的分类(区别在于JVM何时离开):
  • 守护线程:服务用户线程,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成守护线程。
  • 垃圾回收是一个典型的守护线程
  • JVM中都是守护线程时,JVM就会退出。
  • 用户线程:

8.1.2 单核、多核

  1. 单核:CPU仅有一个核心数,同一时间内,只能执行一个线程任务。执行多个线程时,采取的是不断切换线程的方式。
  • 由于CPU频率高、线程切换时间短,让人感觉”同时“执行了多个线程
  1. 多核:CPU有多个核心,每个核心可以执行一个线程。
  2. java.exe:一个Java运行程序至少有3个线程:
  • main():主线程
  • gc():垃圾回收线程
  • 异常处理线程:发生异常时,会影响主线程。

8.1.3 并行、并发

  1. 并行:多个CPU执行多个任务。
  2. 并发:一个CPU同时执行多个任务。

8.2 创建多线程

8.1 方式一:继承Thread类

  1. 创建步骤:
  • 定义子类继承Thread类
  • 子类中重写Thread类中run()方法
  • 创建Thread子类对象
  • 创建一个对象即代表开启一个线程,要开启多个该线程,需要创建多个该对象。
  • 调用子类对象的start()方法。
  1. 注意点:
  • 使用Tread子类对象直接调用run()方法不会开启分支线程,它表示在main线程内,调用了Thread子类对象的方法。
  • 使用Thread子类对象调用start()方法会开启一个线程,开启线程后run()方法何时执行全由CPU调度决定,即main线程和分支线程中的语句执行具有随机性。
  • 一个实例化的Thread子类对象只能调用一次star()方法,重复调用时,会抛出异常:IllegalThreadStateException。
  1. 常用方法:
  • start():启动当前线程;调用当前线程的run()方法
  • run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  • currentThread():静态方法,返回执行当前代码的线程
  • getName():获取当前线程的名字
  • setName("str"):设置当前线程的名字
  • yield():释放调用线程在cpu中的执行权,后续执行哪个线程由CPU确定,有可能还是这个线程。当前进程进入就绪状态。
  • join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  • 有异常问题,可以根据使用位置进行throws或try-catch处理。
  • stop():已过时。当执行此方法时,强制结束当前线程。
  • sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
  • 有异常问题,由于该方法使用在run()方法中,而run()方法是对父类Thread中run()方法的重写,且Thread中run()方法没有抛出异常,根据继承的特性(子类的异常不大于父类),所以子类的run()方法不能抛出异常,只能由try-catch处置。
  • isAlive():判断当前线程是否存活。
  1. 线程优先级:
  • 等级:
  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5 (默认等级)
  • 方法:
  • getPriority():返回线程优先等级
  • setPriority(int num):设置有限等级
  • 注意点:设置了高等级的优先级,并不代表一定执行完该线程后执行其他线程,而是提高了CPU执行该线程的概率而已。
  1. Thread类构造器
  • Thread()
  • Thread(String threadname):创建指定名称的线程
  • 搭配super(threadname)才能在getName()时获得名字
  • Thread(Runnable target)
  • Thread(Runnable target, String name):  

8.2.2 方式二:实现Runnable接口

  1. 创建步骤:
  • 定义类,实现Runnable接口。
  • 实现类中实现Runnable接口中的run方法
  • 创建实现类对象
  • 创建Thread类对象,将实现类对象作为参数传入。
  • 使用Thread类的对象调用start()方法。
  1. 两种方式比较:
  • 相同点:
  • 实现类(继承类)都需要重写run()
  • 都具有线程安全问题
  • 不同点:
  • 方式二没有单继承的局限性
  • 方式二更适合多个线程共享数据(数据只有一份)的情况
  • 开发中优先选择方式二

8.2.3 方式三:实现Callable接口

  1. 创建步骤:
  • 创建一个实现Callable的实现类
  • 实现call方法,将此线程需要执行的操作声明在call()中
  • 创建Callable接口实现类的对象
  • 创建FutureTask的对象,将此Callable接口实现类的对象作为传递到FutureTask构造器中
  • 创建Thread对象,将FutureTask的对象作为参数传递到Thread类的构造器中,Thread的对象调用start()
  • FutureTask的实例对象调用get()方法,获取重写call方法的返回值。
  1. Callable的优点:
  • call()可以返回值的。
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息。
  • Callable是支持泛型

8.2.4 方式四:使用线程池(ThreadPool)

  1. 创建步骤:
  • 提供指定线程数量的线程池:ExecutorService service = Executors.newFixedThreadPool(10);
  • service1.setCorePoolSize(15)
  • service1.setKeepAliveTime()
  • 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
  • service.execute(Runnable runable)
  • service.submit(Callable callable)
  • 关闭连接池:service.shutdown()
  1. 优点:
  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理:
  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没任务时最多保持多长时间后会终止

8.3 线程的生命周期

  1. 新建:
  • 继承方式(方式一):Thread类子类的对象被创建。
  • 实现方式(方式二):Thread类声明并创建。
  1. 就绪:处于新建状态的线程调用start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  2. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态。
  3. 阻塞:线程被人为挂起或执行输入输出操作时,会让出 CPU资源,并临时中止自己的执行,即进入阻塞状态。
  • 阻塞时临时状态,不可以作为最终状态。
  1. 死亡:线程完成了它的全部工作、线程被提前强制性地中止、出现异常导致结束。
  • 死亡是线程的最终状态。

8.4 线程安全

5.4.1 线程安全问题——同步机制

  1. 线程安全问题:未处理的多线程任务在处理共享数据时,会造成数据破坏(重复数据、缺失数据、数据超范围等)。
  • 原因:处理共享数据的情况时,一个线程多条语句只执行了一部分,未处理完时,另一个线程参与进来,也要处理共享数据,造成共享数据错误。
  • 解决办法:单线程处理数据,执行完后再让其他线程参与——同步机制
  • 解决原理:给共享资源加锁,第一个访问资源的线程进行资源锁定,在解锁之前其他线程无法访问,解锁之后,其他线程可以锁定并使用。

8.4.2 Synchronized处理线程安全问题

  1. Synchronized(同步)语法:
  • 同步代码块:synchronized(同步监视器){}
  • 同步方法:public synchronized void show(){}
  1. Synchronized细节:
  • 同步监视器必须唯一。
  • 同步代码块:同步监视器可设置为类名.classthis、任一对象(静态或非静态),取决于是否唯一。
  • 同步方法:静态方法同步监视器默认为类名.class,非静态方法同步监视器默认为this
  1. 同步监视器一般情况:
  • 在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
  • 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

8.4.3 死锁及lock处理线程安全问题

  1. 死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源。
  • 出现死锁后,不会出现异常、不会出现提示、程序也不会运行,处于阻塞状态,无法继续。
  1. Lock(JDK5.0新增):
  • 引入java.util.concurrent.locks.ReentrantLock;
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。  
  • ReentrantLock类实现了 Lock。
  • 创建ReentrantLock对象:private ReentrantLock lock = new ReenTrantLock();
  • 根据对象是否唯一(lock是否唯一),可以在声明时使用static、或static final修饰。
  • 在出现共享资源操作的代码前调用lock()方法
  • 在结束共享资源操作的代码后调用unlock()方法
  • 如果操作资源共享的代码需要使用try包裹,则必须把unlock()写入finally语句块,lock()则不是必须要写入try中
  1. synchronized与Lock的异同:
  • 相同:二者都可以解决线程安全问题
  • 不同:
  • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
  • Lock需要手动的启动同步,同时结束同步也需要手动的实现。
  1. 使用的优先顺序:Lock ---> 同步代码块(已经进入了方法体,分配了相应资源 ) --->同步方法(在方法体之外)
  • 同步代码块包裹的共享资源操作代码可以更小。

8.4.4 同步的深入理解

  1. 同步的范围:
  • 确定同步代码范围时,要将所有操作共享数据的语句包裹在内。
  • 范围太大:操作数据的语句变为单线程的,没有发挥多线程的功能。
  • 范围太小:操作共享数据的语句由遗漏,同步不起作用。
  1. 同步的问题:
  • 优点:解决了线程安全的问题。
  • 缺点:操作同步代码时,只有一个线程运行,其他线程等待,相当于单线程过程,效率低。
  1. 释放锁的操作:
  • 同步方法、同步代码块执行结束
  • 同步方法、同步代码块中遇到break、return
  • 同步方法、同步代码块中出现未处理的Error或Exception
  • 同步方法、同步代码块中执行了线程对象的wait()
  1. 不会释放锁的操作:
  • 同步方法、同步代码块中调用Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 其他线程调用了当前执行线程的suspend()方法将该线程挂起。
  • 应尽量避免使用suspend()resume()控制线程。
  1. 线程安全的懒汉式单例模式
classSingleton {
privatestaticSingletoninstance=null;
privateSingleton() {    }
//  1. 方式一publicstaticSingletongetInstance() {
if (instance==null) {
synchronized (Singleton.class) {
if (instance==null) {
instance=newSingleton();
                }
            }
        }
returninstance;
    }
//  2. 方式二publicstaticSingletongetInstance() {
synchronized (Singleton.class) {
if (instance==null) {
instance=newSingleton();
            }
        }
returninstance;
    }
}
  • 方式一效率优于方式二,假如有n个线程需要创建当前对象,多核CPU让k个线程运行到getInstance()
  • 方式一中共有k个线程判断对象是否等于null,1个线程执行同步代码块并创建对象,k-1个线程执行同步代码块结束判断,后续n-k个线程不会再进入同步代码块。
  • 方式二中共有1个线程执行同步代码块并创建对象,k-1个线程执行同步代码块结束判断,后续n-k个线程还会执行同步代码块进行判断。

8.5 线程通信

  1. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  2. notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,由JVM决定执行哪个。
  3. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
  4. 说明:
  • 三个方法必须使用在同步代码块或同步方法中。
  • 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
  • 三个方法是定义在java.lang.Object类中。
  1. sleep() 和 wait()的异同
  • 相同点:
  • 都可以使当前进程进入阻塞状态
  • 不同点:
  • 声明位置不同:slee()声明在Thread类中,wait()声明在Object类中。
  • 调用要求不同:slee()可以在任何需要的场景下调用,wait()必须在同步方法、同步代码块中调用。
  • sleep()不会释放同步监视器、wait()会释放同步监视器。
目录
相关文章
|
6月前
|
存储 Linux 调度
Linux系统编程 多线程基础
Linux系统编程 多线程基础
30 0
|
5月前
|
Java API 调度
并发编程系列教程(01) - 多线程基础
并发编程系列教程(01) - 多线程基础
38 0
|
7月前
|
Java 程序员 调度
多线程(初阶)——多线程基础
多线程(初阶)——多线程基础
59 0
|
Java API 调度
并发编程之多线程基础
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
并发编程之多线程基础
|
缓存 安全 Java
6. 多线程基础
对一个程序的运行状态, 以及在运行中所占用的资源(内存, CPU)的描述; 一个进程可以理解为一个程序; 但是反之, 一个程序就是一个进程, 这句话是错的。
71 0
6. 多线程基础
|
安全 Java 编译器
多线程基础(上)
多线程基础(上)
52 0
多线程基础(上)
|
Java 编译器 程序员
多线程基础(下)
多线程基础(下)
86 0
多线程基础(下)
|
SpringCloudAlibaba 安全 前端开发
JUC系列(一) 多线程基础复习
问:如何学习JUC? 答: 源码 + Java帮助文档 面试高频, juc 其实就是 Java.util 包下的线程分类的工具
JUC系列(一) 多线程基础复习
|
算法 Java 调度
【多线程基础知识】
【多线程基础知识】
106 0
|
存储 缓存 安全