多线程和并发编程(面试题)

线程和进程的区别?

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换的是从一个线程切换到另一个线程)

并行与并发的区别?

单核CPU

  • 单核CPU下线程是实际是串行执行的
  • 操作系统中有一个组件叫做任务调度器,将CPU的时间片(windows下时间最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
  • 总结一句话就是:微观串行,宏观并行
  • 一般会将这种线程轮流使用CPU的做法成为并发

多核CPU

        每个核(core)都可以调度运行线程,这时候线程可以是并行的

区别

        并发(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个CPU

        并行(paralle)是同一时间动手做(doing)多件事情的能力,4核CPU同时执行4个线程

创建线程的方式有哪些?

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口
  • 线程池创建线程(项目中使用方式)

runnable核callable都可以创建线程,他们有什么区别?

  1. Runnable接口run方法没有返回值
  2. Callable接口call方法有返回值,是个泛型,和Future,FutureTask配合可以用来获取异步执行的结果
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

在启动线程的时候,线程的run()和start()有什么区别?

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次

run():封装了要被线程执行的代码,可以被调用多次

线程包括那些状态?

新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)

状态之间是如何变化的?

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结果是终止状态
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized或者lock)进入阻塞状态,获得锁在切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
    • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

T1、T2、T3三个线程,如果保证它们的顺序性?

可以使用线程中的join方法解决

join()   等待线程运行结束

notify() 和 notifyAll() 有什么区别?

notifyAll():唤醒所有wait的线程

notify:只随机唤醒一个wait线程

java中wait和sleep方法的不同?

共同点

wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,今天阻塞状态

不同点

1,方法归属不同

  • sleep(long)是Thread的静态方法
  • 而wait(),wait(long)都是Object的成员方法,每个对象都有

2.醒来时机不同

  • 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
  • wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等待下去
  • 他们都可以被打断唤醒

3.锁特性不同(重点)

  • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
  • wait方法执行后会释放对象锁,允许其他线程获取该对象锁,(我放弃cpu,但是你们还可以用)
  • 而sleep如果在synchronized代码块中执行,并不会释放锁对象(我放弃cpu,你们也用不了)

如何停止一个正在允许的线程?

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

使用stop方法强行终止(不推荐,方法已作废)

使用interrupt方法中断线程

  •         打断阻塞的线程(sleep,wait,join)的线程方法,线程会抛出interruptException异常
  •         打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitlist
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waitiing状态的线程

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

java中的synchronized有偏向锁,轻量级锁,重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

重量级锁:底层使用的Monitor实现,里面设涉及到了用户态和内核态的切换,进程的上下文切换,成本较高,性能比较低

轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多,每次修改都是CAS操作,保证原子性

偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程在获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,他们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题

轻量级锁

加锁流程

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象

2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无状态则修改成功,代表该线程获得了轻量级锁

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用

4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue

3.如果Lock Record的Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁 

你谈谈JMM(java内存模型)

JMM(Java Memory Model)java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性

JMM把内存分为了两块,一块时私有线程的工作区域(工作内存),一块时所有线程的共享区域(主内存)

线程跟线程之间时相互隔离,线程跟线程交互需要通过主内存

CAS你知道吗?

CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性

CAS使用到的地方很多:AQS框架、AtomicXXX类

在操作共享变量的说话使用的自旋锁,效率上会更高一些

CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

乐观锁和悲观锁的区别

CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试呗

synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

谈谈对volatile的理解

保证线程的可见性

用volatile修改共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

volatile禁止指令重排序

用volatile修改共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止从排序的效果

什么是AQS?

  • 是多线程中的队列同步器,是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
  • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
  • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中有一个线程修成功了state为1,则当前线程就相等于获取了资源
  • 在对state修改的说话使用cas操作,保证多个线程修改的情况下原子性

RenntrantLock的实现原理

RenntrantLock翻译过来就是可重入锁,相对于synchronized它具备一下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都持支重入

原理

  • RenntrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,在次调用lock,是不会在阻塞
  • RenntrantLock主要利用CAS+AQS队列来实现
  • 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

synchronized和Lock有什么区别?

  • 语法层面

        synchronized是关键字,源码在jvm中,用c++语言实现

        Lock是接口,源码由jdk提供,用java语言实现

        使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

  • 功能层面

        二者均属于悲观锁,都具备基本的互斥、同步、锁重入功能

        Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量

        Lock有适合不同场景的实现,如ReentrantLock,ReenTrantReadWriteLock(读写锁)

  • 性能层面

        在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能不赖

        在竞争激烈时,Lock的实现通常会提供更好的性能

