博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
多线程详解
阅读量:3897 次
发布时间:2019-05-23

本文共 6696 字,大约阅读时间需要 22 分钟。

1. 基本概念

1.1 程序和进程

  • 程序 : 数据结构 + 算法 ,存放在硬盘上的可执行文件,即.exe文件
  • 进程 :主要指运行在内存中的可执行文件
  • 目前主流的操作系统都支持多进程,让操作系统同时执行多个任务

1.2 线程

  • 线程就是进程内部的程序流,也就是说每个进程的内部,是支持多线程的
  • 线程是轻量级的,会共享所在进程的系统资源。主流的开发都是多线程
  • 多线程再用时间片轮转法,来保证线程的并发执行,所谓并发就是指宏观并行,围观穿行
    • 从一个时间段上看,是多线程,多个线程同时执行
    • 从一个时间点上看,是单线程,单个线程执行

2. 线程的创建【重点

2.1 Thread类

  • java.lang.Thread 代表线程,任何线程都是Thread类的子类
  • Thread类是线程的模板,封装了复杂的线程开启等操作

2.2 创建方式【2】

  1. 自定义Thread类,重写run方法。然后创建该类对象,来调用start方法(不是直接调用run方法)

    • 如果直接调用run方法,本质上就是单线程
    • 本来是一个main主线程,调用start方法后,一分为二,变为两个线程
    • 两个线程,没有先后顺序,有系统的调度算法决定
    // subThread是 Thread的子类Thread t = new subThread();// JVM会自动调用run方法t.start();
  2. 自定义类实现Runnable接口,并且重写run方法。再创建该类的对象作为实参来构造Thread类型对象,然后使用Thread类对象调用 run方法

    • 虽然是Thread类对象调用run方法,其实调用的是Runnable接口实现类中重写的run方法

      new Thread(new Runnable() {
      // 匿名内部类中,重写run方法 @Override public void run() {
      System.out.println("Runnable接口实现线程的创建"); }}).start();// 上次代码,使用了匿名对象和匿名内部内
    • 使用 lambda : (参数列表)->{方法体}

      new Thread(()->System.out.println("Runnable接口实现线程的创建");).start();

2.3 相关方法

方法声明 功能介绍
Thread() 无参构造
Thread(String name) 根据参数指定线程名,来构造对象
Thread(Runnable target) 实现Runnable接口,构造对象
Thread(Runnable target, String name) 根据参数,指定引用名称来构造对象
void run() 1. 若使用了Runnable引用构造了线程对象,调用run方法时最终调用接口中的重写的run
2. 若没有使用Runnable接口引用构造线程对象,调用该方法时,啥也不做
void start() 用于另外启动一个线程,Java虚拟机会自动调用该线程的run方法

Thread类中的 run方法测试

Thread thread = new Thread();thread.run();
  • run方法源码分析:

    @Overridepublic void run() {
    if (target != null) {
    target.run(); }}
    • 从上面的Java源码源码中我们可以看到,如果target 成员变量=null时,将不执行任何方法。

    • 那这个target?怎么判断呢?看构造方法

      • Thread(Runnable target)

        public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);}
      • Thread()

        public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);}
      • 其实这个target 就是Runnable接口引用,底层这个Runnable接口引用,会赋值给 target变量。所以,在构造方法时,就会确定target是否为空

        1. 所以,若没有使用Runnable接口引用构造线程对象作为参数,在调用该方法时,就会啥也不做。
        2. 使用自定义Thread类,来创建线程时,必须重写 run方法,不然也会是啥也不做
        3. 如果使用实现Runnable接口作为参数,来创建线程。那么:底层的target = Runnable实现类对象。所以,调用run方法,其实就是调用 Runnable实现类对象的重写的run方法

3 线程的生命周期【了解】

在这里插入图片描述

步骤 说明
new Thread() 创建线程(并没有执行)
start() JVM执行 run方法,激活线程,进入就绪状态
线程调度器 使用线程调度器调用该线程,线程进入运行状态。新线程开启

运行状态的3种情况:

  1. 如果cpu分配的时间片用完后,该线程的活还没干完,将进入就绪状态,继续等待调度器的调度
  2. 如果线程的任务,执行完成后,将进入消亡状态
  3. 在线程执行的过程中,发生了阻塞事件,如:sleep方法等。线程将进入阻塞状态,在阻塞状态解除后,进入就绪状态,等待调度器的调度

