您的位置 首页 java

Java多线程面试总结

线程 与进程

进程是程序的一次执行过程,是系统运行程序的基本单位。

Java 中,启动main函数其实就是启动一个JVM进程,main函数所在的线程就是这个进程中的一个线程,也称主线程。

线程是一个比进程更小的执行单位,一个进程在执行期间可以产生多个线程。

多个线程可以共享堆和方法区资源,每个线程有自己的陈序计数器、 虚拟机 栈和本地方法。

程序计数器为啥是每个线程私有的?

字节码解释器通过改变计数器来依次读取指令,从而实现代码的流程控制,如 顺序执行、选择、循环;

多线程 里,计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道线程上次运行到哪了。

如果执行的是native 方法,计数器记录的是undefined地址。只有执行Java代码程序计数器记录的才是地址。

程序计数器主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为啥私有?

虚拟机栈:Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中的入栈到出栈过程。

本地方法栈:和虚拟机类似,执行的Native方法服务。

为保证局部变量不被其他线程所访问。

堆与方法区

堆和方法区是所有线程共享的资源,堆是进程中最大的一块内存,主要存放新创建的对象,方法区用于存放已被加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

并发与并行

并发:指同一个时间段内,多个任务都在执行,单位时间内不一定同时执行;

并行:单位时间内,多个任务同时执行。

线程的生命周期和状态

6种状态:

NEW:初始状态,线程被构建,但还没调用start方法;

RUNNABLE:运行状态,Java线程将操作系统的就绪和运行两种状态称作运行中;

BLOCKED:阻塞状态,表示线程阻塞于锁;

WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程等待其他线程做出一些特定动作,如通知、中断;

TIME_WAITING:超时等待, 在指定时间可自行返回,和WAITING不同;

TERMINATED:终止状态,表示当前线程已经执行完毕;

上下文切换

当前线程在执行完CPU时间片切换到另外一个线程之前会保存自己的状态,以便下次在切换回这个线程时,可以再加载这个线程的状态。线程从保存到再加载的过程就是一次上下文切换。

死锁的4个条件

互斥条件:任意一个时刻只有一个线程占用;

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保存不放;

不剥夺条件:线程已获得的资源在未使用完前,不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;

循环等待条件:若干线程之间形成头尾相接的循环等待资源关系。

sleep()方法与wait()方法区别

sleep没有释放锁,wait释放了锁;

2者都可以暂停线程执行;

wait用于线程间交互通信,sleep用于暂停执行;

wait(不加超时)调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep执行完后,线程会自动苏醒。wait加了超时后线程也会自动苏醒。

线程start()方法与run()方法

new Thread(),线程进入新建状态,调用start()方法会启动一个线程并使线程进入就绪状态,当分配到时间片后可以开始运行了。start会执行线程的相应准备工作,然后自动执行run()方法的内容。

直接调用run()方法,会把run方法当成一个main线程下的普通方法执行,并不会让线程工作。

synchronized 关键字三种使用方法

修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;

修饰静态方法:给当前类加锁,会作用于类的所有对象实例,由于静态成员不属于任何一个实例对象,是类成员。线程A调用实例对象的非静态synchronized方法,而B线程需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥。因为静态synchronized方法占用的锁是当前类的锁,而访问非静态方法synchronized方法占用的锁是当前实例对象锁。

修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized加到static静态方法和synchronized(class)代码块上都是给Class类加锁。synchronized关键字加到实例方法上是给对象实例上锁。

尽量不要用synchronized(String s) 因为jvm中字符串常量池具备缓存功能。

synchronized关键字底层原理

synchronized同步语句块:

javac Syn.java命令生成Syn.class文件;

再执行javap -c -s -v -l Syn.class查看。

同步语句块实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指向同步代码结束位置。

当执行monitorenter时,线程试图获取锁即monitor(monitor对象存在于每个Java对象的对象头中,这也是任意对象都可以作为锁的原因)的持有权。当计数器为0则获取成功,退出执行monitorexit指令,将锁计数器设为0,表明锁被释放。

synchronized修饰方法:

synchronized修饰方法用的ACC_SYNCHRONIZED标识,该标识指明该方法是一个同步方法。

synchronized和ReentrantLock区别(性能已不是衡量标准了)

2者都是可重入锁,自己可以再次获取自己内部的锁,每次获得锁计数器就会加1,每次释放就会减1,当计数器为0锁才会真正释放;

