并发编程面试笔记

并发编程

基础知识

并发编程的优缺点

优点

充分利用多核CPU的计算能力,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

方便进行业务的拆分,提升系统并发能力和性能

提高程序的执行效率,提高程序的运行速度

缺点

并不是总能提高程序运行速度,且可能会遇到很多问题,如:内存泄漏、上下文切换、线程安全、死锁等

并发编程的三要素?在java中怎么保证多线程的运行安全

三要素

原子性

指一个或多个操作要么全部执行成功,要么全部执行失败

可见性

一个线程对共享变量的修改,另一个线程能立刻看到。(synchronized、volatile)

有序性

程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)

出现线程安全的原因

线程切换带来的原子性问题

缓存导致的可见性问题

编译优化带来的有序性问题

解决办法

JDK Atomic开头的原子类,synchronized、Lock可以解决原子性问题

synchronized、volatile、LOCK可以解决可见性问题

Happens-Before 规则可以解决有序性问题

并行和并发有什么区别

并发

多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上看是同时执行的

如:两个队列和一个咖啡机

并行

多个处理器同时处理多个任务,真正意义上的同时进行

两个队列两个咖啡机

串行

有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行,所以不存在线程安全问题

一个队列一个咖啡机

什么是多线程?多线程的优劣

多线程

指程序中包含多个执行流,即在一个程序中同时运行多个不同的线程来执行不同的任务

优点

可以提高CPU的利用率,在多线程中一个线程必须等待的时候,cpu可以运行其他线程,而不是等待,这样大大提高了程序的效率,也就是说单个程序可以创建多个并行执行的线程来完成各自的任务

劣势

线程越多占用的内存越多

多线程需要协调和管理,所以需要cpu时间跟踪线程

线程之间对共享资源的访问会相互影响,必须解决竞用狗共享资源的问题

什么是线程?什么是进程

进程

一个内存中运行的应用程序。每个进程都有自己独立的一块内存空间。

线程

进程中的一个执行任务(控制单元),负责当前进程中程序的执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据

进程和线程的区别

根本区别

进程是操作系统资源分配的基本单位。而线程是处理器任务调度和执行的基本单位

资源开销

包含关系

内存分配

同一个进程内的不同线程数据是可以共享的,每个进程之间的地址空间和资源都是相互独立的

影响关系

执行过程

什么是上下文切换

任务从保持到在加载的过程就是一次上下文切换

多线程编程中线程的个数都大于cpu的核心数,而一个cpu核心在任意时刻就只能被一个线程使用,为了让这些线程都能得到有效执行,cpu采用的策略是,为每个线程分配时间并轮转的形式,当一个线程的时间片用完就会重新处于就绪状态,让给其他线程使用,这个过程就叫做上下文切换

守护线程和用户线程的区别

守护线程

运行在后台,为前台线程服务,一旦所有用户线程都结束运行,守护线程也会随着JVM一起结束工作

用户线程

运行在前台,执行具体的任务。如程序的主线程

注意事项:

1.setDaemon(true) 必须在start()方法前执行,否则会抛出异常

2.在守护线程中产生的新线程也是守护线程

3.不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑

4.守护线程不能依靠finally来确保关闭,守护线程会随JVM一起结束工作

main函数就是一个用户线程,垃圾回收线程就是一个守护线程

linux如何查看cpu利用率

top命令

创建线程的四种方法

继承thread类

实现Runnable接口

实现callable接口

使用executors工具类创建线程池

说一下runnable和callable有什么区别

相同点

都是接口

都可以编写多线程程序

都采用thread.start()启动线程

不同点

Runnable无返回值,Callable接口的call有返回值,是个泛型,和futrue、futureTask配合可以获取异步执行的结果;注:【callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程继续往下执行,如果不调用不会阻塞】

Runnable只能抛出运行时异常,且无法捕获;callable接口call方法允许抛出异常,可以获取异常信息

线程run()和start()有什么区别

概述:每个线程都是通过某个特定的Thread对象所对应的run()方法来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程

start()方法用于启动线程;run()方法用于执行线程运行时的代码。run()可以重复调用,而start()只能调用一次

