口试官:这就是你理解的Java多线程基础?

打印 上一主题 下一主题

主题 874|帖子 874|积分 2622

弁言

现代的操作系统(Windows,Linux,Mac OS)等都可以同时打开多个软件(任务),这些软件在我们的感知上是同时运行的,例如我们可以一边浏览网页,一边听音乐。而CPU实行代码同一时间只能实行一条,但纵然我们的电脑是单核CPU也可以同时运行多个任务,如下图所示,这是因为我们的 CPU 的运行的太快了,把时间分成一段一段的,通过时间片轮转分给多个任务瓜代实行。

把CPU的时间切片,分给不同的任务实行,而且实行的非常快,看上去就像在同时运行一样。例如,网易云实行50ms,浏览器实行50ms,word 实行50ms,人的感官根本感知不到。现在多数的电脑都是多核(多个 CPU )多线程,例如4核8线程(可以近似的看成8个 CPU ),也是把每个核心运行时间切片分给不同的任务瓜代实行。
进程与线程

进程(Process)是操作系统对一个正在运行的步伐的一种抽象,我们可以进程简朴理解为操作系统中正在运行的一个软件,即把一个任务称之为一个进程,例如我们的网易云音乐就是一个进程,浏览器又是另外一个进程。
线程(Thread)线程是一个比进程更小的实行单元,进程是线程的容器,一个进程至少有一个线程而且可以产生多个线程,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据,多线程之间比多进程之间更轻易共享数据,而且线程一般来说都比进程更加高效。

 

java语言内置了多线程支持:JVM 启动时会创建一个主线程,该主线程负责实行 main 方法,一个 Java 步伐实际上是一个JVM进程,JVM进程用一个主线程来实行main()方法内部,我们又可以启动多个线程。此外,JVM另有负责垃圾回收的其他工作线程等。
创建线程

我们需要区分线程和线程体两个概念,线程可以驱动任务,因此需要一个描述任务的方式,这个方式就是线程体,而我们创建线程体有多种方式,而创建线程只有一种:将任务(线程体)表现的附着到线程上,调用 Thread 对象的 start()方法,实行线程的初始化操作,然后新线程调用 run() 方法启动任务。
创建线程体可以使用下面 3 种方式,然而这 3 种方式都是在创建线程体,直到调用 Thread 对象的 start() 方法时才请求 JVM 创建新的线程,具体什么时间运行有线程调度器 Scheduler 决定。

  • 继续 Thread 类;
  1. /**
  2. * 1、定义Thread类的子类
  3. */
  4. public class MyThread extends Thread {
  5.     //2、重写Thread类的run方法
  6.     //run()方法体内的内容就是线程要执行的代码
  7.     @Override
  8.     public void run() {
  9.         // ...
  10.     }
  11. }
  12. public static void main(String[] args) {
  13.     //3、创建线程对象
  14.     MyThread mt = new MyThread();
  15.     //4、启动线程
  16.     mt.start();
  17.     /**
  18.      * 调用线程的start()方法来启动线程,启动线程的实质是请求JVM运行相应的线程,
  19.      * 这个线程具体什么时候运行,由线程调度器(scheduler)决定
  20.      * 注意:
  21.      *   调用start()方法不代表线程能立马运行
  22.      *   线程启动后会运行run()方法
  23.      *   如果启动了多个线程,start()调用的顺序不一定就是线程启动的顺序
  24.      */
  25. }
复制代码

  • 实现 Runable 接口;
  1. //1、实现Runnable接口
  2. public class MyRunable implements Runnable{
  3.     //2、实现run方法
  4.     @Override
  5.     public void run() {
  6.         // ...
  7.     }
  8.     public static void main(String[] args) {
  9.         //3、将实现了Runnable接口的对象传入Thread的构造方法中
  10.         Thread thread = new Thread(new MyRunable());
  11.         //4、启动线程
  12.         thread.start();
  13.     }
  14. }
