Java多线程

多线程

进程是程序的依次执行过程,线程是比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程,多个线程共享进程的堆和方法区内存资源。

1.进程和线程

程序是含有指令和数据的文件,是静态的代码,被存储在磁盘或其他的数据存储设备中。

进程是程序的一次执行过程,线程是进程划分成的更小的运行单位。

进程和线程的联系和区别

进程和线程最大的区别是,各进程是独立的,而各线程则不一定独立,因为同一进程中的多个线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,进程则相反。

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即为一个进程的创建、运行以及消亡的过程。

线程是比进程更小的执行单位。

一个进程在其执行的过程中可以产生多个线程,多个线程共享进程的堆和方法区内存资源,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。

由于线程共享进程的内存,因此系统产生一个线程或者在多个线程之间切换工作时的负担比进程小得多,线程也称为轻量级进程。

进程和线程最大的区别是,各进程是独立的,而各线程则不一定独立,因为同一进程中的多个线程极有可能会相互影响。

线程执行开销小,但不利于资源的管理和保护,进程则相反。

线程的状态

线程在运行的生命周期中的任何时刻只能是 6 种不同状态的其中一种。

  • 初始状态(NEW):线程已经构建,尚未启动。
  • 运行状态(RUNNABLE):包括就绪(READY)和运行中(RUNNING)两种状态,统称为运行状态。
  • 阻塞状态(BLOCKED):线程被锁阻塞。
  • 等待状态(WAITING):线程需要等待其他线程做出特定动作(通知或中断)。
  • 超时等待状态(TIME_WAITING):不同于等待状态,超时等待状态可以在指定的时间自行返回。
  • 终止状态(TERMINATED):当前线程已经执行完毕。

多线程的优点和可能存在的问题

线程也称为轻量级进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程,多个线程同时运行可以减少线程上下文切换的开销。

多线程是开发高并发系统的基础,利用好多线程机制可以显著提高系统的并发能力和性能。

多线程并发编程并不总是能提高程序的执行效率和运行速度,而且可能存在一些问题,包括内存泄漏、上下文切换、死锁以及受限于硬件和软件的资源限制问题等。


2.关键字 synchronized 和 volatile

关键字 synchronized

该关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

关键字 synchronized 解决的是多个线程之间访问资源的同步性。

关键字 synchronized 最主要的三种使用方式是:修饰实例方法、修饰静态方法、修饰代码块。

  • 修饰实例方法:给当前对象实例加锁,进入同步代码之前需要获得当前对象实例的锁。
  • 修饰静态方法:给当前类加锁,进入同步代码之前需要获得当前类的锁。
  • 修饰代码块:指定加锁对象,给指定对象加锁,进入同步代码块之前需要获得指定对象的锁。

关键字 volatile

该关键字修饰的变量会直接在主内存中进行读写操作,保证了变量的可见性。

关键字 volatile 解决的是变量在多个线程之间的可见性。

除了保证变量的可见性以外,关键字 volatile 还有一个作用是确保代码的执行顺序不变。

为了提高执行程序时的性能,编译器和处理器会对指令进行重排序优化,因此代码的执行顺序和编写代码的顺序可能不一致。

添加关键字 volatile 可以禁止指令进行重排序优化。

只有当一个变量满足以下两个条件时,才能使用关键字 volatile。

  • 对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值。

  • 该变量没有包含在具有其他变量的不变式中。

volatile修饰的变量会直接在主内存中进行读写操作,而非各线程自己的工作内存。

可见性的问题主要原因是因为CPU多核,而且每个核上面都有CPU缓存,CPU缓存对于不同核来说相互不可见,因此这个指令的作用是让CPU从内存读取数据,而不是从CPU缓存。

volatile关键字修饰的变量被改变时, 会强制立即写入内存中, 并且让其在CPU缓冲区域中对应的缓冲行无效, 其他线程读取这个变量时, 强制其他线程从内存中重新读取这个变量的值。

关键字 synchronized 和 volatile 的区别

  • 关键字 volatile 是线程同步的轻量级实现,不需要加锁,因此性能优于关键字 synchronized。
  • 关键字 synchronized 可以修饰方法和代码块,关键字 volatile 只能修饰变量。
  • 关键字 synchronized 可能发生阻塞,关键字 volatile 不会发生阻塞。
  • 关键字 synchronized 可以保证数据的可见性和原子性,关键字 volatile 只能保证数据的可见性,不能保证数据的原子性。
  • 关键字 synchronized 解决的是多个线程之间访问资源的同步性,关键字 volatile 解决的是变量在多个线程之间的可见性。

3.多线程相关方法

Thread 类的方法

run 和 start

方法 run 在 Runnable 接口中被定义。

方法 start 在 Thread 类中被定义。

创建一个 Thread 类的实例,即为创建了一个处于初始状态的线程。对一个处于初始状态的线程调用方法 start,该线程被启动,进入运行状态。

调用方法 start 之后,方法 run 会自动执行。

通过调用方法 start,执行方法 run,才是多线程工作。

如果直接执行方法 run,则方法 run 会被当成一个主线程下的普通方法执行,而不会在某个线程中执行,因此不是多线程工作。

sleep

方法 sleep 在 Thread 类中被定义。

该方法的作用是使当前线程暂停执行一段时间,让其他线程有机会继续执行,但是该方法不会释放锁。

方法 sleep 需要捕获 InterruptedException 异常。

join

方法 join 在 Thread 类中被定义。

