1.进程与线程区别与联系
进程是OS中资源分配的最小单元
线程是OS中任务分配的最小单元
创建与销毁一个线程的开销要比一个进程小得多,线程间通信也比进程间通信容易的多。
线程间通信:join()、wait/notify、yield、sleep
2.多线程常用操作方法
sleep:运行 -> 阻塞,当前线程立即交出CPU,进入阻塞态,不会释放对象锁
yield:运行 -> 就绪,系统调度交出CPU,进入就绪态,不会释放对象锁,只会让相同优先级的线程获取CPU
join:运行 -> 阻塞,进入阻塞态,会释放对象锁。(join内部就是wait方法)
wait:运行 -> 阻塞,会释放对象锁,必须与synchronized搭配使用
notify:阻塞 -> 就绪,必须在同步方法或同步代码块中使用
创建 就绪 运行 终止
阻塞
3.线程同步
多个线程以下三个特性(原子性、可见性、有序性)任意一个不满足,都存在线程安全问题
3.1 synchronized实现线程安全
同步代码块
synchroinzed(锁的对象) {}
-普通对象(对象锁)
-类.class
同步方法
-成员同步方法 锁的是当前对象this
-静态同步方法 锁的是当前类.class
synchronized底层实现(对象的Monitor机制)
任意一个对象都有Monitor,synchronized对象锁实际上就是获取该对象的Monitor。
当前线程要想获取到该锁的Monitor的流程:
先判断锁对象Monitor计数器值是否为0,
为0表示此时Monitor还未被任何线程持有,当前线程获取Monitor,并且将持有线程置为自己,将Monitor的值+1.
不为0表示此时Monitor已经被线程持有,判断持有线程是否是当前线程,若是,Monitor值再次+1;
若持有线程不是当前线程,线程进入阻塞态,直到Monitor的值减为0.
可重入锁:持有锁的线程再次获取锁
JDK1.6之后关于synchronized优化
偏向 -> 轻量级锁 -> 重量级锁(自适应自旋)
锁粗化
锁消除
4.Synchronized与ReentrantLock区别
4.1
synchronized底层实现:
monitorenter
monitorexit
JDK1.6关于synchronized优化
CAS(Compare And Swap)
CAS(O,V,N) O:当前线程认为主内存的值 V:主内存中的实际值 N:希望更新的值
自旋:处理器上跑无用指令,但是线程不阻塞。
自适应自旋:重量级锁的优化
JVM给一个时间段,在该时间段内,线程是自旋状态,若在该时间段内获取到锁,下一次适当延长自旋时间;
否则将线程阻塞,下一次适当缩短自旋时间。
随着锁竞争的激烈程度不断升级,没有降级过程。
偏向锁 -> 轻量级锁 -> 重量级锁(JDk1.6之前synchronized默认实现)-线程获取锁失败进入阻塞态(OS 用户态 -> 内核态)
JDK1.6 默认先偏向锁
偏向锁(乐观锁,锁一直是一个线程来回的获取):
当线程第一次获取锁时,将偏向锁线程置为当前线程,以后再次获取锁时,不再有加锁与解锁过程,只是简单判断
下获取锁线程是否是当前线程。
轻量级锁:在不同时间段内有不同的线程尝试获取锁
每次锁的获取都需要加锁与解锁过程。
重量级锁:在同一时刻有不同线程尝试获取锁
锁粗化
将多次连续的加减锁过程粗化为一次大的加减锁过程
public class BigLock {
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
{
sb.append("hello");
sb.append("world");
sb.append("test");
}
}
}
锁消除
在没有多线程访问的场景下,将锁直接消除。
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("world");
sb.append("test");
}
死锁
synchronized
死锁的产生原因:以下四个条件同时满足才会产生死锁
1.互斥:共享资源X,Y只能被一个线程占用
2.占有且等待:线程1已经取得共享资源X,同时在等待资源Y,并且不释放X
3.不可抢占:其他线程无法抢占线程1已经占用的资源X
4.循环等待:线程1等待线程2的资源,线程2等待线程1的资源
死锁的现象:程序出现"假死"现象
死锁的解决:破坏任意一个条件
JDK1.5 引入Lock体系来优雅的解决死锁问题
1.Lock的使用格式
try {
lock.lock();
}catch(Exception e){
}finally {
lock.unlock();
}
2.Lock接口的重要方法:
响应中断
2.1 void lockInterruptibly() throws InterruptedException;
非阻塞式获取锁,若获取锁失败,线程继续执行,不再阻塞
2.2 boolean tryLock();
支持超时,获取锁失败的线程等待一段时间后若还获取到锁,线程退出
2.3 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
2.4Lock的常用子类
ReentrantLock:可重入锁(Lock接口中常用的子类,语义与synchronized基本一致,也是独占锁的实现)
同步队列:所有获取锁失败的线程进入同步队列排队获取锁
等待队列:调用wait的线程置入等待队列,等待被唤醒(notify)
面试题:synchronized与ReentrantLock的关系与区别?
1.synchronized与ReentrantLock都属于独占锁的实现,都支持可重入.
2.区别:
a.synchronized是关键字,JVM层面的实现;ReentrantLock是Java语言层面的实现
b.ReentrantLock具备一些synchronized不具备的特性,如响应中断、支持超时、支持非阻塞式的获取锁,
可以实现公平锁(默认非公平锁).
c.synchronized只有一个等待队列,而lock调用newCondition()产生多个等待队列
Conditidon : Lock
awiat/signal
变种面试题:synchronized与Lock的关系与区别
1.synchronized与ReentrantLock都属于独占锁的实现,都支持可重入.
2.区别:
a.synchronized是关键字,JVM层面的实现;ReentrantLock是Java语言层面的实现
b.ReentrantLock具备一些synchronized不具备的特性,如响应中断、支持超时、支持非阻塞式的获取锁,
可以实现公平锁(默认非公平锁),可以实现读写锁.
c.synchronized只有一个等待队列,而lock调用newCondition()产生多个等待队列
作业:使用Lock+Condition实现多线程生产-消费者模型(周天)
读写锁
ReentrantReadWriteLock:可重入读写锁
juc关于多线程
juc:Lock 1.5
tryLock() : 非阻塞式获取锁
lockInterruptily() : 响应中断
tryLock(long time,TimeUnit) : 支持超时
synchronized与ReentrantLock区别:
1.
synchronized与Lock的区别:
1.synchronized是JVM层面,关键字;Lock是Java语言层面实现的"管程".
2.Lock具备了一些synchronized不具备的特性,如...,支持公平锁,支持多个等待队列,
还支持读写锁
读写锁:读者写者问题(美团)
读线程:读读异步,读写同步
写线程:写写同步
读写锁实现:ReentrantReadWriteLock(实现缓存HashMap + ReentrantReadWriteLock)
读锁:ReadLock,多个线程在同一时刻可以共同取得该读锁
写锁:WriteLock,独占锁,多个线程在同一时刻只有一个线程可以取得该锁
共享锁:多个线程可以同时取得该锁 读锁 ReadLock 共享锁 == 无锁?
当写线程开始工作,所有其他线程(包含读线程)全部进入阻塞态.
JDK1.8 StampedLock 更加乐观的锁实现,性能比ReentrantReadWriteLock还高.
juc包下工具类:CAS+Lock
1.闭锁CountDownLatch
public CountDownLatch(int count) : count表示需要等待的线程个数
public void countDown() : 计数器值-1(类似运动员线程)
public void await() throws InterruptedException : 等待线程调用该方法进入阻塞态,直到计数器减为0.
CountDownLatch对象在计数器值减为0时不可恢复。
只会阻塞调用await方法的线程
2.循环栅栏CyclicBarrier
public CyclicBarrier(int parties) : parties表示需要有多少个线程同时暂停以及恢复执行
public int await() : cyclicBarrier 计数器-1,当减为0时,所有阻塞线程同时恢复执行
public CyclicBarrier(int parties, Runnable barrierAction)
多个线程在恢复执行之前,任意挑选一个线程执行barrierAction任务后,再同时恢复执行。
CyclicBarrier计时器值可以恢复reset(),CyclicBarrier的对象可以重复使用。
3.Exchanger交换器
Exchanger用于两个线程交换数据,当缓冲区只有一个线程时,该线程会阻塞直到配对成功再交换数据恢复执行。
4.Semaphore信号量
8工人 5台设备
public Semaphore(int permits) : 表示许可的数量
public Semaphore(int permits, boolean fair) : 等待时间最长的线程最先获取到许可
public void acquire() : 申请许可,尝试获取许可
public void release() : 释放许可。线程池
ExecutorService:普通调度池
void execute(Runnable r)
Future submit(Callable || Runnable)
ScheduledExecutorService:定时调度池
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
ThreadPoolExecutor:线程池核心类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
常见阻塞队列:
LinkedBlockingQueue:基于链表的无界阻塞队列
-内置的固定大小线程池就采用此队列
SynchronousQueue:不存储元素的无界阻塞队列
-内置的缓冲线程池就采用此队列
Executors:线程池的工具类
-单线程池
newSingleThreadExecutor()
-固定大小线程池(系统资源紧张,适用于负载较重的服务器)
newFixedThreadPool(int nThreads)
-缓存池(服务器负载较轻,适用于处理很多短期异步小任务
当提交任务速度>>任务处理速度,不断产生新线程;任务处理速度>提交任务速度,只有一个线程)
newCachedThreadPool()
-定时调度池(需要执行定时任务场合)
newScheduledThreadPool(int corePoolSize)
核心池
最大线程池
阻塞队列
拒绝策略
线程池:为何推荐使用线程池来新建线程
1.线程池的工作流程
2.如何自定义线程池
-核心线程池类ThreadPoolExecutor参数配置
-线程池工作线程Worker,如何实现
3.在何种场景下选用何种线程池
JMM:Java内存模型(关于并发程序的内存模型-逻辑模型)
1.JMM的工作流程
工作内存:每个线程创建时分配的空间,线程私有.所有变量的读写均在工作内存中进行。
主内存:所有线程共享的内存区域,存放所有共享变量(类的实例变量、静态变量、常量)。
2.JMM三大特性
只有以下三个特性同时满足,才是线程安全的代码。
原子性:基本数据类型的访问读写都属于原子性操作.
若需要更大范围的原子性,需要使用synchronized或lock来保证
可见性:任意一个线程修改了共享变量的值,其他线程能够立即得知此修改
synchronized、volatile、final实现可见性
有序性:逻辑上写在前面的代码优先发生于写在后面的代码。
3.volatile变量的特殊规则
3.1 可见性
volatile boolean shutdownRequested;
// 线程1
public void shutdown() {
shutdownRequested = true;
}
// 线程2
public void work() {
while(!shutdownRequested) {
}
}
3.2 禁止指令重排