复制代码

  • 实现 callable 接口
  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.FutureTask;
  3. public class CallableExample {
  4.     public static void main(String[] args) {
  5.         // 1、实现Callable接口的匿名内部类
  6.         Callable<Integer> callable = new Callable<Integer>() {
  7.             @Override
  8.             public Integer call() throws Exception {
  9.                 System.out.println("Callable task is running");
  10.                 return 42;
  11.             }
  12.         };
  13.         // 2、将Callable包装在RunnableFuture实现类中
  14.         FutureTask<Integer> futureTask = new FutureTask<>(callable);
  15.         // 3、将FutureTask实例传递给Thread类来执行
  16.         Thread thread = new Thread(futureTask);
  17.         thread.start();
  18.         try {
  19.             Integer result = futureTask.get();
  20.             System.out.println("Result: " + result);
  21.         } catch (Exception e) {
  22.             e.printStackTrace();
  23.         }
  24.     }
  25. }
复制代码
在日常使用中,发起能用接口实现就不要用继续 Thread 的方式来创建线程,原因如下:

  • 避免单继续的限制:Java是单继续的语言,假如一个类继续Thread类,就无法再继续其他类。而实现Runnable接口则不会有这种限制,避免了单继续的局限性。
  • 更好的适配性:实现Runnable接口可以更好地支持类似线程池的机制,让线程的实行和任务的分离更清楚。传递Runnable对象给线程池实行任务十分方便,而且可以重复使用。
  • 更好的面向对象设计:继续Thread类是一种功能导向的设计,而实现Runnable接口更倾向于面向对象的设计,符合面向对象的编程头脑。
线程的状态

一个线程对象只能调用一次 start() 方法启动新线程,并在新线程中实行 run() 方法,一旦 run() 方法 实行完毕,线程就终止殒命了。我们通过 Thread 类中的枚举类 State 来看一下 Java 线程有哪些状态:
  1.     public enum State {
  2.         
  3.         /**
  4.          * 新建状态
  5.          * 还没有执行start()方法的线程状态
  6.          */
  7.         NEW,
  8.         /**
  9.          * 可运行状态
  10.          * 在Java虚拟机中运行处于可运行状态的线程,可能正在等待其他资源,例如处理器
  11.          */
  12.         RUNNABLE,
  13.         /**
  14.          * 阻塞状态
  15.          * 处于阻塞状态的线程正在等待监视器锁,以进入同步代码块或在调用wait()后重新进入
  16.          */
  17.         BLOCKED,
  18.         
  19.         /**
  20.          * 无限期等待状态
  21.          * 线程因调用一下方法之一而处于无限期等待状态:
  22.          * Object.wait with no timeout
  23.          * Thread.join with no timeout
  24.          * LockSupport.park
  25.          * 处于等待状态的线程正在等待另一个线程执行特定操作
  26.          */
  27.         WAITING,
  28.    
  29.         /**
  30.          * 具有指定等待时间的等待线程的线程状态
  31.          * 线程处于定时等待状态的原因是调用了以下方法之一,并指定了正等待时间:
  32.          * Thread.sleep
  33.          * Object.wait with timeout
  34.          * Thread.join with timeout
  35.          * LockSupport.parkNanos
  36.          * LockSupport.parkUntil
  37.          */
  38.         TIMED_WAITING,
  39.         /**
  40.          * 已终止线程的线程状态.
  41.          * 线程已执行完毕.
  42.          */
  43.         TERMINATED;
  44.     }
复制代码
 
由源码可知,Java 的线程状态有 6 种:

  • NEW:新创建的线程,还未实行;
  • RUNNABLE:正在运行中线程或正在等待资源分配的预备运行的线程;
  • BLOCKED:等待获取监视器锁的线程;
  • WAITING:等待另外一个线程实行特定操作,没有时间限制;
  • TIMED_WAITING:等待某个特定线程在制定时间段内实行特定操作;
  • TERMINATED:线程实行完毕
线程状态的转换可以参考下图:



  • NEW 状态