start()方法启动线程,真正实现了多线程运行,调用start() 方法无需等待run()方法执行完毕,可以直接继续执行其他代码,此时线程处于就绪状态,并没有运行,然后通过此Thread类调用run()方法来完成其运行状态,run()执行结束,此线程终止,然后cpu再调度其他线程

run()方法在本线程中,只是线程里的一个函数,而不是多线程,如果直接调用run(),其实就是相当于调用了一个普通方法,必须执行完run()方法才会执行下面的代码,所以执行路径还是只有一条,没有线程的特征,所以这也是为什么在多线程执行时要调用start()而不是run().

什么是callable?什么是Futrue?

Callable接口类似于Runnable,但是Runnable不会反悔结果,并且无法抛出异常,而Callable功能较为强大,被线程执行后,可以返回值,这个返回值可以被Futrue拿到

Futrue接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说callable用于产生结果,Futrue用于获取结果

什么是FutrueTask?

一个异步运算的任务

FutrueTask里面可以传入一个Callable的具体实现类,可以对这个异步运算任务的结果进行等待获取、判断是否已经完成、取消任务等操作。

只有等任务完成结果才能取回,如果未完成调用get方法,将会进行阻塞

一个FutrueTask可以对调用了Callable和Runnable的对象进行包装,犹豫FutrueTask也是Runnable接口的实现类,所以它也可以放入线程池中

线程生命周期及五种基本状态

1.新建(new):新创建了一个线程对象

2.可运行(runnable):线程对象创建后,当调动线程的start()方法时,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权

3.运行(running):可运行状态的线程获得了cpu时间片(timeslice),执行程序代码

4.阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对cpu的使用权,停止执行,此时进入阻塞状态,直到进入就绪状态,才有可能再次被选中

阻塞的情况分三种

等待阻塞

执行wait()方法;Jvm会把该线程放到等待队列(waitting queue)中,线程进入等待阻塞状态

同步阻塞

线程在获取synchronized同步锁失败(因为锁被其他线程占用)则jvm会将该线程放入锁池(lock pool)中,线程进入同步阻塞状态

其他阻塞

调用sleep()、join()方法,会进入阻塞状态,当sleep超时、join等待线程终止,线程重新进入就绪状态

5.死亡(dead)线程run()、main()方法执行结束,或者因为异常退出了run()方法,则改线程结束生命周期,死亡的线程不可能在复活。

就绪状态是进入到运行状态的唯一入口,线程想要再次运行,就得先回到就绪状态

java中用到的线程调度算法是什么

分时调度

所有线程轮流获得cpu的使用权,并且平均分配每个线程占用cpu的时间片

抢占式调度

JVM采用的就是这种模型。是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么随机选择一个。处于运行中的线程会一直运行,直到他不得不放弃CPU

线程调度策略

线程调度器现在优先级最高的线程运行,发生以下情况会终止线程的运行

1.线程体中调用了yieid()方法让出了占用权利

2.调用sleep(),使线程进入了休眠状态

3.线程由于IO操作受到阻塞

4.另一个更高优先级的线程出现

5.在支持时间片的系统中,该线程的时间片用完

什么是线程调度器(thread scheduler)和时间分片(time slicing)

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配cpu时间。一旦我们创建一个线程并启动它,它的执行边依赖于线程调度器的实现

时间分片是指将可用的时间分配给可用的Runnable线程的过程。分配CPU时间可以基于优先级或者线程等待的时间

线程调度器并不受到Jvm的控制,所以由应用程序控制是更好的选择【也就是说不要让你的程序依赖于线程的优先级】

与线程同步以及线程调度相关的方法

wait()

使一个线程处于等待(阻塞)状态,并且释放所持有的锁

sleep()

是一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法需要处理Interrupted Exception

notify()

唤醒一个处于等待状态的线程,但是在调用此方法时,并不能确定要唤醒哪一个等待状态的线程,而是由JVM来确定,而且与优先级无关

notifyAll()

唤醒所有处于等待中的线程,该方法并不是将对象的锁给所有线程,而是让他们竞争,只有获得锁的线程才能进入就绪状态

