面试题:
线程有哪些状态?
Java中分成6种状态(原代码里写的):
新建状态New:创建线程
可运行状态Runnable:线程调用start()后进入运行状态,操作系统交给cpu去执行。
终结状态Terminated:代码执行完毕进入终结状态。
阻塞状态Blocked:可运行状态的线程获取锁失败了进入阻塞状态,当持锁线程释放锁后,让其余线程竞争锁,阻塞的线程成功获得锁之后再次进入可运行状态。
等待状态Waiting:可运行状态的线程获得锁但是条件不满足,调用wait()方法释放锁进入等待状态,等条件满足时由其他线程调用notify()方法唤醒等待状态的线程后,线程重新争抢锁,抢到了再进入可运行状态,没抢到就是blocked状态。
超时等待Timed_Waiting:和等待状态不一样的是wait()方法有时间参数,醒来的条件不一样,时间到或者被线程notify()唤醒都会醒来,然后醒着接着抢锁 / 调用sleep()和锁,是否满足条件没关系。
5种状态(操作系统层面):
Java中的Runnable包含就绪,运行还有阻塞I/O
ThreadPoolExecutor线程池的核心参数
核心线程:执行完任务依旧需要保存在线程池。
救急线程:执行完任务不需要保存在线程池中。
7个重要参数:
corePoolSize 核心线程数目:最多保留的线程数
maximumPoolSize 最大线程数目:核心线程+救急线程<=最大线程数目
keepAliveTime 生存时间:针对救急线程
unit 时间单位:针对救急线程
workQueue :阻塞队列,缓冲任务——核心线程和workQueue都有上限,如果都满了再来任务就要创建救急线程来做任务了。
threadFactory 线程工厂:起名
handler 拒绝策略:核心线程和workQueue都有上限,而且救急线程也到上限了(二者的和<=maximumPoolSize),又来任务了怎么处理?
1.AbortPolicy移除策略:线程池资源耗尽了,所以抛出异常。
2.CallerRunsPolicy:哪个线程用线程池submit的就自己做一下这个任务。
3.DiscardPolicy:默默直接丢弃,也没有异常,忙不过来了。
4.DiscardOldestPolicy:把任务队列里等待最久的任务丢了,把新的任务放进队列。
Wait和Sleep的区别
共同点:
wait(),wait(时间)和sleep(时间)的都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
不同点:
1.方法归属(谁的方法):sleep是线程Thread的方法,wait是Object的成员方法。
2.醒来时间(醒来的方式):
sleep(时间)和wait(时间)都会在等待时间到后醒来;wait()和wait(时间)都可以被notify()唤醒,wait()不唤醒就会一直等待;sleep和wait都可以被打断唤醒。
3.锁的特性不同(是否获取和释放锁):
wait要先获取wait对象的锁,sleep不需要;wait执行后会释放锁,允许其他线程获得对象锁;synchronized代码块中如果执行sleep,不会释放锁。
lock VS synchronized
1.语法:
synchronized是关键字,用c++实现;lock是接口,用java实现;使用synchronized时退出同步代码块锁自动释放,而使用lock需要程序员手动unlock()释放锁;
2.功能:
相同:都是悲观锁,都有互斥,同步,锁重入(重复加锁)功能。
不同:lock可获取等待状态(哪些线程在等待),公平锁(先到先得,构造时参数为true),可打断,可超时(去获取锁),多条件变量(多个等待队列,new Condition());lock接口有多个不同场景实现。
注意:
用了lock()但是没抢到锁在blockedQueue里,抢到锁了但是条件不满足使用条件变量c.await()释放锁后在等待队列c中,条件满足时使用c.signal()或者c.signalAll()回到阻塞队列尾部;
如果用tryLock()无参方法,底层总是用到非公平锁。
3.性能:
没有竞争时,synchronized做了优化,如偏向锁,轻量级锁,性能不错;
竞争激烈时,Lock会提供更好的性能。
volatile能否保证线程安全?
volatile可以保证可见行和有序性,但是不能保证原子性。
线程安全的三个方面:
1.可见性:一个线程修改共享变量,另一个线程可以看到最新的结果。
2.有序性:一个线程内代码按编写顺序执行(不会指令重排)
3.原子性:一个线程内多行代码以一个整体运行,期间不能有其余线程代码插队。
可见性问题:
由于共享变量最新值获取失败导致某线程中代码陷入死循环,一般都认为是因为每个线程有自己的共享线程副本,更新后拿的是副本的值而不是内存的值所导致的可见性问题——然而不是共享变量副本所造成的,因为更新之后再开线程能够取到内存最新值。
原因是因为JIT,java即时编译,会陷入死循环的代码很明显运行非常频繁,JIT会自动将热点代码优化,比如stop一直读的都是false,他就直接把代码while(!stop)改成while(!false)来优化效率,但是这就会导致即使stop更新该线程也拿不到更新的true来结束循环。
只要不用JIT,如果只用解释器 || 将死循环的代码执行次数减少,JIT认为不是那么频繁没必要优化 就可以看到效果了。
但是禁用JIT优化或者缩短时间都会降低性能,只为一个变量读取最新值没有必要,所以根本解决办法是使用volatile修饰变量,这样JIT就会放过代码,不对它进行优化。
有序性:
因为编译时优化可能导致指令重排,所以会存在代码执行顺序和实际写代码的顺序不一样。
volatille可以禁止指令重排序。
volatile使用内存屏障避免指令重排,对volatile的变量进行写操作时,如下所示,要写你先写,我写就别干扰我。对volatile的变量进行读操作时,在volatile修饰的变量下面有读屏障,volatile修饰的变量先进行读操作。——被volatile修饰的变量:先读后写
注意屏障是单向的。
原子性问题:一些看起来是原子操作的代码实际在字节码中不是原子操作。
t2代码插队:volatile是在读取的时候拿到最新值,以下这种情况已经读取赋值过了就察觉不到值已经更新了。
可以用synchronized或者加锁保证原子性。
悲观锁和乐观锁:
悲观锁:线程只有占有锁后才能操作共享变量。每次只有一个线程占锁成功,没抢到锁的线程都要停下等待(可运行状态->阻塞状态)。
线程从运行到阻塞再到唤醒——牵涉到线程的上下文切换,频繁发生影响性能。
悲观锁代表:synchronized和Lock锁,实际中synchronized和Lock都做了优化,会多尝试几次来减少阻塞的机会。
乐观锁:不需要加锁,每次只有一个线程能成功修改共享变量,其他失败的线程也不停止,不断重试直到成功。
线程一直运行所以不用上下文切换,但是需要多核cpu线程支持且线程数不能超过cpu的核树。
乐观锁代表:AtomicInteger,使用cas(比较并交换)保证原子性。其底层是Unsafe。
举例加深理解:
static final Unsafe u = Unsafe.getUnsafe();
static final long BALANCE = u.objectFieldOffset(Account.class,"balance");
static class Account{
volatile int balance = 10;
}
public static void main(String args[]){
Account a = new Account();
int o = a.balance; //旧值
int n = o+5; //新值
//如果旧值o和BALANCE不一样证明有线程改过了,那么就会修改失败。
System.out.println(Unsafe.compareAndSetInt(account,BALANCE,o,n));
System.out.println(account.balance);
}
可以优化成以下代码:
public static void main(String args[]){
Account a = new Account();
while(true){
int o = a.balance; //旧值
int n = o+5; //新值
//修改成功就退出,没改成功就继续循环进行更改
if(Unsafe.compareAndSetInt(account,BALANCE,o,n)){
break;
}
}
}
HashTable vs ConcurrentHashMap
相同:都是线程安全的Map集合。
不同:
HashTable并发度低,整个HashTable一把锁,同一时刻只能有一个线程操作它。
ConcurrentHashMap:在1.8之前,数据结构为segment+数组+链表,每个segment一把锁,多个线程访问不同segment,不会发生冲突。
1.8之后,底层数据结构为:数组+链表/红黑树,将数组的链表/树的每个头节点作为锁,多个线程访问的头节点不同,不会冲突。因此数组容量决定其并发度。
加深ConcurrenHashMap的理解:
1.7的ConcurrentHashMap的索引计算:
二次哈希值最后m位代表是存放在segment的数组中的哪个位置,根据segment的数量是2的x次幂选择二次哈希值的高x位,计算其值得到该放在哪个segment中。
如何扩容?
segment数量=并发度,此后segment数量固定不会再变动。
segment的数组大小 = capacity/clever = 容量/并发度,扩容是扩大了segment中数组的容量,每次容量变成原来的2倍。
segment[0]已经有数组了,作用是为后面的segment提供原型,后面segment的小数组会和segment[0]的大小和扩容因子都copy走,也是设计模式中的原型模式的一种实现。
1.8的ConcurrentHashMap和1.7的不同:
底层数据结构:1.7是segment+数组+链表,1.8是数组+链表/红黑树。
初始化时机:1.7 是饿汉式,开始就有了;1.8是懒汉式,直到put第一个元素创建底层数据结构。
扩容时机:1.7是超过扩容因子*容量就扩容,1.8是到了就扩容。——都是扩容2倍。
1.8的ConcurrentHashMap中,capacity不再是数组长度,而是假设要放进去的元素数量。
factor扩容因子只在初始化数组时有效,后面还是按照0.75的扩容因子计算是否扩容。
扩容时出现其他线程想要get或者put怎么办?
get看链表头,如果是forwardingNode,证明数据已经迁移走了,应该去新数组找数据,如果不是证明还没有处理,直接就数组继续get就可以。
如果要找的元素在迁徙的头节点后面,链表迁移的时候重新创建节点,不然可能会出现指向改变的情况,也做了优化:如果迁移之后顺序没变就不用动,变了才重新创建对象。
put的三种情况:
对ThreadLocal的理解?
1.实现线程间资源对象的线程隔离,每个线程使用各自的资源对象,避免争抢造成的线程安全问题。(局部变量也可以实现线程隔离,但是不能跨越方法)。
2.实现了线程内的资源共享。
ThreadLocal的原理:
每个线程有自己的ThreadLocalMap的成员变量,用来存储资源对象。
1.调用set():ThreadLocal作为key,资源对象做value,放入当前的ThreadLocalMap集合中。
2.调用get():ThreadLocal作为key,在当前线程中查找关联的资源值。
3.调用remove():ThreadLocal做key,移除当前线程关联的资源值。
冲突会使用开放寻址法,找下一个空闲的位置。
ThreadLocalMap释放时机:不放可能出现内存泄露问题。
1.ThreadLocalMap 的key是弱引用,因为Thread可能需要长时间运行,如果key不使用了的话,内存不足的时候虚拟机的垃圾回收机制可以释放其占用的内存。
2.ThreadLocalMap的value是强引用,会根据key = null ?来释放值的内存。时机有:
2.1 get()获取key发现key = null,会把值也置为null。
2.2 set key时,启发式扫描,会清理附近的null key。元素个数多,扫描的范围就大一些。多发现null key就多扫描几次。
2.3 remove()时:一般使用ThreadLocal都是作为静态变量,因此GC无法回收。静态变量一直对ThreadLocal保持强引用,因此自己手动清理。