创建线程后未启动线程状态为 NEW,在该线程调用 start() 方法以前会一直保持这种状态。此时,JVM 会为该线程分配内存并初始化其成员变量的值,但是该线程并没有表现出任何线程的动态特征,步伐也不会实行线程的实行体,即 run() 方法的部分。
下面的代码,我们可以调用 Thread.getState() 方法来获取线程的状态,可以看出打印出来的状态为 NEW。
  1. public void ThreadTest() {
  2.     Thread t = new Thread(new Runnable() {
  3.         @Override
  4.         public void run() {
  5.             System.out.println("ThreadTest");
  6.         }
  7.     });
  8.     System.out.println(t.getState()); // NEW
  9. }
复制代码

  • RUNNABLE
当在Java的Thread对象上调用start()方法后,以下过程将会发生:

  • 线程状态变化:线程对象的状态会从NEW(新建)状态转变为RUNNABLE(可运行)状态,表明线程已经预备好运行,但尚未分配到CPU实行。
  • 系统资源分配:线程调度器会为该线程分配系统资源,例如CPU时间。然而,并不保证立即实行,具体实行时机还取决于线程调度器的调度算法和其他运行中的线程。
  • 实行run()方法:当该线程被线程调度器选中并分配到CPU时间时,线程的run()方法会被调用,线程开始实行具体的任务逻辑。
处于RUNNABLE 状态的线程要么正在运行中,要么已经预备好运行但正在等待系统分配 CPU 资源
在Java假造机(JVM)中,JVM 自带的线程调度器负责决定Java线程的实行顺序。它会根据线程的优先级和调度算法来确定哪个线程可以获得 CPU 时间。通常环境下,步伐员可以通过设置线程的优先级来影响线程调度器的决议,但实际线程的调度仍由 JVM 负责。

  • BLOCKED
当线程尝试访问某个由其他线程锁定的代码块时,该线程会因为需要等待获取监视器锁进入 BLOCKED 状态,线程获取锁后就会结束此状态。

  • WAITING
线程正在等待另一个线程实行特定操作时处于 WAITING 等待状态,例如当线程调用以下方法时会进入 WAITING 等待状态:
调用方法
退出条件
Object.wait()
Object.notify() / Object.notifyAll()
Thread.join()
被调用的线程(Thread)实行完毕
LockSupport.park()
-
上述方法中的 wait() 和 join() 没有传入超时时间 timeout 参数,线程只能等待其他线程表现的唤醒或实行完毕,否则不会被分配 CPU 时间片。

  • TIMED_WAITING
线程在这种状态部属于期限等待,无需其他线程表现的唤醒当前线程,在一定时间内被系统自动唤醒。
壅闭和等待的区别在于:壅闭是被动的,等待是自动的。壅闭是在等待获取锁,而等待是在等待一定的条件发生。
调用方法
退出条件
Thread.sleep()
时间结束
设置了 Timeout 参数的 Object.wait() 方法
时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法
时间结束 / 被调用的线程实行完毕
LockSupport.parkNanos() 方法
-
LockSupport.parkUntil() 方法
-


  • TERMINATED
线程实行完毕大概产生非常而结束会进入 TERMINATED 状态,进入该状态的线程已经殒命。
线程同步

并发问题产生的原因是:多个线程同时对一个共享资源进行非原子性操作,这内里包罗了三个产生并发问题的三个条件:多个线程同时,共享资源,非原子性操作,办理线程安全问题的本质就是要粉碎这三个条件,因此可以把多线程的并行实行,修改为单线程的串行实行,即同一时刻只让一个线程实行,这种办理方式就叫做互斥锁。
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock,从而到达保护共享资源的目的,当多条线程实行到被保护的地区时,都需要先去获取到锁,这时间只能有一条线程获取到锁,实行被保护地区的代码,其他线程在保护区外部等待获取锁,直到当前线程实行完毕释放资源后,其他线程才有实行的时机。
synchronized 和 ReentrantLock 可以保证可见性、原子性和有序性,另外一个 Java 的关键字 Volatile 也可以保证可见性,另外后者还可以克制指令重排序。
Synchronized

在 Java 中每个对象都可以作为锁,Synchronized 也是依赖 Java 的对象来实现锁,一共有三种类型的锁:

  • 当前实例锁:锁定的是实例对象,即为 this 锁;
  • 类对象锁:锁定的是类对象,即为 Class 对象锁;
  • 对象实例锁:锁定的给定的对象实例,即位 Object 锁;