sleep()和wait()有什么区别

两者都可以暂停线程的执行

类的不同;sleep是thread线程类的静态方法;wait是Object类的方法

是否释放锁;sleep不释放锁;wait释放锁

用途不同;wait通常用于线程间的交互/通信;sleep用于暂停操作

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

如何停止一个正在运行的线程

在Java中有三种方法可以终止

1.使用退出标志,是线程正常退出,也就是run方法执行完后终止

2.使用stop方法强行停止,不推荐

3.使用interrupt方法中断线程

java中interrupted和isInterrupted方法的区别

interrupt:用于中断线程【注:线程中断仅仅是置线程的中断状态位,不会停止线程,需要用户自己去监视线程的状态位并做处理】

Interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用interrupted返回的是true,第二次后面就是false了

isInterruped:查看当前中断信号是false还是true

什么是阻塞式方法

指程序会一直等待该方法完成,期间不做其他事情

Java中怎样唤醒一个阻塞的线程?

notify方法

如何在两个线程间实现共享

在两个线程间共享变量即可实现

java中如何实现多线程之间的通讯和协作?

可以通过中断和共享变量的方式实现

1.synchronized加锁的线程的Object类的wait、notify、notifyAll

2.ReentrantLock类加锁的线程的Condition类的await、signal、signalAll

同步方法和同步块哪个是更好的选择?

同步块是更好的选择,同步方法是对象级别的,锁的粒度大。同步块更符合开放调用的原则,只需要锁住的代码块锁住相应的对象,这样也可以从侧面避免死锁

同步的范围越小越好

如果你提交任务时,线程池队列已满,这时会发生什么?

1.如果使用的无界队列LinkedBlockingQueue,没关系,继续添加任务到阻塞队列中等待执行,因为无界队列近乎是一个无穷大的队列

2.如果使用的有界队列比如:ArrayBlockingQueue,任务会首先添加到队列中,如果队列满了会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,有界队列还是满,则会使用拒绝策略处理满了得任务

什么叫线程安全?servlet是线程安全的吗?

指某个方法在多线程的环境中被调用时,能够正确的处理多个线程之间的共享变量,是程序能正确的完成

servlet不是线程安全的,它是单实例多线程的,如果多个线程访问同一个方法是不能够保证共享变量的线程安全性

struts2是线程安全的,它是多实例多线程的,每个请求过来都会new一个新的action分配给这个请求,请求完成后销毁

可以使用threadLocal来处理多线程的安全性问题

在java中怎么保证多线程的运行安全

1.使用安全类,原子类

2.使用自动锁 synchronized

3.使用手动锁 lock

对线程优先级的理解

线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被new这个线程类所在的线程所调用,而run方法里面的代码才是被线程自身所调用

一个线程发生异常时会怎么样

如果异常没有被捕获,该线程会停止执行

java中线程数过多会造成什么异常

线程的生命周期开销非常高

降低JVM的稳定性

消耗过多的cpu

并发理论

java中垃圾回收有什么目的,什么时候进行垃圾回收

什么时候回收

在内存中存在没有 引用的对象,或超过作用域的对象时回收

目的

识别并且丢弃应用不再使用的对象来释放资源和重用资源

如果对象的引用被置为null,垃圾回收是否会立即释放对象占用的内存

不会,在下一次回收时才会释放其占用的内存

finalize()方法什么时候被调用?

垃圾回收器决定回收某对象时,就会运行该对象的finalize()方法

finalize是Object的一个方法,一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize方法,并且下一次垃圾回收动作发生时,才是真正的回收

析构函数(finalizeation)的目的是什么?

gc本来就是内存回收,引用还需要finalizeation做什么呢?答案是大部分时候,什么都不用做(也就是不用重载)。只有在特殊情况下,比如你调用了native方法(一般是C写的),可以在finalizeation里去调用c的函数

为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,需要满足两个条件

1.单线程环境下不能改变程序运行的结构

2.存在数据依赖关系的不允许重排序

重排序不会影响单线程环境的执行结构,但是会破坏多线程的执行语义

并发关键字

synchronized的作用