4. 线程的编号和名称【了解】

  • 编号:线程的唯一标识
  • 名称:线程名
方法声明 功能介绍
long getId() 获取当前线程的编号
String getName() 获取当前线程的名称
void setName(String name) 修改线程名
static Thread currentThread() 获取当前正在执行线程的引用

5. 常用方法【重点】

方法声明 功能介绍
static void yield() 让当前线程让出CPU,进入就绪状态
static void sleep(times) 让线程,进入阻塞状态,并且休眠 times毫秒
1. 其他线程可能会打断阻塞状态,则发生InterruptedException异常
2. 所以使用sleep方法,要使用try/catch
3.(因为,子类重写的方法不能抛出比父类更大的异常,只能使用try/catch,不能使用throws
int getPriority() 获取当前线程的优先级
1. 最大优先级:10
2. 最小优先级:1
void setPriority( int newPriority) 修改线程的优先级
优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些
void join() 等待该线程终止
void join(long millis) 参数指定等待的额毫秒数(等待结束后,直接和子线程抢夺时间片运行)
boolean isDaemon() 判断是否为守护线程
void setDaemon (boolean on ) 设置线程为守护线程
  • 注意,优先级越高的线程不一定先打印。只是所,优先级越高CPU分配给他的时间片几率就越高

  • 守护线程:

    1. 必须在主线程启动前,将子线程设置为守护线程

      // 创建线程Thread t = new Thread();// 必须在start前,设为守护线程t.setDaemon(true);t.start();
    2. 守护线程,在主线程结束后。不管守护线程是否完成执行任务,都要立马结束掉线程

  • 小应用,主程序让新线程执行5s后,停掉子线程

    • 利用 flag 和 while来解决
    // flag私有属性,和setterboolean flag = true;public void setFlag(boolean flag) {
    this.flag = flag;}public void method(){
    new Thread(new Runnable() {
    @Override public void run() {
    while (flag){
    System.out.println("子线程正在执行..."); try {
    // 让当前线程(main主线程)睡眠1秒, 效果就是1s打印一次 Thread.sleep(1000); } catch (InterruptedException e) {
    e.printStackTrace(); } } } }).start();}public static void main(String[] args) {
    Thread01 t = new Thread01(); t.method(); try {
    // 让当前线程(main主线程)睡眠5秒 Thread.sleep(5000); } catch (InterruptedException e) {
    e.printStackTrace(); } t.setFlag(false); System.out.println("子线程结束");}

6. 线程的同步机制【重点】

6.1 基本概念

  • 多个线程访问同一资源时,需要对线程之间,进行通信协调,保证数据的一致性
  • 线程安全:多个线程并发读写同一个临界资源时会发生线程并发安全问题
  • 异步操作:多线程并发的操作,各自独立运行
  • 同步操作:多线程串行的操作,先后执行的顺序。