在使用 Synchronize 时也有三种不同的方式:

  • 修饰平凡方法:使用 this 锁,在实行该方法前必须先获取当前实例对象的锁资源;
  • 修饰静态方法:使用 class 锁,在实行该方法前必须先获取当前类对象的锁资源;
  • 修饰代码块:使用 Object 锁,在实行该方法前必须先获取给定对象的锁资源;
  1. public class A {
  2.     String lockObject = new String();
  3.    
  4.     // 锁定当前的实例,this锁,每个实例拥有一个锁
  5.     public synchronized void a() {};
  6.     // 修饰的是静态方法,使用的 class 锁,多个对象共享 class 锁
  7.     public static synchronized void b() {}
  8.    
  9.     public void c() {
  10.         // 修饰的是代码块,使用的 lockObject 对象的锁,也是实例锁
  11.         synchronized(lockObject) {
  12.             // do something
  13.         }
  14.         // 修饰代码块,使用的 B.class 类对象锁
  15.         synchronized(B.class) {
  16.             
  17.         }   
  18.     }
  19. }
  20. public class B {
  21.    
  22. }
复制代码
三种不同的使用方式有不同的应用场景,我们在使用的过程中一定要注意加锁的对象是谁,否则大概会产生意想不到的结果。在加锁时,尽量减少加锁的地区,例如能够在方法体中对代码块加锁,就不要在方法上面加锁,加锁的地区越短越好。
ReentrantLock

ReentrantLock 是 Java.util.concurrent(J.U.C)包中的锁,该锁由 JDK 实现,而 synchronized 是由 JVM 实现的。
  1. public class ReentrantLockDemo{
  2.     private Lock lock = new ReentrantLock();
  3.     private void func() {
  4.         lock.lock(); // 加锁
  5.         try {
  6.             for (int i = 0; i < 10; i++) {
  7.                 system.out.prrint(i)
  8.             }
  9.         } finally {
  10.             lock.unlock(); // 确保释放锁
  11.         }
  12.     }
  13. }
  14. public static void main(Stirng[] args) {
  15.     ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
  16.     ExecutorService executorService = Executors.newCachedThreadPool();
  17.     executorService.execute(() -> lockExample.func());
  18.     executorService.execute(() -> lockExample.func());
  19. }
复制代码
上面的代码演示了ReentrantLock 的使用方法,表现的调用 lock()方法加锁,在 finally 中表现的释放锁。
锁比力

不同点
synchronized
reentrantLock
实现方式
JVM
JDK
性能
新版本 Java 对 synchronized 进行了大量的优化,大抵相同
等待可制止
不可
可以
公平锁
非公平
默认非公平,支持公平锁
绑定多个条件

帮点多个 Condition 对象
在需要使用锁时,除非需要使用 reentrantLock 的高级功能,否则优先使用 synchronized 关键字加锁,这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生支持它,而 ReentrantLock 不是全部的 JDK 版本都支持,并且使用 syschronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保释放锁。
线程池

线程池可以管理一系列线程,当有任务需要处理惩罚时,直接从线程池内里获取线程来处理惩罚,当线程处理惩罚完任务时再放回到线程池中等待下一个任务,这样可以减少每次创建线程的开销,提升资源的使用率。线程池提供了一种限制和管理资源的方式,每个线程池还维护了一些基本的统计信息,例如 已完成任务的数量等。在《Java 并发编程的艺术》一书中提到使用线程有三点好处:

  • 降低资源的斲丧率:通过重复使用已创建的线程,降低线程创建和销毁造成的开销;
  • 提高相应速度:当任务到达时,任务不需要等待线程创建结束即可实行;
  • 提高线程的可管理性:线程是稀缺资源,假如无限制的创建,不仅会斲丧系统资源,还会降低系统的稳定性,使用线程池可以进行同一分配、调优和监控;
创建线程池

可以使用内置的线程池,通过 Executor 框架的工具类 Executors 来创建预先定义好的线程池。
Executors

