目录
- 进程和线程概念,区别?
- Thread中的start和run方法区别?
- Thread和Runnable区别?
- 如何实现处理线程的返回值?
- 线程的六个状态?
- sleep和wait的区别?
- notify与notifyAll区别 ?
- yield方法介绍?
- interrupt如何中断线程?
- 线程状态转换
- synchronized使用?
- synchronized基础(Java对象头和Monitor)?
- 自旋锁
- Java6以后synchronized锁优化?
- synchronized的四种状态(随着线程竞争情况逐渐升级)
- ReentrantLock(再入锁)
- ReentranLock公平性设置
- ReentranLock与synchronized区别?
- Java内存模型(JMM)
- JMM中主内存和工作内存数据存储类型以及操作方式
- volatile(JVM提供的轻量级同步机制)
- volatile如何实现立即可见?
- volatile如何禁止指令重排?
- volatile与synchronized区别?
- CAS解释
- 线程池(Executors)
- 线程池创建方法
- 线程池启动方式
- 为什么使用线程池(线程池优点)?
- 线程池运行流程
- 线程池拒绝策略处理(ThreadPoolExecutor构造函数参数handler)
- 线程池状态
- 线程池大小如何确定?
进程和线程概念,区别?
每一个程序就是一个进程,进程独占内存空间,保存各自的运行状态,相互间互不干扰且可以进行切换,为并发处理任务提供了可能;(单核时只能有一个进程在执行);
一个进程可以包括多个线程(至少包含一个线程),线程是进程的执行单元;
区别:
- 进程是资源分配的最小单位,线程是CPU调度的最小单位;
- 进程有独立的地址空间,互不影响;线程没有独立的地址空间;
- 进程切换的开销比线程切换开销大;
Tips:Java采用单线程模型,JVM虚拟机是多线程的(例如垃圾收集器线程等等);
Thread中的start和run方法区别?
区别:
start()方法源码中会调用一个native的start0()方法(.cpp),会调用JVM_StartThread来创建并启动子线程;
执行start()方法会创建一个新的子线程并启动;执行run()方法只是Thread的一个普通方法调用;
Thread和Runnable区别?
区别:
-
Thread是一个实现了Runnable接口的类并且实现了run方法;
Thread的构造方法参数:Runnable实现类实例,FutrueTask实例
-
由于Java是单继承的,推荐使用Runnable接口;
如何实现处理线程的返回值?
-
主线程等待法,主线程调用sleep方法(让主线程去循环等待子线程结束并赋值;缺点:代码臃肿,等待时间无法精准控制;);
-
使用Thread类的join()阻塞当前线程以等待子线程处理结束(依旧不是很精准);
-
通过Callable接口实现call()获取线程返回值(通过FutrueTask Or 线程池获取)
FutrueTask的构造方法可以传入Callable实现类的实例;
isDone()可以判断call是否执行结束;
get()用来获取线程完成后的返回值;
线程的六个状态?
-
新建(new):创建线程后尚未启动的线程(未调用start方法)
-
运行(Runnable):包含Running(调用start方法后)和Ready(位于线程池中,等待获得cpu资源后就会变为Running)
-
无限期等待(Waiting):不会被分配CPU执行时间,需要显式唤醒;(调用没有设置timeout参数的Object.wait方法;调用没有设置timeout参数的Thread.join方法;LockSupport.park方法;唤醒需要notify或者notifyAll)
-
限期等待(Timed Waiting):在一定时间后会有系统自动唤醒;
(调用Thred.sleep方法;调用设置timeout参数的Object.wait方法;调用设置timeout参数的Thread.join方法;LockSupport.parkNanos方法;LockSupport.parkUntil方法)
-
阻塞状态(Blocked):等待锁(进入Synchrionized代码块或者方法)
-
结束(Terminated):已终止的线程状态,线程已经执行结束(终止后不能复活)
sleep和wait的区别?
-
sleep是Thread类的方法;wait是Object的方法;
-
sleep方法可以在任何地方使用;wait只能在synchronized代码块或者synchronized方法中使用(是因为只有获取到锁才能释放锁);
-
sleep只会让出cpu,不会释放锁;wait会让出cpu,还会释放锁;
notify与notifyAll区别 ?
- notifyAll会唤醒所有在等待池中的线程并添加到锁池中去竞争锁;(使用wait方法后的线程会释放锁进入等待池,不会去竞争锁;如果使用notifyAll方法后,等待池中的线程会重新进入锁池;当线程A获得锁,其他线程就会进入锁池去等待锁释放进行竞争;)
- notify会随机唤醒一个等待池中的线程添加到锁池中去竞争锁;
yield方法介绍?
- 当调用Thread.yield函数时,会给线程调度器一个暗示:当前线程愿意让出CPU使用,但是线程调度器有可能会忽略;
- yield不会对锁行为有影响(不会释放锁);
interrupt如何中断线程?
stop()、suspend()、resume()已经被抛弃,突然停止线程并且释放锁;
目前使用的方法是interrupt(),通知线程应该中断了;
- 如果线程是被阻塞状态,那么线程将直接退出被阻塞状态,并且抛出InterruptedException异常;
- 如果线程是正常运行状态,那么会将该线程的中断标记设置为true,该线程将继续正常运行,也不会突然停止;
也就是说interrupt不能真正的中断线程,需要被调用的线程配合完成中断;
- 在线程正常运行任务时,经常去查看本线程的中断标志位,如果中断标志位为true,就应该自行停止线程;
- 如果线程处于正常活动状态,那么线程中断标记被设置为true,该线程会继续正常运行;
线程状态转换
synchronized使用?
特性:互斥性(同一时间只能允许一个线程操作锁)和可见性(在锁被释放前对共享变量所做的操作是对随后获得该锁的线程是可见的);
分类:对象锁与类锁(类锁和对象锁是不干扰的)
-
对象锁(同一个对象的同步代码块和同步方法是互斥的)
1. synchronized(this){ //同步代码块 } 2. synchronized method(){ //同步方法体 }
-
类锁
1. synchronized(类.class){ //同步代码块 } 2. synchronized static method(){ //同步方法体 }
synchronized基础(Java对象头和Monitor)?
-
synchronized的锁对象是存储在对象头中的,由MarkWord(存储对象的运行时数据)和ClassMetadataAddress(jvm通过该指针确定这个对象是哪个类的数据)组成;
-
Monitor(C++实现)
底层ObjectMonitor.hpp:
其中会有Entrylist(锁池)、Waitlist(等待池)、Owner和Count,如果有多个线程访问同步代码时,会先进入Entrylist,线程获得Monitor锁后会进入Owner区域中,将Owner设置为当前线程(初始为null),Count+1;如果线程调用wait方法 ,将会进入Waitlist中,Owner恢复为null,Count-1;
Tips:早期的synchronized锁属于重量级锁,性能较低,线程切换需要依赖底层操作系统,需要从用户态转换到核心态,开销太大;Java6后对synchronized锁进行优化;
自旋锁
- 很多情况下,共享数据的锁定状态持续时间较短,切换线程不值得;
- 通过让线程执行循环来等待锁的释放,不会让出cpu;
- 缺点:如果其他线程占用锁时间较长,会带来许多额外的性能开销;
Java6以后synchronized锁优化?
-
自适应自旋锁
- 自旋次数不再固定;
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定(以上次自旋结果预测是否能获得锁);
-
锁消除
当jvm编译时,对运行上下文进行扫描,去掉不可能存在竞争的锁;
//StringBuffer是线程安全的,以下sb只会在append方法中使用,不会被其他线程调用 //jvm会自动清除内部的synchronized锁 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2);
-
锁粗化
通过扩大加锁的范围,避免反复加锁和解锁;
StringBuffer sb = new StringBuffer(); while(i<100){ //连续的append会多次加锁和解锁 //jvm会执行锁粗化,将synchronized范围扩大至只需要加一次锁 sb.apped(i); }
-
偏向锁(减少同一线程获取锁的代价,适合只有一个线程访问同步块或同步方法)
如果一个线程获得了锁,那么锁就进入偏向模式,MarkWord会将该锁标记为偏向锁;当再次请求该锁时,无需再做任何同步操作,只需要检查MarkWord中标记是否为偏向锁,以及当前线程ID是否等于MarkWord中的ThreadID,省去了CAS加锁和解锁操作。
缺点:不适用于锁竞争比较激烈的多线程场合;
-
轻量级锁(适用于线程交替执行同步块或同步方法)
是偏向锁升级来的,使用自旋操作,竞争的线程不会阻塞,提高响应速度;
缺点:若长时间得不到锁,自旋会消耗CPU性能;
-
重量级锁(适用于追求吞吐量,同步块或同步方法使用执行时间较长)
线程竞争不会使用自旋,不会消耗CPU;
缺点:线程阻塞,响应时间慢,多线程下频繁加锁解锁,性能消耗巨大;
synchronized的四种状态(随着线程竞争情况逐渐升级)
锁膨胀方向(锁升级):无锁—>偏向锁—>轻量级锁—>重量级锁
ReentrantLock(再入锁)
-
位于JUC包下;和FutureTask都是基于AQS(队列同步器)实现的;
-
可以实现比synchronized更细粒度的控制;例如fairness
-
调用lock()之后,必须调用unlock();(try调用lock(),finally调用unlock();)
-
与synchronized一样是可重入的,当一个线程获取已经获取的锁时,就会自动获取成功;
ReentranLock公平性设置
ReentranLock fairLock = new ReentranLock(true);
参数为true时,倾向于将锁赋予等待时间最久的线程;
公平锁:获取锁的顺序安先后调用lock方法的顺序;
非公平锁:线程抢占顺序不一定;例如synchronized是非公平锁;
ReentranLock与synchronized区别?
- synchronized是关键字;ReentranLock是类;
- ReentranLock可以对获取锁的等待时间进行设置,避免死锁;
- ReentranLock可以获取各种锁的信息;
- ReentranLock可以灵活地实现多路通知;
- synchronized操作的是MarkWord,lock调用的是Unsafe类的park方法;
Java内存模型(JMM)
本身是抽象概念,并不是真实存在的,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量的访问方式。
主内存:
- 存储Java实例对象;
- 成员变量、类信息、常量、静态变量等;
- 属于数据共享的区域,多线程并发操作时会引发线程安全问题;
工作内存:
- 存储当前方法的所有本地变量信息,本地变量对其他线程不可见;
- 字节码行号指示器,Native方法信息;
- 属于线程私有数据区域,不存在线程安全问题;
JMM中主内存和工作内存数据存储类型以及操作方式
- 方法里的基本数据类型将直接存储在工作内存的栈帧结构中;
- 引用类型的本地变量:引用存储在工作内存中;实例存储在主内存中;
- 成员变量、static变量、类信息均会存储在主内存中;
- 主内存共享方式就是线程各自拷贝一份数据到工作内存,操作完成后刷新回主内存;
volatile(JVM提供的轻量级同步机制)
- 保证被volatile修饰的共享变量对所有线程总是可见的;
- 禁止指令重排;
- 保证可见性,不保证原子性;
- 性能略高于synchronized;
volatile如何实现立即可见?
当写一个volatile变量时,JMM会把这个线程对应的工作内存刷新到主内存中;
当读一个volatile变量时,JMM会把该线程的工作内存无效化,只能从主内存重新获取最新的值;
volatile如何禁止指令重排?
内存屏障(CPU指令)
- 保证特定操作的执行顺序;
- 保证某些变量的内存可见性;
通过插入内存屏障指令,禁止对内存屏障前后的指令进行重排序优化;
强制刷出各种CPU的缓存数据,因此CPU缓存也能读取到数据的最新版本,保证可见性;
volatile与synchronized区别?
- volatile本质是告诉JVM当前变量在工作内存中是不准确的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问,其他线程进入锁池等待;
- volatile只能使用在变量级别;synchronized可以使用在变量、方法、代码块以及类级别;
- volatile保证变量的修改可见性,不保证原子性;synchronized可以保证变量的修改可见性和原子性;
- volatile不会造成线程阻塞;synchronized可能会造成线程阻塞;
- volatile标记的变量不会被编译器优化;synchronized可能会被编译器优化;
CAS解释
操作流程:先去主内存拿到要操作的数据到工作内存,经过计算后,刷新回主内存;
-
支持原子更新操作,适用于计数器,序列发生器等场景;
-
乐观锁机制;
-
CAS操作失败时可以决定是继续尝试,还是执行别的操作;
缺点: -
若循环时间长,则开销很大;
-
只能保证一个共享变量的原子操作;
-
ABA问题;(如果一个数据从A->B->A,CAS则认为从来没有改变过;解决:JUC下的AtomicStampedReference);
synchronized,ReentranLock是悲观锁机制(始终假定会发生并发冲突),CAS(Compare and Swap)是乐观锁机制(始终假定不会发生并发冲突);
线程池(Executors)
-
newFixedThreadPool(int nThreads)
指定工作线程数量的线程池
-
newCacheThreadPool()
创建一个具有缓存功能的线程池
- 创建线程数量默认为Integer.MAX_VALUE;
- 如果线程空闲超过指定时间,则会被终止并移出缓存;
- 系统长时间闲置,不会消耗太多资源;
-
newSingleThreadExecutor()
创建一个单线程工作的线程池
- 如果线程异常结束,会有另外一个线程来取代它;
- 保证每个线程的顺序执行;
- 同一时间内不会有多个活动线程;
-
newScheduledThreadPool(int corePoolSize)
创建一个周期性执行线程的线程池,传入核心线程数量;
-
newSingleThreadScheduledExecutor()
创建一个周期性执行线程的单线程线程池;
-
newWorkStealingPool()(Java8加入)
内部会构建ForkJoinPool,利用working-stealing算法(如果有线程已经执行结束,为了提高利用率,将会窃取其他等待线程来执行;),可以并行处理任务,不保证处理顺序;
-
使用ThreadPoolExecutor构造方法创建自定义线程池
线程池创建方法
//指定线程数量的线程池
ExecutorService pool1 = Executors.newFixedThreadPool(1);
//缓存线程池
ExecutorService pool2 = Executors.newCachedThreadPool();
//单线程线程池
ExecutorService pool3 = Executors.newSingleThreadExecutor();
//周期性线程池
ExecutorService pool4 = Executors.newScheduledThreadPool(1);
//周期性单线程线程池
ExecutorService pool5 = Executors.newSingleThreadScheduledExecutor();
//Fork/Join线程池
ExecutorService pool6 = Executors.newWorkStealingPool();
//自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
3, 6, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
线程池启动方式
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//创建线程
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("创建线程");
}
};
//提交线程并启动
executorService.submit(runnable);
//启动线程
executorService.execute(runnable);
为什么使用线程池(线程池优点)?
- 降低资源消耗(重复利用创建的线程,避免重复创建和销毁线程的消耗);
- 提高线程的可管理性(统一分配,调优等);
- 提高响应速度(任务不需要等待线程创建就可以执行);
线程池运行流程
当提交线程之后,线程会进入工作队列WorkQueue队列,然后提交给内部线程池创建Worker开始工作,Worker管理线程的创建和销毁;
线程池拒绝策略处理(ThreadPoolExecutor构造函数参数handler)
线程池状态
- RUNNING:能接受新提交的任务,并且可以处理队列中的任务;
- SHUTDOWN:不再接受新任务提交,可以继续处理剩余的任务;
- STOP:不再接受新任务提交,也不出来剩余任务;
- TIDYING:所有的任务都已经终止;
- TERMINATED:terminated()方法执行完后进入该状态,消亡;
线程池大小如何确定?
CPU密集型:线程数=CPU核数+1;
I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间);