synchronized依赖于JVM而ReentrantLock依赖于API;

ReentrantLock增加了高级功能:

ReentrantLock提供可中断等待锁的线程机制,通过lock.lockInterruptibly()实现;

ReentrantLock可以指定是公平锁还是非公司锁。而synchronized只是非公平锁。所谓公平锁即先到的线程先获取锁;

synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待、通知机制。ReentrantLock借助Condition接口与newCondition()方法。使用notify()/notifyAll()方法进行通知时,被通知的线程由JVM选择,而ReentrantLock类结合Condition实例可以实现“选择性通知”

volatile 关键字

主要作用就是保证变量可见性及防止指令重排序。

可见性:

当前处理器缓存行数据会回写到系统内存;

这个回写操作会引起其他CPU里的缓存了该内存地址的数据无效(其他CPU通过缓存嗅探来使缓存失效)。

防止重排:

每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;

每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

ThreadLocal

ThreadLocal内部维护一个类型Map的ThreadLocalMap数据结构,key为当前对象的Thread对象,值为Object对象。

ThreadLocal 内存泄漏 问题:ThreadLocalMap中使用的key为ThreadLocal弱引用,而value为强引用。若ThreadLocal没有被外部强引用,垃圾回收时,key会被清理掉,而value不会被清理。如果不采取措施value永远无法被回收,造成内存泄漏。ThreadLocalMap中调用set、get、remove方法时,会清理掉key为null的记录。使用完ThreadLocal方法后,最好手动调用remove()方法。

线程池

使用线程池好处:

降低资源消耗;

提高响应速度;

提高线程的可管理性;

Executors 返回线程池对象的弊端:
FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的 线程数 量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。

ThreadPoolExecutor 重要参数分析:

corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量;

maximumPoolSize:当队列中存放任务达到队列容量时,当前可以同时运行的线程最大数量;

workQueue:当新任务来的时候,判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务会被放在队列中。

keepAliveTime:当线程池中线程数量大于corePoolSize时,如果此时没新任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTIme才会被回收。

threadFactory:创建新线程时用。

ThreadPoolExecutor饱和策略:

AbortPolicy:抛出异常拒绝新任务;

CallerRunsPolicy:执行任务。

DiscardPolicy:不处理新任务,直接丢弃掉。

DiscardOldestPolicy:此策略将丢弃最早未处理的任务请求。

ThreadPoolExecutor执行顺序:

当线程数小于核心线程数,创建线程;

当线程大于等于核心线程数,且任务队列未满时,将任务放入任务队列;

当线程大于等于核心线程数,且任务队列已满,若线程数小于最大线程数,创建线程;若线程数等于最大线程,拒绝执行任务。

ThreadPoolExecutor默认值
* corePoolSize=1
* queueCapacity=Integer.MAX_VALUE
* maxPoolSize=Integer.MAX_VALUE
* keepAliveTime=60s
* allowCoreThreadTimeout=false
* rejectedExecutionHandler=AbortPolicy()

如何来设置
* 需要根据几个值来决定
– tasks :每秒的任务数,假设为500~1000
– taskcost:每个任务花费时间,假设为0.1s
– responsetime:系统允许容忍的最大响应时间,假设为1s
* 做几个计算
– corePoolSize = 每秒需要多少个线程处理?
* threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
* 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
– queueCapacity = (coreSizePool/taskcost)*responsetime
* 计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
* 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
– maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
* 计算可得 maxPoolSize = (1000-80)/10 = 92
* (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
– rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
– keepAliveTime和allowCoreThreadTimeout采用默认通常能满足

AtomicInteger原理

主要利用CAS(compare and swap)+volatile 和 native方法来保证原子操作,提升执行效率。

AQS(AbstractQueuedSynchronizer)

AQS是一个用了构建锁和同步器的框架。

AQS核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效工作线程,并且将共享资源设置为状态锁定。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制。将暂时获取不了锁的线程加入到队列中。

AQS使用一个volatile 修饰的int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。

AQS组件:

Semaphore(信号量):允许多个线程同时访问。

CountDownLatch(倒计时器):用来控制线程等待,让一个线程等待直到倒计时结束。

CyclicBarrier(循环栅栏):和CounDownLatch类似,但是他可以循环。

文章来源:智云一二三科技

文章标题:Java多线程面试总结

文章地址:https://www.zhihuclub.com/185800.shtml

关于作者: 智云科技

热门文章

网站地图