Executors 工具类提供的创建线程池的方法如下图所示:

 
从上图中可以看出,Executors 工具类可以创建多种类型的线程池,包罗:

  • FixedThreadPool:固定线程数量的线程池,在创建该线程池时,需要传入一个线程池中线程个数的 int 参数,当有一个新的任务提交时,线程池中若有空闲线程,则立即实行。若没有空闲线程,则新的任务会被暂存在一个任务队列中,待有线程空闲时处理惩罚。
  • SingleThreadPool:单线程线程池,在该线程池中只有一个线程,若超过一个线程提交到该线程池,任务会被保存到任务队列中,比及该线程空闲时,按照先入先出的顺序实行队列中的任务。
  • CachedThreadPool:可缓存线程的线程池,该线程池的线程数量不确定,在优先使用空闲线程的条件下,碰到新的任务提交时,会创建一个新的线程来处理惩罚任务,任务处理惩罚完毕后回到线程池等待复用。
  • ScheduledExecutorPool:给定的延迟后运行的任务或定期实行任务的线程池。
自定义创建

如下图,可以通过 ThreadPoolExecutor 构造函数来创建线程池(推荐)。

 
优先推荐使用 ThreadPoolExecutor 来创建线程池,在《阿里巴巴 Java 开辟手册》中指出线程资源必须使用线程池来提供,不答应在应用中自行表现创建线程,也强制线程池不答应使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式来创建线程池。
使用内置的线程池有以下缺点:

  • newFixedThreadPool 和 SingleThreadPool使用的是无界队列 LinkedBlockingQueue,任务队列最大成都为 Integer.MAX_VALUE,大概堆积大量的请求,从而导致 OOM;
  • CachedThreadPool:使用的是同队伍列 SyschronousQueue,答应创建的线程数量为 Integer.MAX_VALUE, 假如任务实行较慢,大概会创建大量的线程,从而导致 OOM。
  • ScheduledExecutorPool:使用的无界的延迟壅闭队列 DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,大概堆积大量的请求,从而导致 OOM;
实际上内置的线程池也是调用 ThreadPoolExecutor 来创建的线程池:
  1. // 无界队列 LinkedBlockingQueue
  2. public static ExecutorService newFixedThreadPool(int nThreads) {
  3.         return new ThreadPoolExecutor(nThreads, nThreads,
  4.                                       0L, TimeUnit.MILLISECONDS,
  5.                                       new LinkedBlockingQueue<Runnable>());
  6. }
  7. // 无界队列 LinkedBlockingQueue
  8. public static ExecutorService newSingleThreadExecutor() {
  9.         return new FinalizableDelegatedExecutorService
  10.             (new ThreadPoolExecutor(1, 1,
  11.                                     0L, TimeUnit.MILLISECONDS,
  12.                                     new LinkedBlockingQueue<Runnable>()));
  13. }
  14. // 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
  15. public static ExecutorService newCachedThreadPool() {
  16.         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
  17.                                       60L, TimeUnit.SECONDS,
  18.                                       new SynchronousQueue<Runnable>());
  19. }
  20. // DelayedWorkQueue(延迟阻塞队列)
  21. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
  22.     return new ScheduledThreadPoolExecutor(corePoolSize);
  23. }
  24. public ScheduledThreadPoolExecutor(int corePoolSize) {
  25.     super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
  26.           new DelayedWorkQueue());
  27. }
复制代码
线程池参数

我们来看一下自定义创建线程池的参数有哪些?
[code]    public ThreadPoolExecutor(int corePoolSize, // 核心线程数                              int maximumPoolSize, // 最大线程数                              long keepAliveTime, // 当线程数大于核心线程数时,                                                  // 多余的空闲线程存活时间                              TimeUnit unit, // 时间单元                              BlockingQueue workQueue, // 任务队列                              ThreadFactory threadFactory, // 线程工厂,用于创建线程,一般默认                              RejectedExecutionHandler handler) { // 拒绝策略        if (corePoolSize < 0 ||            maximumPoolSize

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

风雨同行

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表