线程基础
线程与进程的区别
- 进程是程序的一次执行过程。它资源分配的单位。
- 线程是程序执行的单位。
并行和并发的区别
- 单核CPU下,线程串行。(并发:多线程轮流使用一个或多个CPU)
- 多核CPU下,每个核都可调度线程。(并行:多CPU同时执行多个线程。
创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程
runnable和callable的区别
- Runnable接口run方法没有返回值;Callable接口call方法有返回值
- Runnable接口run方法的异常只能内部消化不能上抛;Callable接口call方法允许抛出异常
线程的run()和start()的区别
- start():用于启动线程,只能被调用一次。
- run():封装要被线程执行的代码,可以被调用多次。
wait()和sleep()方法的区别
1.方法归属不同
- sleep() 是Thread类的静态方法
- wait() 是Object类的成员方法,任何对象都有
2.唤醒时机不同
- 执行 sleep() 的线程会在等待相应毫秒后唤醒
- wait() 可以被 notify() 唤醒
3.锁特性不同
- wait()方法的调用必须先获取对象的锁;sleep()不需要
- wait()执行完会释放对象锁;sleep()若在synchronized中执行,不释放对象锁
线程的状态与转换条件
三个线程如何顺序执行
使用线程中的join()方法解决。
t.join():等待线程运行结束。阻塞调用此方法的线程,直到t线程执行完成。
并发安全
Synchronized
实现原理
基于进入和退出Monitor对象来实现方法同步和代码块同步。
- 方法级的同步是隐式,JVM可以通过 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
- 代码块的同步是利用monitorenter和monitorexit这两个字节码指令。
底层实现
synchronized的底层实现是依赖于Java对象头,以及Monitor对象监视器。
Monitor监视器锁有三个重要属性:_Owner 、_WaitSet 和 _EntryList 。
- _owner指向持有ObjectMonitor对象的线程。
- 当多个线程同时访问时,首先会进入 _EntryList 集合等待。
- 当线程获取到对象的monitor 后,把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1。
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。
Monitor实现的锁属于重量级锁,于是有了锁升级,基于对象头(MarkWord)。
- 最轻程度的锁为偏向锁,资源总是由同一线程多次获得。偏向锁只依赖于锁对象,锁对象在64位虚拟机里由64Bit的Markword来控制,线程获取锁时,通过CAS方式将线程ID设置到对象头里的MarkWord的Thread ID中,线程ID指针在MarkWord中占用54个比特位。偏向锁的标识位为101
- 当对象进行了hash操作,那么锁就会失效,因为HashCode在MarkWord中占用31个比特位。无锁的表示位为001
- 接着被另外的线程所访问,偏向锁升级为轻量级锁,MarkWord中设置指向线程栈的lock record指针。其他线程会自旋尝试获取锁,不会阻塞。轻量级锁的标识位为000
- 一旦线程竞争,升级为重量级锁,其他线程都会被阻塞。重量级锁的标识位为010
JMM(java内存模型)
- JMM把内存分为两块,一块是私有线程的工作内存,一块是所有线程的共享区域(主内存)。
- 线程与线程间相互隔离,交互需通过主内存。
Volatile关键字
- 保证线程间的可见性:修饰共享变量,防止编译器等优化发生,让线程对共享变量的修改对另一个线程可见。
- volatile禁止指令重排序:修饰共享变量会在读、写共享变量时加入不同屏障,阻止其他读写操作,从而阻止重排序。
AQS
全称为AbstractQueuedSynchronizer(抽象队列同步器)。它是构建锁的基础框架。
- AQS中有个属性state表示状态,0为无锁,1为有锁。
- 还维护了一个双向队列作为FIFO队列,其他线程会进入队列等待,线程释放锁时会唤醒队列中head的元素。
ReentrantLock(可重入锁)
可重入锁指:调用lock()方法获取了锁后,再调用lock(),是不会再阻塞的。
ReentrantLock利用CAS+AQS队列实现。支持公平锁和非公平锁。
Synchronized和Lock的区别
- synchronized是关键字,在 jvm 中由c++实现;Lock 是接口,在 jdk 中由 java 实现。
- synchronized退出锁时自动释放;Lock需要调用unlock方法释放。
- Lock的功能比synchronized多,如公平锁。
死锁产生的条件
互斥,请求保持,不可剥夺、循环等待。
(一个线程需要同时获取多把锁,容易发生死锁。)
线程池
线程池核心参数
- 核心线程数
- 最大线程数
- 生存时间(救急线程的生存时间)
- 时间单位
- 任务队列:当没有空闲核心线程时,新任务到此队列等待,队列满就会创建救急线程。(ArrayBlockingQueue 和 LinkedBlockingQueue)
- 线程工厂
- 拒绝策略:当所有线程在忙,工作队列也满,才会触发。
- AbortPolicy:抛出异常,默认策略;
- CallerRunsPolicy:调用者的线程来执行;
- DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,执行当前任务;
- DiscardPolicy:丢弃任务。
如何确定核心线程数
- IO密集型任务:核心线程数大小设置为2N+1
- CPU密集型任务(或者高并发、任务执行时间短):核心线程数大小为N+1
线程池的种类
- Executors.newFixedThreadPool():固定大小的线程池,核心线程数与最大线程数相等
- Executors.newSingleThreadExecutor():单线程化的线程池,保证任务FIFO执行
- Executors.newCachedThreadPool():可缓存的线程池,核心线程数为0
- Executors.newScheduledThreadPool():提供了“延迟”和“周期执行”功能
不推荐用Executors创建线程池
应该使用7个参数的ThreadPoolExecutor的方式,按需设置核心线程数和最大线程数,避免无限队列长度,规避OOM。
ThreadLocal
ThreadLocal是解决线程安全问题的一个操作类,它为每个线程分配一个独立的内部存储空间,实现了线程内的资源共享。
- set(value):设置值。根据当前线程对象,通过getMap()获取ThreadLocalMap。
- get():获取值。通过getEntry()获取ThreadLocalMap中的Entry对象。通过HashCode & (数组长度 - 1) 定位数组下标。
- remove():清除值。同get()
ThreadLocal内存泄漏
强引用:表示一个对象处于有用且必须的状态,GC无法回收处于强引用的对象,即便出现OOM。
User user = new User();
弱引用:表示一个对象处于可能有用但非必须的状态。GC一旦发现弱引用,会回收相关联的对象。
User user = new User();
WeakReference weakRef = new WeakReference(user);
每一个Thread维护一个ThreadLocalMap,Entry对象继承了WeakReference。其中key是弱引用的ThreadLocal实例,value是强引用的线程变量副本。
避免内存泄漏
在使用ThreadLocal后主动使用remove()方法释放key、value。