用来控制线程同步,在多线程环境中控制synchronized代码段不被多个线程同时执行。可用来修饰类、方法、变量

说说自己是怎么使用synchronized

三种使用方式

实例方法

作用于当前对象实例加锁,进入同步代码前需要获取当前对象实例的锁

静态方法

给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明是该类的一个静态资源,不管new了多少个对象,只有一份)所以如果线程A调用一个实例对象的非静态synchronized方法,而B需要调用这个对象的静态synchronized方法,是允许的,因为访问静态synchronized方法占用的锁是当前类的,而非静态synchronized方法占用的锁是当前实例对象锁

代码块

指定加锁对象,对给定对象加锁,进入代码块前要获取给定对象的锁

synchronized关键字加到静态方法static和synchronized(class)代码块上都是给Class上锁。加到实例方法上是给对象实例上锁,尽量不要使用synchronized(String s)因为Jvm中,字符串常量池具有缓存功能

synchronized可重入的原理

重入锁是指一个线程获取到该锁后,该线程可以继续获得该锁。

底层维护了一个计数器,当线程获取锁时,计数器+1,再次获得再次+1,释放锁时-1,当计数器为0时,表示该锁未被任何线程持有,其他线程可以竞争

什么是自旋

当前锁被其他线程占用还未释放,那么当前线程就会进入循环,一直重复获取锁的状态,从而不会被挂起或者睡眠状态,当获取到锁后会跳出循环,执行相关代码。可以减少上下文之间的切换。

synchronized锁升级的原理是什么?

在对象头有一个threadId字段,在第一次访问的时候threadId为空,jvm让其持有偏向锁,并将threadId设置为其线程id,再次进入会判断这个threadId是否与线程id一直,如果一致就可以直接使用此对象,如果不一致则升级为轻量级锁,通过自旋一定次数来获取锁(默认10次)如果还没有获取到要使用的对象,此时就会把锁从轻量级升级到重量级锁。

其目的是为了减少加锁带来的性能消耗。java6之后优化的

线程B怎么知道线程A修改了变量

1.volatile修饰变量

2.synchronized修饰修改变量的方法

3.wait/notify

4.while 轮询

synchronized、volatile、CAS比较

synchronized是悲观锁,属于抢占式,会引起其他线程的阻塞

volatile提供多线程共享变量的可见性和禁止指令重排序优化

CAS是基础冲突检测的乐观锁(非阻塞)

synchronized和Lock的区别

synchronized是java内置关键字,在jvm层面,Lock是一个类

synchronized可以给类、方法、代码块加锁,lock只能给代码块加锁

synchronized不用手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而Lock需要手动加锁和释放锁,如果使用不当没有unLock()去释放锁,会造成死锁

synchronized无法知道有没有成功获取到锁,而Lock可以

synchronized和ReentrantLock区别是什么

synchronized是关键字,RenntrantLock是类。ReentrantLock比synchronized更灵活,可以被继承,可以有方法及各种变量

两者都是可重入锁

RenntrantLock使用灵活,但是必须手动获取锁和手动释放锁、只适用于代码块

二者的锁机制不一样,RenntrantLock底层调用的是Unsafe的park方法加锁,而synchronized操作的是对象头中的Mark word

Java中的每一个对象都可以作为锁,这是synchronized实现同步的基础

普通同步方法,锁是当前的实例对象

静态同步方法,锁是当前类的Class对象

同步方法块,锁是括号里面的对象

volatile关键字的作用

可见性,java提供了volatile关键字来保证可见性和禁止指令重排。【volatile提供happens-before的保证,确保一个线程的修改对其他线程是可见的。当一个共享变量被volatile修饰时,他会保证修改的值立即被更新到主存,当有其他线程需要读取时,他会去内存中读取新值】

从实践的角度而言,volatile的一个重要作用就是和cas结合,保证原子性

常用语多线程环境下的单词操作(单次读或者单次写)

java中能创建volatile数组吗

能!不过只是一个指向数组的引用,而不是整个数组。意思是:如果改变引用指向的数组,将会受到volatile的保护,如果多个线程同时改变数组的元素,就不能起到之前的保护作用了