6.2 线程同步的实现

  • IDEA快捷键:Ctrl + Alt + T

  • 使用synchronized关键字来实现线程的同步 (也叫对象锁机制),从而保证线程执行的原子性

    • 部分代码的锁定
    synchronized(类类型的引用) {
    // TODO 编写所有需要锁定的代码;}
    • 全部代码的锁定

      synchronized(this) {
      // TODO 整个方法体的代码}

      如果使用Runnable接口实现类做参数,实现的线程。那么,this就是Runnable接口的实现类引用

    • synchronized修饰类:

      • 比如:StringBuffer的append方法
      public synchronized StringBuffer append(char c)
  • 注意:

    如果synchronized的参数,不是同一个对象的引用,那么会锁不住。就可能出现线程安全问题

6.3 线程安全类、非线程安全类

  • StringBuffer类是线程安全的类,但StringBuilder类非线程安全的类。
  • Vector类和Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
  • Collections.synchronizedList()Collections.synchronizedMap()等方法实现安全。
    • 即,将ArrayList、HashMap非线程安全的类,实现线程安全

6.4 死锁

  • 线程-1,执行的代码:

    public void run(){
    synchronized(a){
    //持有对象锁a,等待对象锁b synchronized(b){
    // TODO 编写锁定的代码; } }}
  • 线程-2,执行的代码:

    public void run(){
    synchronized(b){
    //持有对象锁a,等待对象锁b synchronized(a){
    // TODO 编写锁定的代码; } }}
  • 两个线程竞争了不可剥夺的资源。比如:上面2个线程,同时执行,线程1需要锁b,才能释放锁a。而线程2需要锁a,才能释放锁b。这样相互就形成了死锁,永远都打不开

  • 结论

    在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!

6.5 使用 Lock(锁) 实现线程同步

6.5.1 基本概念

  • 从Java5开始提供了更强大的线程同步机制,用显式定义的同步锁对象来实现
  • java.util.concurrent.locks.Lock接口是控制多个线程,对共享资源进行访问的工具
  • Lock接口的实现类:ReentrantLock类,该类拥有和 synchronized 相同的并发性。在线程安全的控制中,经常使用 ReentrantLock类 显式示加锁和释放锁

6.5.2 常用方法

方法声明 功能介绍
ReentrantLock() 无参构造,构造对象
void lock() 获取锁
void unlock() 释放锁

举例:

ReentrantLock rl = new ReentrantLock();public void run(){
// 加锁 rl.lock(); // TODO 加锁的代码部分 // 释放锁 rl.unlock();}

6.5.3 和synchronized的比较

  • Lock是显式锁,需要手动的调用去加锁、解锁;synchronized 是隐式锁,执行锁定的代码后自动释放
  • Lock锁,Java虚拟机将花费更少的时间来调度线程,性能更好
  • Lock锁,只能同步代码块方式的锁;synchronized,有同步代码块、同步方法这两种方式的锁

6.6 Object类中常用方法

方法声明 功能介绍
void wait() 将线程进入阻塞状态
void wait( long timeout ) 指定参数的时间过去后,接触阻塞状态
void notify() 将进入阻塞状态的线程唤醒
void notifyAll() 用于唤醒所有进入阻塞的线程

6.7 生产者/消费者模型

在这里插入图片描述

7. 线程池

  • 一个客户端就要创建一个新的线程

  • 当大量客户端连接服务器,就要不断地销毁和创建线程,这严重影响到了服务器的性能

  • 所以引入,线程池这一概念。任务直接提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务

    后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程。任务是提交给整个线程
    池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

7.1 相关类和方法:

  • 从Java5开始提供了线程池的相关类和接口:

    • java.util.concurrent.Executors类
    • java.util.concurrent.ExecutorService接口
  • Executors类

    • 是个工具类线程池的工厂类

    • 常用方法如下

      方法声明 功能介绍
      static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
      static ExecutorService newFixedThreadPool( int nThreads ) 创建一个可重用固定线程数的线程池
      static ExecutorService newSingleThreadExecutor() 创建一个只有一个线程的线程池

) | 创建一个只有一个线程的线程池 |

  • ExecutorService接口

    • 是真正的线程池接口,主要实现类是ThreadPoolExecutor

    • 常用方法如下

      方法声明 功能介绍
      void execute(Runnable command) 执行任务和命令
      Future submit(Callable task) 执行任务和命令
      void shutdown() 启动有序关闭

转载地址:http://jhuen.baihongyu.com/

你可能感兴趣的文章
从关系型数据库到非关系型数据库
查看>>
【数据库基础】数据库事务 - Transaction
查看>>
【设计模式基础】行为模式 - 3 - 职责链(Chain of responsibility)
查看>>
【Java基础】反射 - Reflection
查看>>
【C++基础】const成员函数
查看>>
【设计模式基础】行为模式 - 5 - 策略(Strategy)
查看>>
【Maven】Archetype
查看>>
【Java.Web】Cookie —— 基础
查看>>
【Tools.Eclipse】代码自动提示
查看>>
【Java.Web】MVC —— Model1 V.S. Model2
查看>>
【Java.Web】MVC —— 基于Servlet Controller的Model2 —— 示例
查看>>
【Java.Web】MVC —— 基于Filter Dispatcher的Model2 —— 示例
查看>>
【Java.Web】MVC —— Action的验证器 —— Validator
查看>>
【Java.Spring.MVC】使用Spring MVC构建Web应用程序
查看>>
【DB.PL/SQL】程序流程控制 —— 异常处理
查看>>
【Java.IO】I/O 【字节】【处理流】 - 之 - 【压缩流】 - ZipInputStream,ZipOutputStream
查看>>
【Java.JDBC/ORM】纯JDBC系统的开发随想
查看>>
【Unix/Linux】【系统】环境变量
查看>>
【Architecture】CPU-bound(计算密集型) 和I/O bound(I/O密集型)
查看>>
【MacOS】Mac 系统下类似于 apt-get 的软件包管理器 -- Homebrew
查看>>