一. Thread的四种创建方式
1. 继承Thread类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
2. 实现Runnable接口中run方法
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
3. FutureTask包装Callable
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
4. 从线程池中获取线程
二. Thread的五种状态
五种状态,创建、就绪、运行、阻塞和死亡
- 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
- 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
- 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
- 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
三. 进程与线程的区别
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
四. Thread.sleep() 和Object.wait() 的区别
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,不会释放锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
五. notify()和notifyAll()的区别
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
六. 线程池
1. 线程池的三大方法、七大参数、四种策略
七大参数:
- corePoolSize, 核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
- maximumPoolSize, 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
- keepAliveTime, 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
- unit,时间单位
- workQueue, 保存任务的阻塞队列
- threadFactory, 创建线程的工厂
- handler, 拒绝策略
线程池中线程创建流程是:
corePoolSize --》 workQueue --》 maximumPoolSize --》 rejectedExecution
- 当线程数小于corePoolSize时,创建线程执行任务。
- 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
- 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
- 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。
ThreadPoolExecutor默认有四个拒绝策略:
- ThreadPoolExecutor.AbortPolicy() 直接抛出异常RejectedExecutionException
- ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行
- ThreadPoolExecutor.DiscardPolicy() 直接丢弃后来的任务
- ThreadPoolExecutor.DiscardOldestPolicy() 丢弃在队列中队首的任务
2. 线程池submit()和execute()的区别
(1)submit()有返回值,而execute()没有返回值。
(2)submit()方便Exception处理,意思就是如果你的task里会抛出exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
七. 锁
1. 锁的几种状态
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
重量级锁,上锁、协调需要操作系统协调和调度
轻量级锁,自旋锁(CAS算法-compare and swap 比较和交换),cas操作怎么避免aba问题?可以通过加版本号的方式解决,版本号用原子类实现,如atomicInteger等可以实现版本号
偏向锁,单线程情况下,在对象头部打标签,标记为偏向锁,超过两个以上的线程,转为轻量级锁
2. 几种常见的锁
自旋锁,当一个线程准备获取锁时,此时锁被其他线程获取,当前线程循环等待获取锁,不停判断是否获取到锁,成功获取到锁时,则退出循环。减少了多线程环境下上下文切换的开销
乐观锁、悲观锁,自旋锁是乐观锁的一种,synchronized 锁是一种悲观锁,独占锁,互斥锁
可重入锁,那是因为这种锁是可以反复使用,这里的反复使用仅局限于一个线程。
公平锁,在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可以获得锁还是线程2可以获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个。因此不能保证其公平性,而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。所以我的理解是公平锁是可充入锁的一种特例。
八. ThreadLocal的实现原理以及使用场景
ThreadLocal底层是通过ThreadLocalMap实现的。ThreadLocalMap是是ThreadLocal的一个内部类,但是使用的时候是在Thread中使用的。每一个线程都有一个ThreadLocalMap,这个Map的key是ThreadLocal对象,value就是我们塞进去的值。
使用场景:
(1)ThreadLocal可以用来做参数传递,从Controller一直传递到service层
(2)不同线程间数据隔离
(3)用于事务操作,用于存储线程事务信息。
Spring事务开始的时候,会给当前线程绑定一个JDBC Connection,在整个事务执行过程中都用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。