1. 概述
2. 进程与线程
2.1 程序,进程与线程
- 程序:由指令和数据组成,是静态的。
- 进程:程序中的指令需要加载到CPU,数据需要加载至内存。进程就是用来加载指令,管理内存。当一个程序被运行,从磁盘中加载这个程序到内存中就开启了一个进程程。
- 线程:一个进程之内有一到多个线程。Java0中,线程作为最小调度单位,进程作为资源分配的最小单位。
进程 VS 线程
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
- 进程拥有共享的资源。
3. Java线程
3.1 三种创建方法
- 继承 Thread 类并重写 run 的方法:
- 优势:
- 在 run() 方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread() 方法。
- 缺点:
- 不能继承其它类。
- 任务和代码不分离,多个线程执行一样的任务时需要多份任务代码(需要在run方法中写同样的代码)。
- 优势:
- 实现 Runnable 接口的 run 方法,传给 Thread :
- 优势:
- 任务和代码分离。
- 可以继承其他类。
- 优势:
- 实现Callable 接口的 call() 方法,传给 FutureTask :
- 优势:拥有返回值。
3.2 start 与 run
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程。
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。
3.3 sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)。
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出
InterruptedException。 - 睡眠结束后的线程未必会立刻得到执行,虽然锁没用释放但是也需要去抢占时间片。
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性,因为带时间单位。 yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程。
- 具体的实现依赖于操作系统的任务调度器。
- 会让出CPU给其他线程。
- 当没有其他线程可让时候当前线程会继续执行。
3.4 join方法
- 作用:用于线程同步。
- join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法.会使得调用线程执行完成后继续执行。
3.5 Interrupt
- 调用线程的interrupt方式并不是会结束线程,而是会设置线程的打断标记为true,我们需要自行对打断标记进行相应的动作。。
- 但是当线程处于sleep、wait、join状态时使用interrruop方法会抛出
InterruptedException异常,清空打断状态设置为false。 - 打断正常运行的线程, 不会清空打断状态。
Interrupt打断LockSupport.park() 使用interrupt方法打断park方法时不会抛出InterruptedException异常也不会重置打断标记为false,而是把打断标记设置为ture,但是当使用park方法被打断后再使用LockSupport.park()就不会生效,我们需要使用interrupted方法重置打断标记为false后再使用LockSupport.park()才会生效。
3.6 两阶段终止模式之如何”优雅“的终止线程
1. 错误思路
- 使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
- 使用 System.exit(int) 方法停止线程目的仅是停止一个线程,但这种做法会让整个程序都停止。
2. 两阶段终止模式
- 利用 isInterrupted 方法来判断结束的时机,然后料理后事。
3.7 守护线程与用户线程
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。两者区别之一是当最后一个非守护线程结束时,JVM 会正常退出,垃圾回收线程就是一个守护线程。
3.8 线程的六种状态
NEW、RUNNABLE、TERMINATED、BLOCKED、WAITINF、TIMED_WAITING
- NEW: 创建了线程而未调用start方法
- RUNNABLE:线程得到了CPU的分片处于运行状态
- TERMINATED:执行结束的线程处于这一状态
- BLOCKED:当线程去申请其他线程已经在使用的独占锁时,会处于这一状态
- WAITINF:调用wait、join等方法时,会进入到这一状态
- TIMED_WAITING:与WAITINF类似,只不过不是无限制的等待,如sleep()
4. 共享模式之管程
4.1 Monitor 概念
Java 对象头格式:根据不同的状态 Mark Word 也不相同
- Monitor 被翻译为监视器或管程
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 Monitor 结构如下:
- WaitSet:等待队列,线程调用 wait() 方法后会进入到此队列。
- EntrtList: 堵塞队列,抢占不到锁时,就会进入到此队列。
- Owner: 指向拥有锁的线程,只能有一个线程拥有锁。
- 刚开始 Monitor 中 Owner 为 null。
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中能有一个 Owner。
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,调用了 wait() 方法,等待其他线程的 notify。
4.2 synchronized 原理
4.2.1 偏向锁
偏向锁的使用场景:只有一个线程持有过这个锁,多个线程错开加锁就会升级为轻量级锁。
4.2.2 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争)。
4.2.3 重量级锁
重量级锁的使用场景:多个线程对同一个对象锁发生竞争。
4.2.4 锁膨胀
偏向锁多个线程错开加锁时会膨胀为轻量级锁,如果在尝试加轻量级锁的过程中,发现有多个线程产生竞争时,这时需要进行锁膨胀,申请 Mointor 将轻量级锁变为重量级锁。
4.2.5 自旋优化
当线程去访问同步块,获取 monitor 时发现 Owner 不为 null,这时不会直接进入到 EntryList 中,而会自旋几次尝试去获得锁。
4.3 wait/notify/notifyAll
**这三个方法必需在拥有锁的情况下才能使用,不然会抛出异常IllegalMonitorStateException。**obj为锁对象。
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待。
- obj.notify() 在 object 上正在 Monitor 中的 waitSet 等待的线程中 挑一个(不按照 wait 顺序) 唤醒。
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒。
wait 和 sleep 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法 。
- wait会释放锁,sleep不会释放锁。
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用。
wait 正确的使用方式:
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
4.4 LockSupport
- 暂停当前线程:LockSupport.park()
- 恢复某个线程的运行:LockSupport.unpark(暂停线程对象)
park 和 unpark VS wait 和 notify
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程.
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
- wait,notify 和 notifyAll 必须配合锁对象一起使用,而 park,unpark 不必。
7. 死锁/活锁/饥饿
- 死锁: t1 线程获得 A 对象锁,接下来想获取B对象的锁 t2 线程获得 B 对象锁,接下来想获取 A 对象的锁。
- 活锁: 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
- 饥饿: 一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。
8. ReentrantLock
ReentrantLock有以下特点:
- 可重入: 重入是指同一个线程如果首次获得了这把锁,那么它成为这把锁的拥有者,再次去申请这把锁时不再受到阻碍。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
- 可打断: ReentrantLock提供了lockInterruptibly方法,可以使用interrupt()去打断。
- 锁超时: ReentrantLock提供了tryLock方法,尝试去得到锁,得到返回true,失败为false。
- 公平锁: 通过构造函数传递参数 ture 可以设置为公平锁;公平锁按照先后顺序来调度线程,而非公平锁可能最后到的最先执行。
- 条件变量(Condition): synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
ReentrantLock VS synchronized
- 可中断。
- 可以设置超时时间。
- 可以设置为公平锁。
- 支持多个条件变量。
5. 共享模型之内存
5.1 可见性
- 因为高速缓存的存在,线程 A 对一个变量的修改,线程 B 可能看不到这次的修改,因为线程 A 的修改后的数据可能没返回到主存中。
5.2 原子性
- 操作具有不可分割性,在整个原子性的操作中,不允许其他线程介入。 原子性 + 原子性 ≠ 原子性
5.3 有序性
- JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,也就是指令重排
6. 共享模型之无锁
6.1 volatile
可以修饰成员变量和静态成员变量,保障可见性、有序性和原子性。
6.2 内存屏障
- 可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
volatile 的底层实现原理是内存屏障
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
6.2 CAS
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
CAS 的特点:
- 结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于乐观锁的思想:当其他线程来修改共享变量后,自己会进行重试进行修改。
- CAS 体现的是无锁并发、无阻塞并发。
- 因为没有使用 synchronized,所以线程不会陷入阻塞
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
7 共享模型之工具
7.1 线程池
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。将线程池状态和数量放在同一个变量中,是为了保护原子性,让修改线程池和数量时不用分成两步。
| 状态名 | 高3位 | 说明 |
|---|---|---|
| RUNNING | 111 | 运行状态 |
| SHUTDOWN | 000 | 不会接收新任务,但会处理阻塞队列剩余任务 |
| STOP | 001 | 会中断正在执行的任务,并抛弃阻塞队列任务 |
| TIDYING | 010 | 任务全执行完毕,活动线程为 0 即将进入终结 |
| TERMINATED | 011 | 终结状态 |
7.1.1 线程池构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时起个好名字
- handler 拒绝策略
- 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排队。
- 如果等待队列选择了有界队列,那么任务超过了队列大小时,会创建 (maximumPoolSize - corePoolSize) 数目的救急线程来使用。
- 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现。
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
7.1.2 线程池的三个实现
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
评价:适用于任务量已知,相对耗时的任务
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点:
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收)
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货),因为没有核心线程来取,这样队列中不会有线程,那么保证了所有线程都是救急线程。
评价:适合任务数比较密集,但每个任务执行时间较短的情况
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(
1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特点:
- 线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
- 与newFixedThreadPool(1) 的区别:FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法,而 newFixedThreadPool(1) 初始时为1,以后还可以修改
评价:适合串行执行的场景。
7.2 J.U.C
7.2.1 AQS原理
AQS概述 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具实现的同步器框架。 特点: -用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。例如:state为0共享模式,1为独占模式。 - compareAndSetState - cas 机制设置 state 状态。 - 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源。
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList,一个线程占有这个锁后,其它线程就会进入等待队列,进入等待。
- 条件变量来实现等待、唤醒机制,支持多个条件变量。
关键点:
- 原子维护 state 状态
- 堵塞及恢复线程
- 维护队列
7.2.8 线程安全类集合概述
分为三大类:
- 遗留的线程安全集合如Hashtable,Vector
- Hashtable:hashMap的安全集合实现,内部使用synchronized实现,并发性能低。
- Vector:list的安全集合的实现,
- 使用 Collections 装饰的线程安全集合:
- Collections.synchronizedCollection
- Collections.synchronizedList
- Collections.synchronizedMap
- Collections.synchronizedSe
- java.util.concurrent.*
- Blocking大部分实现基于锁(ReentrantLock),不满足条件时阻塞。如BlockingQueue。
- CopyOnWrite对于修改的开销很大。
- Concurrent类型的容器,内部大部分使用cas操作优化,提高并发量。具有一定的弱一致性,当然弱一致性和并发性是不可共存的。
7.2.11 CopyOnWriteArrayList
- 底部是采用写入时拷贝的思想。
- 写入时拷贝:增删改操作会将低层数组拷贝一份,其操作都会在新数组上执行,这时其他线程可以并发读,只不过在复制的过程中,得到的是旧数据。
- 适合读多写少的应用场景
- CopyOnWriteArraySet内部也是使用CopyOnWriteArrayList实现的。