文章目录
多线程
线程池满了如何处理额外的请求
可以使用等待队列实现,或者换用无界队列实现的线程池
同一个对象的两个同步方法能否被两个线程同时调用
不能,因为非静态同步方法,使用的是对象锁,所以同一个对象锁,同一时刻只能有一个线程获得
为什么要用线程池
为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多;在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”;线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足
悲观锁和乐观锁的区别,怎么实现
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题
数据库中的悲观锁一般都是依靠本身提供的锁机制,而乐观锁一般都是基于数据库版本(version)记录机制实现的。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据
java 中的悲观锁是通过线程状态切换实现的,当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notify(),notifyAll()唤醒回来。在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度。独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。进程挂起和恢复执行过程中存在着很大的开销,所以悲观锁时间代价就会非常的高的。乐观锁思想是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直while循环,如果失败就重试,直到成功为止。CAS就是一种乐观锁思想的应用
悲观锁适合写入操作比较频繁的场景。如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量;乐观锁比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量
什么是线程死锁,死锁如何产生,如何避免线程死锁
当线程A持有独占锁a并尝试获取独占锁b,而线程B持有独占锁b并尝试获取独占锁a的情况下,就会发生,A,B线程互相阻塞等待,称之为死锁。
死锁产生的原因:
两个锁需是互斥的,也就是独占锁
线程尝试获取新锁的时候并不会释放已持有的锁
循环等待,即若干线程之间形成一种头尾相接的循环等待资源关系
如何避免死锁,我们需要避免了逻辑中出现多个线程互相持有对方线程所需要的独占锁的的情况,就可以避免死锁。
线程池用过吗,都有什么参数,底层如何实现的
线程池ThreadPoolExecutor 包含7个参数,如下:
corePoolSize : 线程池保持的核心线程数量,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
maximummPoolSIze: 最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize; 当阻塞队列是无界队列, 则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue
keepAliveTime: 当线程数大于核心线程数时,空闲线程的等待时间,超时则终止线程,默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时, 如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时keepAliveTime参数也会起作用, 直到线程池中的线程数为0
unit: 空闲线程等待的时间单位
workQueue:存储线程的工作队列,当提交的线程说大于最大线程数时,不能进入执行的线程则放入工作队列中等待。一般使用ArrayBolckingQueue、LinkedBlockingQueue、SynchronousQueue.
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene; 是无界的,可以不指定队列的大小,但是默认Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的。
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene; 如果应用程序确实需要比较大的工作队列容量, 而又想避免无界工作队列可能导致的问题,不妨考虑SynchronousQueue。SynchronousQueue实现上并不使用缓存空间。 使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务
DelayedWorkQueue:延迟队列
threadFactory: 创建线程所使用的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名
handler:表示当拒绝处理任务时的策略,线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
线程池的实现原理:
线程提交之后会被包装成Work执行,Work 是继承了线程同步器AQS可以安全的进行线程状态的切换,其次AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态。这样可以维持线程池的正常运行。
详谈Java四种线程池及new Thread的弊端,说说几种常见的线程池及使用场景
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 适用于执行很多短期异步的小程序或者负载较轻的服务器
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。 如Runtime.getRuntime().availableProcessors()。 适用于执行长期的任务,性能好很多
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 适用于周期性执行任务的场景
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,适用于一个任务一个任务执行的场景
多线程同步锁
synchronized原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。synchronized 是可重入的
对动态方法的修饰,作用的是调用该方法的对象(或者说对象引用)
对代码块的修饰,作用的是调用该方法的对象(或者说对象引用)
对静态方法的修饰,作用的是静态方法所在类的所有对象(或者说对象引用)
对类的修饰,作用的是静态方法所在类的所有对象(或者说对象引用)
Lock :常用的是ReentrantLock 和 ReentrantReadWriteLock即重入锁和读写锁
ReentrantLock,它的特点就是在同一个线程中可以重复加锁,只需要解锁同样的次数就能真正解锁,。可以指定是否公平,判断锁的持有线程是否为当前线程,实现可重入。继承了AQS ,AQS是一个锁的同步器,内部使用了CLH队列和CAS; ReentrantReadWriteLock 适用于多读少写,读写锁有如下特征:
如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁
如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败
如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败
ReentrantReadWriteLock就将state一分二位,高16位表示共享锁的数量,低16位表示独占锁的数量。2^16 – 1 = 65535。ReadLock 是共享锁,HoldCounter 是绑定线程上的一个计数器,读锁的获取、释放过程中该对象在获取线程获取读锁是+1,释放读锁时-1;WriteLock 是一个独占锁。
synchronzied 和lock 区别:
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率
如何理解有界和无界队列
队列需要指定最大容纳数量,或者默认有容纳数量的队列叫做有界队列,否则为无界队列
Java线程的状态
新生:新创建了一个线程对象
就绪:调用了线程的start() 方法
运行:线程获取了CPU执行时间片
阻塞:睡眠,让步,等待
死亡:线程退出run()方法
线程交互:
sleep:线程转为不可运行态,到时自动转为可运行状态,但不会立即运行
yeild: 让步,停止当前线程,转为可运行状态
join: 等待,让一个线程B“加入”到另外一个线程A的尾部
notify:唤醒在此对象监视器上等待的单个线程
notifyAll:唤醒在此对象监视器上等待的所有线程
wait:当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
interrupt:中断当前线程,线程不在存活,会抛出异常。interrupt()常常被用来终止“阻塞状态”线程
唤醒一个阻塞的线程
interrupt()常常被用来终止“阻塞状态”线程
fail-fast 与 fail-safe 机制有什么区别
fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception,内部一般使用一个modCount记录操作数
fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。fail-safe机制有两个问题:需要复制集合,产生大量的无效对象,开销大;无法保证读取的数据是目前原始数据结构中的数据
AtomicInteger为什么要用CAS 而不是synchronized
在大并发环境下,AtomicInteger 效率要比synchronized高很多。
synchronized 需要线程监视器的进出操作。并且线程阻塞需要时间开销
如何考虑线程池核心数量和最大线程数设置
首先,需要考虑到线程池所进行的工作的性质:O密集型,CPU密集型;简单的分析来看,如果是CPU密集型的任务,我们应该设置数目较小的线程数,比如CPU数目加1。如果是IO密集型的任务,则应该设置可能多的线程数,由于IO操作不占用CPU,所以,不能让CPU闲下来。当然,如果线程数目太多,那么线程切换所带来的开销又会对系统的响应时间带来影响
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32
实际业务的线程池数量应该结合请求量和响应时间结合以上原则做适量计算。