常见面试题
1、线程状态。
2、synchronized原理
3、对象头包含哪些内容
4、CAS原理
5、讲讲aqs
6、volatile原理
7、讲讲ThreadLocal
8、CountDownLatch了解吗
9、线程池
1、线程状态
线程状态 | 解释 |
---|---|
NEW | 尚未启动的线程状态,即线程创建,还未调用start方法 |
RUNNABLE | 就绪状态(调用start,等待调度)+正在运行 |
BLOCKED | 等待监视器锁时,陷入阻塞状态 |
WAITING | 等待状态的线程正在等待另一线程执行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待时间的等待状态 |
TERMINATED | 线程完成执行,终止状态 |
常用方法
wait()和sleep():
- wait属于TIMED_WATING,自动被唤醒。属于Object类的方法。wait属于WATING,需要手动唤醒
- sleep()只会让出CPU,不会导致锁行为的改变。Object.wait()不仅让出CPU,还会释放锁资源
- sleep()可以在持有锁或者不持有锁的时候执行。wait()必须在持有锁的时候才可以执行
PS:为什么wait方法必须在持有锁的时候才能执行?
wait方法会将持有锁的线程从owner扔到waitSet集合中,这个操作实在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。
并发编程的三大特性
一、原子性
原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
如何保证:
1、synchronized
2、CAS
在CPU层面保障的一个原子性,
3、Lock锁
二、可见性
可见性是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主存中获取数据的话就太慢了,CPU就提供了L123的三级缓存,每次去主存中拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存中拿数据。
如何保证:
1、volatile
如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主存操作
当写一个volatile变量时,JMM会把该线程对应的CPU缓存及时刷新到主内存找那个
当度一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主存中重新读取共享变量
2、synchronized
synchronized也是可以解决可见性问题的。
如果涉及到了synchronized的同步代码块或者同步方法,获取锁资源之后,会将内部涉及到的变量从CPU缓存中移除,必须去主存中重新拿数据,并且释放锁之后,会立即将CPU缓存中的数据同步到主内存中
3、lock
lock锁是基于volatile实现的,lock锁内部在进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
4、final
final修饰的属性,被final修饰不能进行修改,就不需要保证可见性
三、有序性
.java文件中的内容会被编译,在执行前需要转化为CPU可以识别的指令,CPU在执行这些指令时,为了提升效率,在不影响结果的前提下,会对指令进行重排
如何保证有序性
1、as-if-serial
2、happens-before
3、volatile
2、synchronized原理
使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁实现,他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
monitorenter和monitorexit会让对象在执行时,使得其锁计数器加1或者减1.每个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的monitor锁的所有权的时候,monitorenter指令会发生如下三种情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
- 这把锁已经被别的线程获取了,等待锁释放
monitorexit:
释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
内存语义上来讲,加锁的过程会清楚工作内存中的共享变量,再从主存读取,而释放锁的过程是将工作内存中的共享变量写回主内存。
3、对象头包含哪些内容
4、CAS原理
简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。CAS操作是原子性的,但他只能保证一个共享变量的原子性操作,并且循环时间开销大,而且cas有个众所周知的问题就是ABA。
cas缺点:多线程情况下,如果都用cas的话,都相对占用部分CPU资源
cas的问题:ABA。通过版本号的方式解决
5、讲讲aqs
什么是aqs?
AbstractQueuedSynchronized抽象类,是JUC包下的一个基础类。
aqs核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制aqs是用clh队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
aqs的组成部分是:
提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次aqs种维护了一个双向链表,有head,有tail,并且每个节点都是node对象。
6、volatile原理
作用:
1、防止指令重排
2、实现可见性
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,便只能去主内存中读取该变量。
7、讲讲ThreadLocal
ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key, value键值对的能力。
弱引⽤的⽬的是为了防⽌内存泄露,如果是强引⽤那么ThreadLocal对象除⾮线程结束否则始终⽆法被回收,弱引⽤则会在下⼀次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除⾮线程结束运⾏。
只要ThreadLocal使⽤恰当,在使⽤完之后调⽤remove⽅法删除Entry对象,实际上是不会出现这个问题。
8、CountDownLatch了解吗
CountDownLatch底层也是由AQS,用来同步一个或多个任务的常用并发工具类。
1、通过构造函数初始化传入参数实际为aqs的state变量赋值,维持计数器倒数状态。
2、当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入aqs阻塞队列等待。
3、其他线程调用countDown()时,state值原子性递减,当state值为0的时候,唤醒所有调用await方法阻塞的线程。