该方法的作用是阻塞调用该方法的线程,直到当前线程执行完毕之后,调用该方法的线程再继续执行。

方法 join 需要捕获 InterruptedException 异常。

yield

方法 yield 在 Thread 类中被定义。

该方法的作用是暂停当前正在执行的线程对象,并执行其他线程。

实际调用方法 yield 时无法保证一定能让其他线程执行,因为线程调度时可能再次选中原来的线程对象。

Object 类的方法

wait

方法 wait 在 Object 类中被定义。

该方法必须在 synchronized 语句块内使用,作用是释放锁,让其他线程可以运行,当前线程进入等待池中。

notify 和 notifyAll

方法 notify 和 notifyAll 在 Object 类中被定义。

方法 notify 的作用是从等待池中移走任意一个等待当前对象的线程并放到锁池中,只有锁池中的线程可以获取锁。

方法 notifyAll 的作用是从等待池中移走全部等待当前对象的线程并放到锁池中,锁池中的这些线程将争夺锁。

中断线程

中断线程的方法是 interrupt,在 Thread 类中被定义。

该方法不会中断一个正在运行的线程,只是改变中断标记。

当线程处于等待状态、超时等待状态或阻塞状态时,如果对线程调用方法 interrupt 将线程的中断标记设为 true,则中断标记会被清除,同时会抛出 InterruptedException 异常。可以通过 try-catch 块捕获该异常,即可终止线程。

当线程处于运行状态时,可以对线程调用方法 interrupt 将线程的中断标记设为 true,从而达到终止线程的目的,也可以添加一个 volatile 修饰的额外标记,当需要终止线程时,更改该标记的值即可。

不推荐使用方法 stop 和 destroy 终止线程。

  • 方法 stop 会立即停止方法 run 中剩余的全部工作,并抛出 ThreadDeath 错误,导致清理性工作无法完成,另外方法 stop 会立即释放该线程的所有锁,导致对象状态不一致。
  • 方法 destroy 只是抛出 NoSuchMethodError,没有做任何事情,因此无法终止线程。

方法 sleep、join 和 yield 的区别有哪些?

  • 方法 sleep 的作用是使当前线程暂停执行一段时间,让其他线程有机会继续执行;

  • 方法 join 的作用是阻塞调用该方法的线程,直到当前线程执行完毕之后,调用该方法的线程再继续执行;

  • 方法 yield 的作用是暂停当前正在执行的线程对象,并执行其他线程。


4.线程池

线程池是一种线程的使用模式。

创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

线程池的好处

在开发过程中,合理地使用线程池可以带来 3 个好处。

  • 降低资源消耗。

    重复利用线程池中已经创建的线程,可以避免频繁地创建和销毁线程,从而减少资源消耗。

  • 提高响应速度。

    由于线程池中有已经创建的线程,因此当任务到达时,可以直接执行,不需要等待线程创建。

  • 提高线程的可管理性。

    线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程池的创建

可以通过 ThreadPoolExecutor 类创建线程池。ThreadPoolExecutor 类有 4 个构造方法,其中最一般化的构造方法包含 7 个参数。

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

7 个参数的含义如下。

  • corePoolSize:核心线程数

    定义了最少可以同时运行的线程数量,当有新的任务时就会创建一个线程执行任务,当线程池中的线程数量达到 corePoolSize 之后,到达的任务进入阻塞队列。

  • maximumPoolSize:最大线程数

    定义了线程池中最多能创建的线程数量。

  • keepAliveTime:等待时间

    当线程池中的线程数量大于 corePoolSize 时,如果一个线程的空闲时间达到 keepAliveTime 时则会终止,直到线程池中的线程数不超过 corePoolSize。

  • unit:参数 keepAliveTime 的单位

  • workQueue:阻塞队列

    用来存储等待执行的任务。

  • threadFactory:创建线程的工厂

  • handler:当拒绝处理任务时的策略

向线程池提交任务

可以通过方法 execute 向线程池提交任务。该方法被调用时,线程池会做如下操作。

  1. 如果正在运行的线程数量小于 corePoolSize,则创建核心线程运行这个任务。
  2. 如果正在运行的线程数量大于或等于 corePoolSize,则将这个任务放入阻塞队列。
  3. 如果阻塞队列满了,而且正在运行的线程数量小于 maximumPoolSize,则创建非核心线程运行这个任务。
  4. 如果阻塞队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,则线程池抛出 RejectExecutionException 异常。

上述操作中提到了两个概念,「核心线程」和「非核心线程」。核心线程和非核心线程的最大数目在创建线程池时被指定。

核心线程和非核心线程的区别如下:

  • 向线程池提交任务时,首先创建核心线程运行任务,直到核心线程数到达上限,然后将任务放入阻塞队列。
  • 只有在核心线程数到达上限,且阻塞队列满的情况下,才会创建非核心线程运行任务。

关闭线程池

可以通过调用线程池的方法 shutdown 或 shutdownNow 关闭线程池。

这两个方法的原理是遍历线程池中的工作线程,对每个工作线程调用 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。

方法 shutDown 和 shutDownNow 有以下区别:

  • 方法 shutDown 将线程池的状态设置成 SHUTDOWN,正在执行的任务继续执行,没有执行的任务将中断。

  • 方法 shutDownNow 将线程池的状态设置成 STOP,正在执行的任务被停止,没有执行的任务被返回。

posted @ 2022-05-27 09:30  萝卜不会抛异常  阅读(68)  评论(0)    收藏  举报