volatile和atomic有什么不同

volatile可以确保先行关系,即写操作会发生在读操作之前,及共享变量的可见性

atomic可以让操作具有原子性

volatile能使一个非原子性操作变成原子操作吗?

关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步

可以使用volatile修饰long和double可以保证其操作原子性

对于64位的引用地址读写,都是原子性操作

synchronized和volatile的区别

synchronized表示只有一个线程可以获取对象的锁,执行代码,阻塞其他线程

volatile表示变量在cpu的寄存器是不确定的,必须在主存中读取,保证多线程环境下变量的可见性,禁止指令重排序

volatile是变量修饰符,synchronized可以修饰类、方法、变量

volatile仅能实现变量的修改可见性,不能保证原子性,synchronized都可以实现

volatile不会阻塞线程,synchronized会阻塞线程

volatile标记的变量不会被编译器优化,synchronized会

什么是不可变对象,他对并发应用有什么帮助

对象一旦被创建它的状态(对象的数据、属性)就不能改变

不可变对象即为不可变类。比如String、BIgdecimal、BigInteger

只有满足一下条件才是不可变

状态不能再创建后再被修改

所有域都是final类型,并且它被正确创建(创建期间没有this引用的溢出)

不可变对象保证了对象内存的可见性,对不可变对象的读取不需要额外的同步手段,提高了代码的执行效率

Lock体系

java Concurrency api中的Lock接口是什么?对比同步有什么优势

Lock接口比同步方法和同步块,提供了更具扩展性的锁操作,他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象

优势

可以使锁更公平

可以是线程在等待锁的时候响应中断

可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

可以在不同的范围以不同的顺序获取和释放锁

整体来说Lock是synchronized的扩展,Lock提供了,无条件、可轮询的(tryLock())方法,定时的、可中断的、可多条件队列的锁操作。另外Lock的实现类基本都支持非公平锁和公平锁,synchronized只支持公平锁,当然在大部分情况下,非公平锁是高效的选择

乐观锁和悲观锁的理解及如何实现,有哪些方式实现?

悲观锁

总是假设最坏的情况,每次那数据都认为别人会修改,所以每次拿数据都会上锁,这样别人想要拿到这个数据就会被阻塞,知道拿到锁。如数据库中的行锁、表锁、读锁、写锁等及java关键字synchronized

乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是更新的时候会去判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读得应用类型,这样可以提高吞吐量,如CAS

乐观锁的实现方式

使用版本标识来确定读到的数据与提交时的数据是否一致,提交后修改版本标识,不一致可以采取丢弃和重试的策略

java中的CAS,当多个线程使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是告知这次竞争失败,并可以再次尝试

什么是CAS

比较与交换

cas是一种基于锁的操作,而且是乐观锁

cas包含三个操作数,内存位置(V)、预期原值(A)和新值(B),如果内存地址(V)中的值和预期原值(A)是一样的,那么就将内存里的值更新成新值(B)

CAS会产生什么问题?

ABA问题

线程A拿到内存位置的值为10,这时线程B也拿到,第一次将值修改为100,又再次修改为了10,这时候线程A的进行CAS时发现内存中还是10,然后线程A操作成功,尽管线程A操作成功,但是可能会有潜在的问题。从java1.5开始JDK的atomic包里提供了一个AtomicStampedReference来解决ABA问题

循环时间开销大

对于资源竞争强烈的情况,cas自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized

只能保证一个共享变量的原子操作

多个共享变量加锁

什么是线程死锁

线程在占有资源的同时又去申请别人的资源,但是大家都不释放资源,导致一直等待造成死锁

形成死锁的是个必要条件

互斥

线程/进程对于分配到的支援具有排他性,一个资源只能被一个线程占用,直到被该线程释放

请求与保持

因请求被占用资源而发生堵塞时,对已获得的资源保持不放

不剥夺

已获得的资源未使用完之前不能被其他线程强行剥离,只能自己用完才释放资源

循环等待

当死锁发生时,所等待的线程必定会形成一个环路(死循环),造成永久阻塞

如何避免死锁

只需破坏四个产生条件中的其中一个就行

