一、线程的基础知识
1、线程和进程的区别
一个线程就是一个指令流,将指令流中的一条条指令以一定顺序交给CPU执行
一个进程之内可以分为一到多个线程。
二者对比
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务。
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间。
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换是指从一个线程切换到另外一个线程)
2、并行和并发的区别
单核CPU
- 单核CPU下线程实际还是串行执行的
- 操作系统中有一个组件叫作任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间(时间片很短)切换的非常快,人类的感觉是同时运行的。
- 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
并发:(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个CPU
并发:(parallel)是同一时间动手做(doing)多件事的能力,4个CPU同时处理4个线程。
3、创建线程的方式有哪些
1)继承Thread类
public class MyThread extends Thread{
@Override
public void run(){
System.out.println();
}
public static void main(String[]args){
//创建线程
MyThread my1 = new MyThread();
my1.start();
}
}
2)实现runable接口
public class MyRunable implements Runnable{
@Override
public void run(){
System.out.println("");
}
public static void main(String[]args){
MyRunable my = new MyRunable();
my.start();
}
}
3)实现Callable接口
public class MyCallable implements Callable<String>{
@Override
public String call() throws Exception(){
System.out.println(System.currentThread().getName());
return "OK";
}
public static void main(String[]args){
MyCallable my = new MyCallable();
FutureTask<String> ft = new FutureTask<String>(my);
Thread t1 = new Thread(ft);
t1.start();
String result = ft.get();
System.out.println(result);
}
}
4)线程池创建线程
public class MyExecutors implements Runnable{
@Override
public void run(){
System.out.println("MyRunnable ... run ...");
}
public static void main(String[]args){
//创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors());
//关闭线程池
threadPool.shutdown();
}
}
2、Runnable 和 Callable有什么区别?
Runnable 接口run方法没有返回值
Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以获取异步执行的结果。
Callable接口的call方法允许抛出异常;而Runnable接口的run不能抛出异常。
3、启动线程的时候,可以使用run方法吗?run()和start()方法执行有什么不同
可以使用run方法。
run():只是一个普通方法可以执行多次。
start() :用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑。start只能执行一次。
4、线程间包括哪些状态,状态之间是如何变化的
5、新建T1、T2、T3 三个线程,如何保证它们按顺序执行
可以使用线程中的join方法解决
join() 等待线程运行结束。
t.join() 阻塞调用此方法的线程进入 timed_waiting。
直到线程t执行完成后,此线程再继续执行。
6、notify()和notifyAll() 有什么区别
notify() :随机唤醒一个处于wait状态下的线程。
notifyAll():唤醒全部处于wait状态下的线程。
7、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,其它线程也不能用)。
8、如何停止一个正在运行的线程
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止(不推荐,方法已作废)。
- 使用interrupt方法中断线程
-
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
- 打断正常线程,可以根据打断状态来标记是否退出线程。
二、线程中并发安全
1、Synchronized 互斥锁
Synchronized 对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁,其它线程再获取这个对象锁时就会阻塞住。
Monitor
Monitor被翻译为监视器,是由jvm提供,c++语言实现。
Owner:存储当前获取锁的线程,只能有一个线程可以获取
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程。
WaitSet: 关联调用了wait方法的线程,处于waiting状态的线程。
2、Synchronized 关键字的底层原理。
基础
- Synchronized[对象锁]采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。
- 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor。
- 在monitor内部有三个属性,分别是owner、entrylist、waitset。
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist管理的是处于阻塞状态的线程,waitset是线程调用wait(),处于waiting状态的线程。
进阶
- Monitor实现的锁属于重量级锁,锁升级过程。
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能较低。
- 在JDK1.6引入了两种新型锁机制: 偏向锁和轻量级锁,它们的引入是为了在没有多线程竞争或基本没有竞争的场景下因使用传统所机制带来的性能开销问题。
3、对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。
MarkWord
4、Monitor重量级锁
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Wrod中就被重置指向Monitor对象的指针。
5、轻量级锁
在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁都是没必要的。因此JVM引入了轻量级锁的概念。
1、加锁流程
1)在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2)通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3)如果是当前线程已经持有了该锁了,代表这是一次锁重入了。设置Lock Record第一部分为null,起到了一个重入计数的作用
4)如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
2、解锁过程
1)遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2)如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3)如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
6、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
7、Monitor实现的锁属于重量级锁,锁的升级过程?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有,不同线程交替持有锁、多线程竞争锁三种情况。
8、Java内存模型(JMM)
JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则规范对内存的读写操作从而保证指令的正确性。
谈谈JMM(Java内存模型)
JMM java内存模型,定义了共享内存中多线程读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)。
线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存。
9、CAS
CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作。
AbstractQueuedSynchronizer(AQS框架)
AtomicXXX类
10、CAS数据交换流程
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功
因为没有加锁,所以线程不会陷入阻塞,效率较高
如果竞争激烈,重试频繁发生,效率会受影响
11、乐观锁和悲观锁
CAS是基于乐观锁的思想:最乐观的设想,不怕别的线程来修改共享变量,就算改了也没关系,自旋重试。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,上锁之后谁都不能修改。
12、volatile的理解
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具有两层含义
1)保证线程间的可见性
2)禁止进行指令重排序
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
13、谈谈你对volatile的理解
问题分析:主要是因为JVM虚拟机中有一个JIT(即时编译器)给代码做了优化。
解决方案一:在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失。
解决方案二:在修饰stop变量的时候加上volatile,当前搞死JIT,不对volatile修饰的变量进行优化。
volatile禁止指令重排序
用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,组织其他读写操作越过屏障,从而达到阻止重排序的效果。
volatile使用技巧:
- 写变量让volatile修饰的变量在代码最后位置
- 读变量让volatile修饰的变量在代码最开始位置
14、什么是AQS
全程是AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
AQS与Synchronized的区别
synchronized | AQS |
关键字,c++语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
AQS-基本工作机制
AQS是否是公平锁:
新的线程与队列中的线程共同来抢资源,是非公平锁。
新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。
15、ReentrantLock的实现原理
ReentrantLock翻译过来就是可重入锁,相对于synchronized具备以下特点:
- 可中断。
- 可以设置超时时间。
- 可以设置公平锁。
- 支持多个条件变量。
- 与synchronized一样,都支持重入。
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
构造方法接收一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在多线程访问的情况下,公平锁表现出较低的吞吐量。
1、ReentranLock的实现原理。
ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用lock,是不会再阻塞。
ReentrantLock主要利用CAS+AQS队列来实现。
支持公平锁和非公平锁,在提供的构造器中无参数默认是非公平锁,可以传参数设置为公平锁。
2、synchronized 和 Lock有什么区别
语法层面
synchronized是关键字,源码在jvm中,用c++语言实现。
Lock是接口,源码由jdk提供,用java语言实现。
使用synchronized时,退出同步代码块会自动释放,而使用Lock时,需要手动调用unlock方法。
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。
Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量。
Lock有适合不同场景的实现,如ReentrantLock, ReentrantReadWriteLock(读写锁)。
性能层面
在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不错。
在竞争激烈时,Lock的实现通常会提供更好的性能。
16、死锁产生的条件是什么
死锁:一个线程需要同时获取多把锁,这时容易产生死锁。
线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A锁。
1、如何进行死锁诊断
当程序出现死锁现象,可以使用jdk自带的工具:jps和jstack
jps:输出JVM中运行的进程状态信息。
jstack:查看java进程内线程的堆栈信息。
1.2 其它解决工具,可视化工具
jconsole
用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能检测工具。
打开方式:java安装目录bin目录下 直接启动jconsole.exe就行。
VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。
打开方式:java安装目录bin目录下 直接启动 jvisualvm.exe就行。
17、聊一下ConcurrentHashMap
ConcurrentHashMap是一种线程安全的高效Map集合。
底层数据结构:
JDK1.7 底层采用分段的数组+链表实现。
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
1、JDK1.7中ConcurrentHashMap
2、JDK1.8中ConcurrentHashMap
18、导致并发程序出现问题的根本原因
Java并发编程的三大特性
- 原子性
- 可见性
- 有序性
原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行。
不是原子操作,怎么保证原子操作呢?
- synchronized : 同步加锁
- JUC里面的lock:加锁
内存可见性:让一个线程对共享变量的修改对另一个线程可见。
解决方案: synchronized、volatile、LOCK
有序性:指令重排(JIT),处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
解决方案:volatile
三、线程池
1、说一下线程池的核心参数
corePoolSize:核心线程数目
maximumPoolSize:最大核心线程数目 maximumPoolSize = 救急线程 + 核心线程
keepAliveTime:生存单位时间
TimeUnit:时间单位 毫秒、秒
BlockingQueue<Runnable>:当没有空闲核心线程时,新来的任务会加入到此队列排队,队列满会创建救急线程执行任务。
ThreadFactory:线程工厂 -可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
RejectedExecutionHandler:当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略。
2、线程池的执行原理
3、线程池中哪些常见的阻塞队列
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
- LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
- DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的。
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出
4、ArrayBlockingQueue与LinkedBlockingQueue的区别
5、如何确定核心线程数
IO密集型任务
一般来说:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1
CPU秘籍型任务
一般来说:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1.
6、线程池的种类有哪些
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有4种。
1、创建使用固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreds
,0L
,timeUnit.MILLSECONDS
,new LinkedBlockingQueue<Runnable>);
}
核心线程数与最大线程数一样,没有临时线程。
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE。
适用于任务量已知,相对耗时的任务。
2、单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行。
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegateExecutorService(
new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLSECONDS,new LinkedBlockingQueue<Runnable>()
);
}
适用于按照顺序执行的任务。
3、可缓存线程池
public static ExecutorService newCachedThredPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.Seconds,
new SynchronousQueue<Runnable>);
}
核心线程数为0
最大线程数是Integer.MAX_VALUE
阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
适合任务数比较密集,但每个任务执行时间较短的情况。
7、为什么不建议用Executors创建线程池
参考阿里开发手册《Java开发手册-嵩山版》
四、使用场景
1、线程池使用场景(CountDownLatch、Future)
项目使用到了多线程
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)。
- 其中构造参数用来初始化等待计数值。
- await() 用来等待计数归零
- countDown() 用来让计数减一