线程的实现方式

  1. 继承 Thread 类

  2. 实现 Runnable 接口

  3. 使用 Callable 和 Future

Thread 类中的 start()run() 方法有什么区别?

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码;通过调用 Thread 类的 start () 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此 Thread 类调用方法 run () 来完成其运行操作的, 这里方法 run () 称为线程体,它包含了要执行的这个线程的内容, Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
  2. run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待 run 方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程 —— 这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

线程 RUNNABLE 状态

线程对象通过 start 方法进入 runnable 状态,启动的线程不一定会立即得到执行,线程的运行与否要看 cpu 的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。

线程的 BLOCKED 状态

线程正在等待获取锁。

进入 BLOCKED 状态,比如调用了 sleep , 或者 wait 方法
进行某个阻塞的 io 操作,比如因网络数据的读写进入 BLOCKED 状态
获取某个锁资源,从而加入到该锁的阻塞队列中而进入 BLOCKED 状态

线程的 TERMINATED 状态

`TERMINATED`` 是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。

下面几种情况会进入 TERMINATED 状态:

线程运行正常结束,结束生命周期
线程运行出错意外结束
JVM Crash 导致所有的线程都结束

如何知道代码段被哪个线程调用?

1
System.out.println(Thread.currentThread().getName());

停止线程

run 方法执行完成,自然终止。

stop() 方法, suspend() 以及 resume() 都是过期作废方法,使用它们结果不可预期。

大多数停止一个线程的操作使用 Thread.interrupt() 等于说给线程打一个停止的标记,此方法不回去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

interruptedisInterrupted

interrupted : 判断当前线程是否已经中断,会清除状态。

isInterrupted :判断线程是否已经中断,不会清除状态。

yield

放弃当前 cpu 资源,将它让给其他的任务占用 cpu 执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 cpu 时间片。

线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到 cpu 资源比较多,也就是 cpu 有限执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。

Java 的优先级分为 1~10 个等级,数字越大优先级越高,默认优先级大小为 5。超出范围则抛出: java.lang.IllegalArgumentException

优先级继承特性

线程的优先级具有继承性,比如 a 线程启动 b 线程, b 线程与 a 优先级是一样的。

线程种类

Java 线程有两种,一种是用户线程,一种是守护线程。

守护线程的特点

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当 Java 虚拟机中不存在非守护线程时,守护线程才会随着 JVM 一同结束工作。

Java 中典型的守护线程

GC (垃圾回收器)

如何设置守护线程
Thread.setDaemon(true)

PS:Daemon 属性需要再启动线程之前设置,不能再启动后设置。

join

join 是指把指定的线程加入到当前线程,比如 join 某个线程 a, 会让当前线程 b 进入等待,直到 a 的生命周期结束,此期间 b 线程是处于 blocked 状态。

什么是 synchronized?

synchronized 关键字可以时间一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对想的所有读写都将通过同步的方式来进行。

synchronized 包括哪两个 jvm 重要的指令?

monitor enter 和 monitor exit

synchronized 关键字用法?

可以用于对代码块或方法的修饰

synchronized 锁的是什么?

普通同步方法 —————> 锁的是当前实力对象。

静态同步方法 —————> 锁的是当前类的 Class 对象。

同步方法 块 —————> 锁的是 synchonized 括号里配置的对象。

Java 对象头

synchronized 用的锁是存在 Java 对象头里的。对象如果是数组类型,虚拟机用 3 个字宽 (Word) 存储对象头,如果对象是非数组类型,用 2 字宽存储对象头。

Tips:32 位虚拟机中一个字宽等于 4 字节。

volatile 关键字

volatile 是轻量级的 synchronized, 它在多处理器开发中保证了共享变量的 “可见性 “。

Java 语言规范第 3 版对 volatile 定义如下,Java 允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为 volatile,Java 线程内存模型所有线程看到这个变量的值是一致的。

重入锁 ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

  1. 锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。

  2. 所得最终释放。线程重复 n 次是获取了锁,随后在第 n 次释放该锁后,其他线程能够获取到该锁。

ReentrantLock 默认锁?

默认非公平锁

公平锁和非公平锁的区别

公平性与否针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO

读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻塞。读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。
Java 中提供读写锁的实现类是 ReentrantReadWriteLock

Java 并发容器,你知道几个?

ConcurrentHashMap、CopyOnWriteArrayList 、CopyOnWriteArraySet 、ConcurrentLinkedQueue、

ConcurrentLinkedDeque、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、

LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、SynchronousQueue、

LinkedTransferQueue、DelayQueue

Java 里的阻塞的队列

工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

创建线程池参数有哪些,作用?

  1. corePoolSize: 核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。

  2. maximumPoolSize: 线程池最大数,允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。

  3. keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多,并且每个任务执行时间较短,可以调大时间,提高线程利用率。

  4. unit: 线程保持活动时间单位,天(DAYS)、小时 (HOURS)、分钟 (MINUTES、毫秒 MILLISECONDS)、微秒 (MICROSECONDS)、纳秒 (NANOSECONDS)

  5. workQueue: 任务队列,保存等待执行的任务的阻塞队列。

一般来说可以选择如下阻塞队列:

ArrayBlockingQueue : 基于数组的有界阻塞队列。

LinkedBlockingQueue : 基于链表的阻塞队列。

SynchronizedQueue : 一个不存储元素的阻塞队列。

PriorityBlockingQueue : 一个具有优先级的阻塞队列。

向线程池提交任务

可以使用 execute()submit() 两种方式提交任务。

execute() : 无返回值,所以无法判断任务是否被执行成功。

submit() : 用于提交需要有返回值的任务。线程池返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future的get() 来获取返回值, get() 方法会阻塞当前线程知道任务完成。 get(long timeout,TimeUnit unit) 可以设置超市时间。

关闭线程池

可以通过 shutdown () 或 shutdownNow () 来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 来中断线程,所以无法响应终端的任务可以能永远无法停止。

shutdownNow 首先将线程池状态设置成 STOP, 然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。

shutdown 只是将线程池的状态设置成 shutdown 状态,然后中断所有没有正在执行任务的线程。

只要调用两者之一,isShutdown 就会返回 true, 当所有任务都已关闭,isTerminaed 就会返回 true。

一般来说调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,可以直接调用 shutdownNow 方法。

线程池如何合理设置

配置线程池可以从以下几个方面考虑。

  • 任务是 cpu 密集型、IO 密集型或者混合型
  • 任务优先级,高中低。
  • 任务时间执行长短。
  • 任务依赖性:是否依赖其他系统资源。
    • cpu 密集型可以配置可能小的线程,比如 n + 1 个线程。

    • io 密集型可以配置较多的线程,如 2n 个线程。

    • 混合型可以拆成 io 密集型任务和 cpu 密集型任务,

    • 如果两个任务执行时间相差大,否 -> 分解后执行吞吐量将高于串行执行吞吐量。
      否 -> 没必要分解。

    • 可以通过 Runtime.getRuntime ().availableProcessors () 来获取 cpu 个数。

    • 建议使用有界队列,增加系统的预警能力和稳定性。

JDK 内置的拒绝策略

  • AbortPolicy 抛出异常。

  • DiscardPolicy 直接静悄悄的丢弃这个任务,不触发任何动作。这个策略基本不会使用

  • DiscardOldestPolicy 丢弃队列最前面(最旧)的任务,然后重新尝试执行任务(重复此过程)。

  • CallerRunsPolicy 由调用线程处理该任务 。

Edited on Views times