破坏互斥条件

无法破坏,因为用锁就是让他们互斥

破坏请求与保持条件

一次性申请所有资源

破坏不剥夺条件

占用部分资源的进程再进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

破坏循环等待条件

按顺序申请资源来预防;按某一顺序申请资源,释放资源反序释放

死锁与活锁的区别,死锁与饥饿的区别

活锁

任务或执行者没有被阻塞,由于某些条件没有满足,导致一直在重复尝试,失败,尝试

死锁

指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力的作用,他们都无法推进下去

饥饿

一个或多个线程因为种种原因无法获得所需的资源,导致一直无法执行

导致饥饿的原因

1.高优先级的线程吞噬低优先级的cpu时间

2.线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在他之前持续的对该同步块进行访问

3.线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)

它们的区别在于:处于活锁的实体是在不断的改变状态,而死锁的表现为一直等待。活锁有可能自行解开,死锁不能

多线程锁升级的原理

在java中锁共有四种状态,级别从低到高分别为,无所状态、偏向锁、轻量锁、重量锁。这几个状态会随着竞争情况逐渐升级。锁可以升级但是不会降级

AQS

aqs介绍

是一个抽象的队列同步器,通过维护一个共享资源状态和一个(FIFO)先进先出的线程等待队列来实现一个多线程访问共享资源的同步框架

aqs原理分析

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

aqs对资源的共享方式

Exclusive(独占)

只有一个线程能执行,如:ReentrantLock

公平锁

按照线程在队列中的顺序,先到先得

非公平锁

当线程要获取锁时,无视队列顺序直接去抢,谁抢到就是谁的

share(共享)

多个线程可以同时执行

aqs底层使用了模板范式模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样使用:

1.使用者继承AbstractQueuedSynchronizer并重写指定方法

2.将aqs组合在自定义同步组件上实现,并调用起模板方法,这些模板方法会调用使用者重写的方法

自定义同步器需要重写下面几个方法

isHeldExclusively() 该线程是否正在独占资源,只有用到condition才需要实现

tryAcquire(int) 独占方式,尝试获取资源,成功true 失败false

tryRelease(int)独占方式 ,释放资源 成功true 失败false

tryAcquireShared(int)共享方式。尝试获取资源,负数表示失败,0成功,但没有剩余可用资源,正数表示成功,且有剩余资源

tryReleaseShared(int)共享方式。尝试释放资源,成功true 失败false

什么是可重入锁(ReentrantLock)?

ReentrantLock重入锁,是实现Lock接口的一个类,也是实际编程中使用频率很高的一个锁。支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁,再次获取不会被阻塞

在java关键字Synchronized影视支持重入锁,通过获取自增、释放自减的方式实现重入。

ReentrantLock还支持公平锁和非公平锁

要想支持重入性,需要解决两个问题

1.在线程获取锁的线程是当前线程的话,则再次获取成功

2.由于锁会被获取n次,那么只有锁在被释放同样的n次后,该锁才算是完全释放成功

并发容器

什么是ConCurrentHashMap?

ConcurrentHashMap是java中一个线程安全且高效的HashMap实现

Java中ConcurrentHashMap的并发度是什么?

concurrentHashMap把实际的map划分成若干部分来实现他的可扩展性和线程安全

什么是并发容器的实现

java中的同步集合和并发集合有什么区别

并发集合的可扩展性高

SynchronizedMap和ConcurrentHashMap有什么区别

SynchronizedMap一次锁住整张表来保证线程安全,所以只能有一个线程来访问Map

ConcurrentHashMap使用分段锁来保证多线程下的性能,一次只锁住一个桶【默认将hash表分为16个桶,如果get、set操作只锁住当前需要用到的桶】

CopyOnWriteArrayList是什么,有什么应用场景?有哪些优缺点?

它是一个并发容器,在非复合场景下操作他是线程安全的。

场景:读多写少

缺点:

由于写操作的时候需要拷贝数组,会消耗内存,如果原数组数据量大的情况下,可能导致young gc 或者full gc

不能用于实时读的场景,像拷贝数组、新增数组都需要时间,所以调用一个set后,读取到的数据可能还是旧的,是最终一致性的

