1、线程和进程的区别?
二者对比:
- 进程是正在运行程序的实力,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不用的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2、并行和并发有什么区别?
- 并发:在同一时刻,有多个指令在单个CPU上交替执行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
3、创建线程的方式有哪些?
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程(项目中常用方式)
4、Runnable和Callable有什么区别?
- Runnable接口run方法没有返回值
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能上抛。
5、线程的run()和start()有什么区别?
- start():用来启动线程,通过该线程调用run()方法执行run方法中所定义的逻辑代码,start方法只能被调用一次。
- run():封装了要被线程执行的代码,可以被调用多次。
6、线程包括哪些状态?
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)
7、线程状态之间是如何变化的?
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
8、新建T1、T2、T3三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决
join() 等待线程运行结束
public class ThreadTest {
public static void main(String[] args) {
//创建线程1
Thread t1 = new Thread(()->{
System.out.println("t1");
});
//创建线程2
Thread t2 = new Thread(() -> {
try {
//加入线程t1,只有t1线程执行完毕之后,才会执行此线程
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
});
//创建线程3
Thread t3 = new Thread(()->{
try {
//加入t2线程,只有当t2线程执行完毕之后,才会执行这个t3线程
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
});
//启动线程
t1.start();
t3.start();
t2.start();
}
}
9、notify()和notifyAll()有什么区别?
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个wait线程
10、在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,你们也用不了)
11、如何停止一个正在运行的线程?
有三种方式可以停止线程
- 使用退出标志, 使线程正常退出,也就是当run()方法完成后线程终止
- 使用stop方法强行终止(不推荐)
- 使用interrupt方法中断线程
- 打断阻塞的线程(sleep、wait、join)的线程,线程会抛出异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
1、synchronized关键字的底层原理
1)synchronized对象锁采用互斥的方式让同一时刻至多有一个线程能持有对象锁
2)它的底层是由Monitor实现的,Monitor是JVM级别的对象(C++实现),线程获得锁需要使用对象(锁)关联Monitor
3)在Monitor内部有三个属性,分别是owner、entrylist、waitset
- owner:存储当前获取锁的线程,只能有一个线程可以获取
- entryList:关联没有抢到锁的线程,处于Blocked状态的线程
- waitSet:关联调用了wait方法的线程,处于waiting状态的线程
2、 谈谈JMM(Java内存模型)
1)JMM指的是Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
2)JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
3)线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
3、你知道什么是CAS吗?
- CAS全称是Compare And Swap 比较再交换,它体现的是一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
- CAS使用到的地方很多:AQS框架,AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
4、乐观锁和悲观锁的区别?
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试呗
- synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
5、请你谈谈你对volatile的理解
1)保证线程间的可见性
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
2)进制进行指令重排序
指令重排:用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,组织其他读写操作越过屏障,从而达到阻止重排序的效果。
- 写变量让volatile修饰的变量的代码在最后位置
- 读变量让volatile修饰的变量的代码在最开始位置
6、什么是AQS?
1)ASQ是多线程中的队列同步器,是一种锁机制,它是作为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的。
2)AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
3)在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中有一个线程修改成功了state为1,则当前线程就相当于获取了资源
4)在对state修改的时候使用的cas操作,保证了多个线程修改的情况下的原子性
7、AQS是公平锁还是非公平锁?
既可以是公平锁又可以是非公平锁
- 新的线程与队列中的线程共同来抢资源,是非公平锁
- 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
8、ReentrantLock的实现原理?
- ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用lock,是不会再阻塞
- ReentrantLock主要利用CAS + ASQ队列来实现
- 支持公平锁和非公平锁,在提供的构造器中无参默认是非公平锁,也可以传参设置为公平锁
9、synchronized和Lock有什么区别?
语法层面:
synchronized是关键字;源码在JVM中,用C++语言实现
Lock是接口,源码由JDK提供,用Java语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
功能层面:
二者均属于悲观锁,都具备基本的互斥,同步,锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁,可打断,可超时,多条件变量
Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
性能层面:
在没有竞争时,synchronized做了很多优化,比如偏向锁、轻量级锁、性能不赖
在竞争激烈时,Lock的实现通常会提供更好的性能
10、死锁产生的条件是什么?
一个线程需要同时获取多把锁,这时就容易发生死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都会停止执行的情况,某一个同步块同时拥有“两个以上对象的锁”时,就有可能发生死锁的问题
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
也就是多个线程互相拿着对方需要的资源,然后形成僵持
11、如何进行死锁诊断?
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看Java线程内线程的堆栈信息,查看日志,检查是否有死锁,如果有死锁现象,需要查看具体代码分析后,可修复
- 可视化工具jconsole、VisualVM也可以检查死锁问题
12、聊一下ConcurrentHashMap
1)底层数据结构:
- JDK1.7底层采用分段的数组 + 链表实现
- JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组 + 链表/红黑二叉树
2)加锁的方式:
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
13、导致并发程序出现问题的根本原因是什么?
1.原子性 synchronized、lock解决
2.内存可见性 volatile、synchronized、lock解决
3.有序性 volatile解决
1、说一下线程池的执行原理?
2、线程池中有哪些常见的阻塞队列?
ArrayBlockQueue和LinkedBlockingQueue的区别?
3、如何确定核心线程数?
1)IO密集型任务
一般来说:文件读写、DB读写、网络请求等。核心线程数大小设置为2N+1
2)CPU密集型任务
一般来说:计算型代码,bitmap转换,Gson转换等。核心线程数大小设置为 N+1
N指的是CPU的核数
①、高并发、任务执行时间短----->CPU核数 + 1,减少线程上下文的切换
②、并发不高、任务执行时间长
1.IO密集型的任务------>CPU核数*2 + 1
2.计算密集型任务------->CPU核数 + 1
③、并发高、业务执行时间长,解决这种问题的关键不在线程池而在于整体架构的设计的优化,对架构和业务做优化
4、线程池的种类有哪些?
①、newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
②、newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(先进先出)执行
③、newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
④、newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
5、为什么不建议用Executors创建线程池?
参考阿里Java开发手册
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE:可能会堆积大量的请求,从而导致OOM
2)CachedThreadPool:
允许的创建线程数量为:integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
6、说一下线程池的核心参数?
- corePoolSize:核心线程数目
- maximumPoolSize:最大线程数目(最大线程数 = 核心线程 + 救急线程)
- keepAliveTime:生存时间
- unit:时间单位(救急线程的生存时间单位)
- workQueue:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory:线程工厂(可以定制线程对象的创建)
- handler:拒绝策略