死锁产生的条件是什么?

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断?

jps:输出JVM中运行的进程状态信息

jstack:查看java进程内线程的堆栈信息

jconsole

用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具

打开方式:java安装目录bin目录下直接启动jconsole.exe就行

VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间的内存中的对象,已被GC的对象,反向查看分配的堆栈

打开方式:java安装目录bin目录下直接启动jvisualvm.exe就行

ConcurrentHashMap

1、底层数据结构:

  • jdk1.7底层采用分段的数组+链表实现
  • jdk1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

2、加锁方式

  • jdk1.7采用Segment分段锁,底层使用的是ReentrantLock
  • jdk13.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首届点,相对于Segment分段锁粒度更细,性能更好

导致并发程序出现的问题的根本原因是什么?

(java程序中怎么保证多线程的执行安全)

java并发编程三大特征

原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行  (synchronized,lock)

内存可见性:让一个线程对共享变量的修改对另一个线程可见 (volatile、synchronized、lock)

有序性:(volatile)

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

说一下线程池的核心参数?

  • corePoolSize        核心线程数
  • maximumPoolSize        最大线程数目 = (核心线程 + 救济线程的最大数目)
  • keepAliveTime        生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  • unit        时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  • workQueue        当没有空闲线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  • threadFactory        线程工厂-可以制定线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler        拒绝策略-当线程都在繁忙,workQueue也要放满时,会触发拒绝策略

线程池中有哪些常见的阻塞队列

workQueue - 当没有空闲线程时,信赖任务会加入到此队列排队,队列满会创建救急执行任务

1.ArrayBlockingQueue:基于数据结构得有界阻塞队列,FIFO

2.LinkedBlockingQueue:基于链表结构得有界阻塞队列,FIFO

3.DelayedWorkQueue:是一个优先级队列,它可以保证每次出队得任务都是当前队列中执行时间最靠前得

4.SynchronousQueue:不存储元素得阻塞队列,每个插入操作都必须等待一个移除操作

ArrayBlockingQueue和LinkedBlockingQueue区别

LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层时链表底层是数组
是懒惰得,创建节点的时候添加数据前提初始化Node数组
入队会生成新NodeNode需要时提前创建好的
两把锁(头尾)一把锁

如何确定核心线程数

  • IO密集型任务

        一般来说:文件读写,DB读写、网络请求等                           核心线程数大小设置为2N+1

  • CPU密集型

        一般来说:计算性代码、Bitmap转换、Gson转换等                核心线程数大小设置为N+1

1、高并发、任务执行时间短 > ( CPU核数+1),减少线程上下文的切换

2、并发不高、任务执行时间长

  • lO密集型的任务 >(CPU核数* 2 + 1)
  • 计算密集型任务 >( CPU核数+1 )

3、并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考 (2)

线程池的种类有哪些

1、newFixedThreadPool:创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待

2、newSingleThreadExecutor:创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行

3、newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

4、newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行

为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》

【强制】线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明 : Executors 返回的线程池对象的弊端如下 :

1) FixedThreadPool和 SingleThreadPool :

        允许的请求队列长度为 Integer.MAX VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool :

        允许的创建线程数量为Integer.MAX VALUE,可能会创建大量的线程,从而导致OOM。

线程池使用场景(CountDownLatch、Future)

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await() 用来等待计数归零
  • countDown() 用来让计数减一

多线程使用场景一(es数据批量导入)

使用了线程池+countDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM

 多线程使用场景二(数据汇总)

在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,都可以使用线程池+future来提升性能

 多线程使用场景三(异步调用)

为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可能提升方法响应时间

如何控制某个方法允许并发访问线程的数量

在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量

1、创建Seamaphore对象,可以给一个容量

2、acquire()可以请求一个信号量,这时候的信号量个数-1

3、release() 释放一个信号量,此信号量个数+1

谈谈对ThreadLocal的理解

1.ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题

2、TheradLocal同时实现了线程内的资源共享

3、每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象

        调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中

        调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源之

        调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

4、ThreadLocalMap中的key是弱引用,值为强引用;key会被GC释放内存,关联value的内存并不会释放。建议主动remove释放key,value

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值