实际应用中可能没法保证数据量大小,如果数据量大每次add/set都要重新复制数组,代价太大

设计思想

读写分离

最终一致性

使用另外开辟空间的思路,来解决并发冲突

ThreadLocal是什么?有什么使用场景?

它是一个本地线程副本变量的工具类,在每个线程中都创建一个ThreadLocalMap对象。是一种空间换时间的做法,每个线程都可以访问自己内部的ThreadLocalMap对象内的value,通过这种方式避免资源在多线程中的共享

原理:线程局部变量是局限于线程内部的变量,属于线程自身,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量的时候要小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦工作完成没有释放,java应用就存在内存泄漏的风险

什么是线程局部变量

是局限于线程内部的变量,属于线程自身所有

ThreadLocal造成内存泄漏的原因?

ThreadLocalMap中使用的key为弱引用,而value是强引用,所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候key会被清理掉,而value不会,这样ThreadLocalMap中就会存在key为null的entry。假如我们不做任何措施,value永远无法被gc回收,这样就会造成内存泄漏。

现实中ThreadLocalMap已经考虑了这种情况,在调用get、set、remove方法时,会清理掉key为null的记录,使用完ThreadLocal方法后最好手动调用一下remove方法。

ThreadLocal内存泄漏怎么解决?

每次使用完都调用一下remove()方法

什么是阻塞队列(BlockingQueue)?实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

是一个支持两个附加操作的队列

在队列为空时,获取元素的线程会等待队列变为非空

当队列满时,存储元素的线程会等待队列可用

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里那元素的线程

阻塞队列就是生产者存放元素的容器,而消费者也是从容器里拿元素

jdk提供7个阻塞队列

ArrayBlockingQueue :由数组结构组成的有界阻塞队列

LinkedBlockingQueue :由链表组成的有界阻塞队列

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列

DelayQueue:一个使用优先级队列实现的无界阻塞队列

SynchronousQueue:一个不存储元素的阻塞队列

LinkedTransferQueue:由链表组成的无界阻塞队列

LinkedBlockingDeque:由链表组成的双向阻塞队列

线程池

什么是线程池?有哪几种创建方式?

在面向对下你给编程中,创建和销毁对象是很耗时的,所以提高服务程序效率的一种手段就是尽可能的减少创建、销毁对象的次数,特别是一些特别好资源的对象,这就是池化技术的产生原因

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取,不用自行创建,使用完不需要销毁而是放入池中,从而减少创建和销毁线程对象的开销

常用的线程池

newSingleThreadExecutorL:创建一个单线程的线程池。这个线程池只有一个线程在工作(单线程串行执行所有任务)如果这个唯一的线程因为异常结束,那么会有一个新的线程替代它

newFixedThreadExecutor:创建固定大小的线程池。没提交一个任务就创建一个线程,知道线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果一个线程因为异常而结束,那么线程池会补充一个新的线程。建议使用

newCachedThreadExecutor:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理人物。此线程不对线程池大小做限制,线程池大小完全依赖于操作系统能够创建的最大线程数

newScheduledThreadExecutor:创建一个无限大小的线程池,此线程支持定时及周期性执行任务的需求

线程池有什么优点?

提高系统资源的使用率

降低资源消耗

提高响应速度

提高线程的可管理性

线程池都有哪些状态?

running:正常状态,接受心热奴王,处理等待中的任务

shutdown:不接受新任务,但是会继续处理等待队列中的任务

stop:不接受新任务的提交,不在处理等待队列中的任务,中断正在执行的任务线程

tidying:所有任务都销毁了,workCount=0,线程转换为tidying状态时,会执行钩子方法terminated()

terminated:terminated()方法结束后,线程池状态就是这个

什么是executor框架?为什么要使用executor

excutor是一个根据一组执行策略调用、调度、执行和控制的异步任务框架

可以非常方便的创建线程池

在java中executor和executors的区别?

executors是一个工具类,其不同的方法按照我们的需求创建了不同的线程池,来满足业务需求

executor接口对象能执行我们的线程任务

