JAVA 多线程---面经
线程与进程
提到进程那就要说程序,程序有指令和数据,程序从磁盘加载到内存,cpu获得指令进行执行,其中还会用到各种资源如网络资源,磁盘等。一个程序从磁盘进入内存,就是进程实例的创建。
一个程序可以有多个进程实例,比如浏览器,一个浏览器有网络进程,存储进程,gpu进程,各个标签页也有进程,浏览器插件等,很多进程。
一个程序也可也可能只有一个进程实例,比如网易云音乐,360安全等
一个进程内可以创建多个线程,同个进程内的线程可以共享进程的某些内存,线程的上下文切换消耗资源更少,更轻量。
JVM进程启动会启动哪些线程? - 将王相 - 博客园 (cnblogs.com)
yield
只是让出cpu,从running到就绪状态,如果得到cpu时间片后,继续执行。
sleep,join,wait,park
join:比如在main线程,创建t1线程,那么t1.join需要等待t1中的代码执行完成后,main线程继续向下执行
wait,notify:调用wait方法,线程进入等待池。调用notify则等待池中随机唤醒一个进入锁池。调用notifyall则唤醒等待池中所用线程进入锁池。
sleep和wait比较:
- sleep不释放锁,wait让锁进入等待池
- sleep不需要sychronized来先获得锁
- sleep时Thread类,wait时object类
LockSupport.park() ,也是可以阻塞线程,通过其他线程unpark来继续向下走。底层是通过unsafe类的park方法实现。
park
先执行unpark,后执行park,也是可以的!!
先执行多次unpark,再执行一次park也是可以的,但是再执行一次park,那么就会阻塞了。原因是多次执行unpark仅仅是计数器赋值为1,并不是加1操作。
synchronized
对象头
markword:gc年龄,hash值,锁状态(无锁,偏向锁,轻量级锁,重量级锁)
classpoint:指向该对象所属的类
synchronized关键字,使用反编译可以看到监视器的enter和exit,moniter的实现由虚拟机的c++代码实现,这个类为moniterobject,其中又count,owner(获得了锁的当前线程),entrylist(阻塞中的线程,获取不到锁的线程),waitset(等待的线程,调用wait方法等待)。
锁升级
吊打Java面试官-Java锁升级详解 - 云+社区 - 腾讯云 (tencent.com)
markword记录了锁状态,当一个线程第一次获得这个对象锁时,那么也就从无锁状态进入偏向锁状态
偏向锁:第一次使用CAS操作将线程id放到MarkWord头中,之后再获取对象锁时,如果发现这个线程id是自己的就不用重新CAS。否则CAS竞争,成功则重新设置线程id,失败则升级为轻量级锁。
轻量级锁:将MarkWord通过CAS放到线程的锁记录空间中,然后对象头的Markword指向锁记录地址。如果只有一个线程尝试获取轻量级锁,会进行自旋等待,一旦有两条以及以上的线程抢占该锁,轻量级锁会升级为重量级锁。
锁标志位置为10
,Mark Word存储的就是指向重量级锁的指针
ReentrantLock
- 可重入
- 可打断
- trylock设置超时
- 公平锁或非公平
- condition,这个的实现是一个conditionObject,链表数据结构,每个node节点存储当前等待的线程。
Reentrantlock类比MonitorObject,那么CHL队列就是EntryList,存放阻塞的线程。condition就是waitSet,等待的线程存放到condition中。(对于锁的控制,还以深入,比如设计一个同步器,需要的数据结构,这个数据结构中,要有计数器,阻塞队列,等待队列,当前执行中的线程,等等这些都是一通百通的)
活跃性
- 死锁
- 活锁:比如一个线程对count++,一个线程对count--,这样永远执行下去
- 饥饿锁:由于优先级太低,导致的得不到执行的线程
volatile
- 可见性
- 防止指令重排
Fork/Join
可以用来计算斐波那契,1-n的和。它可以分线程来计算。
ForkJoinPool为线程池。需要实现RecursiveTask或RecursiveAction作为任务
ConcurrentHashMap
BlockingQueue
比如ArrayBlockingQueue,LinkedBlockingQueue原理是通过reentrantlock,condition进行锁控制。
ArrayBlockingQueue
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
如果队列满了,调用notFull.await();
方法,所有调用put方法的线程,都放入notFull这个condition中。也就是说当队列满的时候,当前所有想要put的线程,都将进入notFull这个condition中等待。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
可以看到take,这里的如果队列为空,那么所有想要获得队列的线程,则都进入notEmpty这个condition等待!
lock.lockInterruptibly();
这个方法是,如果当前线程是中断状态,则直接抛出异常。否则去获得锁(也就是lock方法)。
在ArrayBlockingQueue,linkedBlockingQueue,都是通过reentranlock加锁,通过notEmpty,notFull这两个condition来限制队列为空,为满。