线程池有什么作用?创建线程池的时候需要哪些参数?使用线程池的时候有哪些注意事项?
一个Java的线程对应了操作系统内核的线程,直接new一个线程是非常消耗资源的,创建线程池便于统一管理线程,可以大大降低资源消耗。
ThreadPool有七大参数
阿里开发手册:不要new,可能存在OOM内存溢出的风险,
sleep()方法和wait()方法的区别
sleep()方法它是Thread类的一个静态方法,主要用于暂停线程的执行,可以把该线程占用的CPU让给其他线程,但是仍然保持对锁的使用。
wait()方法是Object类的一个方法,通常与同步方法或者同步代码块一起使用。当一个线程在在调用wait()方法之后,它会释放对象的锁,并进入该对象的等待队列waitset中,调用wait()方法的线程会一直等待,直到其他线程调用同一个对象的notify()或者notifyAll()方法。wait()方法多用于线程之间的协作。
Blocked是正在等待锁的线程,在EntryList里面
Waiting是获得了锁,但是不满足某个条件调用了wait方法的线程,在WaitSet里面
Waiting线程在Owner线程调用notify()或者notifyAll()方法时唤醒,进入EntryList重新竞争锁
Blocked线程在Owner线程释放锁时,会唤醒EntryList中的方法
必须要获得锁synchronized(obj)才能使用wait方法和notify方法
synchronized(lock) {
while (!condition) {
lock.wait();
}
//do something
}
//another Thread
synchronized(lock) {
lock.notifyAll();
}
线程的生命周期
五种状态(操作系统)
初始状态:相当于Java里面new了一个Thread对象,但是没有调用它的start方法
可运行状态:就绪状态,可以被操作系统的调度程序调度,时刻准备着被CPU分到时间片去运行
运行状态:获得CPU时间片运行的状态
运行状态的线程在CPU时间片用完之后,会转化为可运行状态,会导致线程的上下文切换
阻塞状态:如果调用阻塞API,比如说BIO读写文件,这时候该线程实际上不会用到CPU,会导致线程的上下文切换。在BIO结束之后,操作系统会将线程转换为可运行状态,阻塞状态没有获得CPU时间片的能力
终止状态:线程执行完毕
六种状态(JUC API)
NEW:线程被状态但是没有调用start方法
RUNNABLE:操作系统的可运行状态、运行状态和阻塞状态都是Runnable状态
BLOCKED:和操作系统的阻塞状态不是一个状态,操作系统的阻塞状态是指没有获得时间片,BLOCKED是指没有获得对象锁,进入了这个锁monitor得entryList里面等待获得锁而阻塞
WAITING:如果调用obj.wait方法或者join方法,等待其他线程notify它
TIMED_WAITING:有时限的等待
1 | new Thread().start() | 6 | t.join(n)/t.interrupt() |
2 | obj.wait()/obj.notify/t.interrupt() | 7 | LockSupport.parkNanos()/ |
3 | t.join()/t.interrupt() | 8 | Thread.sleep() |
4 | LockSupport.park()/unpark() | 9 | 获取锁对象竞争失败 |
5 | obj.wait(n)/ | 10 | 当前线程运行完毕 |
Java对象头的保存方式
1,Mark Word
2,指向类的指针
3,数组长度(只有数组对象才有)
synchronized实现原理
synchronized就是对象锁,它能够实现同一时刻至多只有一个线程持有对象锁,其他对象再想要获得这把锁就会阻塞住,在底层,就是这个对象关联了一个monitor,持有这把锁的线程是owner,让后没有竞争到的线程会在entryList中等待,处于blocked状态。如果线程因为缺少条件而调用obj.wait()方法,就会进入waitset,处于wait状态,只有其他线程在持有锁的过程中调用obj.notify()或者notifyAll()方法时,才会重新进入entryList,获得竞争锁的能力。
轻量级锁实现原理
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word。
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。
-
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。
-
如果 cas 失败,有两种情况:如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程;如果是本线程执行了 synchronized 锁重入,那么再添加一条 LockRecord 作为重入的计数。
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入,计数减一; 当退出 synchronized 代码块(解锁时)锁记录的值不为 null ,这时使用 cas 将 Mark Word 的值恢复给对象 头。 成功,则解锁成功; 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
锁膨胀实现原理
线程加锁的优先级
偏向锁-轻量级锁-重量级锁
为什么调用对象的hashcode之后就不再是偏向锁状态?
因为在正常状态,对象的对象头中保存了31位的hashcode,而使用偏向锁之后,54位的空间都被分配给了线程ID,没有空间存储hashcode。而对于轻量级锁和重量级锁,虽然对象头会指向锁记录或者monitor,但也把hashcode保存在了锁记录或者monitor中,等锁释放的时候会交换回来。
HashMap并发死链问题
只有在JDK7版本下才会发生
先在HashMap里面放12个元素,放第13个元素的时候会触发扩容机制
用两个线程分别放第13个元素,两个线程在操作一个哈希槽的链表产生了死循环问题
ConcurrentHashMap实现原理
1.8
带有forwarding node表示是处理过的,从后往前进行处理,其他线程看到这个node之后就会知道这个node已经被处理过,在get过程中发现某一个node是forwarding node就会到新的table里面去找相应的key
在链表长度大于8且node长度大于64时,会触发树化操作,TreeBin是头结点,TreeNode是红黑树里面的每一个节点
初始化initTable:
get方法:
put方法:
chm不允许有空的键值
cas来保证不会有多个线程重复创建哈希表
只有在node下标冲突时才会加锁,锁的是node头结点,