线程池中submit()和executor()有什么区别

接收参数

executor()只能执行runnable类型的任务。submit()可以执行Runnable和callable类型的任务

返回值

submit()方法可以返回持有计算结果的Futrue对象,而executor没有

异常处理

submit()方便exception处理

什么是线程组,为什么在java中不推荐使用?

threadGroup类,可以把线程归属到某一个线程中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点像树

线程组是为了方便线程的管理,线程池是管理线程的生命周期、复用线程、减少创建销毁线程的开销

为什么不推荐

stop、resume会有安全问题,会导致死锁,官方已经弃用

executors和threadPoolExecutor创建线程池的区别?

《阿里巴巴java开发手册》中强制线程池不允许使用executors创建,而是通过TreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors各个方法的弊端

newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM

newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM

ThreadPoolExecutor创建线程池的方式只有一种,那就是走他的构造函数,参数自己定

你知道怎么创建线程池吗?

ThreadPoolExecutor

ThreadPoolExecutor构造函数重要参数分析

corePoolSize:核心线程数

maximumPoolSize:线程池中最大线程数。

任务队列已满,线程池会创建新线程,但是会受到这个值的限制

任务队列未满,任务加入到任务队列中

KeepAliveTime:线程池中的线程数量大于核心线程数(corePoolSize)时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是等待,等待时间超过了KeepAliveTime才会被回收销毁

unit:KeepLiveTime的时间单位

workQueue:线程任务队列,新任务进来,会判断当前运行的线程数量是否到达核心线程数量,到达任务会被存放到队列中

threadFactory:创建新线程的线程工厂

handler:线程池任务队列超过MaxinumPoolSize之后的拒绝策略

线程池流程图

ThreadPollExecutor饱和策略

abortPolicy(默认策略):直接抛出异常RejectedExecutionException来拒绝新的任务

discardPolicy:不处理直接丢弃

discardoldestPolicy:丢弃最早的未处理的任务,并执行当前任务

CallerRunsPolicy:不进入线程池执行,任务将由调用者线程执行。如果执行程序已关闭,则会丢弃该任务。当最大池被填满时,此策略为我们提供可伸缩队列。因此它会降低新任务的提交速度,影响性能,如果程序可以承受延迟并且任何一个请求都要执行的话可以选择这个策略

自定义策略:实现rejectedExecutionHandler接口自定义策略

原子操作类

什么是原子操作,在java Concurrency api中有哪些原子类

原子操作:多个步骤组成的一个操作,要么不执行,要么就要执行完所有操作,不可被中断的一系列或者一个操作

原子类:AtomicInteger、AtomicBoolean

子主题 3

说一下atomic原理

在多线程的环境下,当有多个线程同时对单个变量进行操作时,具有排他性。

【多个线程进行修改,只能有一个成功,其他线程可以像自旋一样,继续尝试,一直到执行成功】

相关推荐
我明天再来学Web渗透9 分钟前
【使用Apache Flink 实现滑动窗口流式计算】
开发语言·flink·apache·linq
码农研究僧10 分钟前
Spring Boot 中的事件发布与监听:深入理解 ApplicationEventPublisher(附Demo)
java·spring boot·后端·事件发布与监听
NiNg_1_23413 分钟前
Spring Boot 实现文件上传和下载
java·spring boot·后端·文件上传
gentle_ice1 小时前
leetcode——二叉树的中序遍历(java)
java·数据结构·算法·leetcode
苹果酱05671 小时前
mysql.sock.lock 导致mysql重启失败
java·spring boot·毕业设计·layui·课程设计
吃一口大米饭1 小时前
合并两个有序链表(leetcode刷题)
java·数据结构·算法·leetcode·链表
简 洁 冬冬2 小时前
Java中的Servlet
java·开发语言·servlet
fly spider2 小时前
多线程-线程池的使用
java·面试·线程池·多线程·juc
组合缺一2 小时前
Solon Cloud Gateway 开发:导引
java·gateway·reactor·solon·响应式
matlabgoodboy3 小时前
留学生scratch计算机haskell函数ocaml编程ruby语言prolog作业VB
开发语言·后端·ruby