1、Java实现多线程的方法
创建线程的常用方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池方式创建
说明:
通过继承 Thread 或 实现 Runnable接口,Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常。
采用实现接口方式创建线程的优缺点
- 优点: 线程类只是实现了Runnable或者Callable接口,还可以继承其他类。这种方式下,多个线程共享一个target对象,非常适合多个线程处理同一份资源,从而可以将CPU、代码和数据分开,形成清晰模型,较好体现面向对象的思想。
- 缺点: 编程稍微复杂一些,如果需要访问当前线程,需要使用
Thread.currentThread()
方法。
采用继承类方式创建线程的优缺点
- 优点: 编写简单,若需要访问当前线程,使用this即可获取当前线程。
- 缺点: 线程类已经继承了Thread类,Java语言是单继承,所以无法再继承其它父类了。
2、如何停止一个正在运行的线程
- (1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。(主要是方法体结束)
- (2)使用stop方法强行终止,但不推荐这个方法。
- (3)使用 interrupt 方法中断线程
java
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while(!stop) {
try {
sleep(1000);
} catch (InterruptedException e) {
stop = true; //修改共享变量的状态
}
}
}
}
java
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
m1.start();
Thread.sleep(3000);
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
}
}
3、notify 和 notifyAll 的区别
(1)特点:
- notify可能会导致死锁;notifyAll则不会
- notifyall唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中;notify只能唤醒一个
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码。
(2)wait()使用方式
wait() 应配合while循环使用,务必在 wait() 调用前后都检查条件,若不满足条件,必须调用notify() 唤醒另外的线程来处理,直至条件满足再往下执行。
(3)notify 的使用场景
notify() 是对 notifyAll() 的一个优化,使用正确精准场景是:WaitSet是一个可处理的等待任务列表,用户任务会放入WaitSet,唤醒任意一个线程都能正确处理任务,当线程无法正确处理时,则任务返回WaitSet同时notify下一个线程。
4、sleep 和 wait 的区别
(1)所属父类的区别
- sleep() 属于 Thread 类
- wait() 属于 Object 类
(2)方法在线程的执行上的区别
- sleep() 导致程序暂停执行直到睡眠时间过去,在调用过程中,让出cpu该其他线程,但保持监控状态,线程不会释放对象锁。
- wait() 线程会直接放弃对象锁,此对象进入等待锁定池直至notify()本线程,才进入对象锁定池准备,获取对象锁时进入运行状态。
5、volatile 是什么?可以保证有序性吗?
(1)volatile的含义
当一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰之后,具备两层含义:
- 1)保证不同线程对这个变量进行操作时的可见性 ,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile 关键字会强制将修改的值立即写入主存。
- 2)禁止指令重排序(volatile 不是原子性操作)
(2)什么叫保证部分有序性
当程序执行到volatile变量的读操作或写操作时,在其前面的操作的更改肯定全部已经执行完毕,且结果已经对后面的操作可见,而其后面的操作还未执行。
示例
1 x = 2;
2 y = 0;
3 flag = true; //volatile变量
4 x = 4;
5 y = -1;
在指令重排序的过程中,语句3 不会放到 语句1、语句2 之前,也不会放到 语句4、语句5 之后。
注意: 语句1 和 语句2 的顺序,语句4 和 语句5 的顺序是不做任何保证。
(3)volatile 使用场景
一般用于 状态标记量 和 单例模式的双检锁
6、Thread 类中的start 和 run 方法有什么区别?
- start() 用来启动新常见的线程,这时并不会执行方法,而是在CPU调度到该线程时才会继续执行方法,在内部调用了 run() 方法。
- run() 在原来的线程中调用,能立即运行线程方法。
7、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
(1)Java提供的锁是对象级的,每个对象都有锁,通过线程获取对象锁。
(2)当线程需要等待某些锁,那么调用wait方法就有意义了。如果wait定义在Thread中,线程正在等待的锁是不明显的。
简单回答: 因为Java提供的锁属于对象锁,并且wait、notify、notifyAll是锁级别操作,所以定义在Object中。
8、为什么wait和notify方法要在同步块中调用?
(1)只有调用线程拥有某个对象的独占锁时,才能调用 wait 、 notify 和 notifyAll 方法。
(2)如果不调用 wait 和 notify 方法,则会抛出 IllegalModitorStateException 异常。
(3)避免wait和notify产生竞争状态条件。
wait() 方法强制释放对象锁,意味着在调用 wait 的时候,当前线程已经获得了对象锁,因此线程必须在同步方法或同步代码块中调用 wait 。
在调用对象的notify 和 notifyAll 之前,调用线程必须得到对象锁,因此线程必须在同步方法或同步代码块中调用 notify 和 notifyAll 。
- **wait 的使用场景是:**调用线程希望某个状态(或变量)被设置后继续执行。
- notify 和 notifyAll 的使用场景: 调用线程希望告知其它等待的线程"特殊状态已被设置"。这个状态作为线程间通信的通道,它必须是一个可变共享状态(或变量)。
9、interrupted 和 isInterrupted的区别
interrupted() 和 **isInterrupted()**的主要区别:前者会将中断状态清除而后者不会。
Java多线程的中断机制是用内部标识实现的,调用 Thread.interrupt 来中断一个线程并设置中断标识为 true。
当中断线程调用静态方法 Thread.intuerrupted 检查中断状态时,中断状态会被清除。
当中断线程调用非静态方法 isIntuerrupted 检查中断状态时,中断状态不会被清除。
10、synchronized 和 ReentrantLock的区别
(1)相似点
- 都是加锁同步,且都是阻塞式同步。
(2)区别:
- Synchronized,它是Java的关键字,在原生语法层面的互斥,需要JVM实现。
- ReentrantLock,是JDK 1.5之后提供API层面的互斥锁,需要 lock() 和 unlock() 配合 try/finally完成。
(3)Synchronized执行原理:
Synchronized 经过编译,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。
在执行 monitorenter 指令时,首先尝试获取对象锁,如果这个对象没被锁定,或者当前线程已经拥有了对象锁,把锁的计算器加1。
在执行monitorexit指令时,会将锁计算器减1。
当计算器为0时,锁将释放,若获取对象锁失败,则当前线程阻塞,直至对象锁被释放。
(4)ReentrantLock锁的功能
由于ReentrantLock是java.util.concurrent包提供的一套互斥锁,提供了一些高级功能,主要有以下3项:
- 等待可中断: 持有锁的线程长期不释放时,正在等待的线程可以放弃等待,避免死锁的情况。
- 公平锁: 多线程等待同一个锁时,必须按照申请锁的时间获取锁,Synchronized锁是非公平锁,ReentrantLock默认的构造函数是常见非公平锁,可以通过参数设为公平锁,但公平锁的性能不是很好。
- 锁绑定多对象: 一个ReentrantLock对象可以同时绑定多个对象。
11、线程T1,T2,T3如何保证顺序执行
线程类的 join() 方法,在一个线程中启动另一个线程,另一个线程完成启动,该线程继续执行。(T3调用T2,T2调用T1完成顺序执行)实际上,run方法中用join方法限定了三个线程的执行顺序。
无论三个线程如何排序开启,都会以T1,T2,T3的顺序执行。
java
public class ThreadTest{
public static void main(String[] args) throws CloneNotSupportedException {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
System.out.println("t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
final Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
System.out.println("t3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
t1.start();
t3.start();
}
}
/*结果*/
t1
t2
t3
12、SynchronizedMap和ConcurrentHashMap有什么区别?
- SynchronizedMap 与 HashTable 一致,实际上调用map所有方法,都对整个map进行同步。
- ConcurrentHashMap 的实现却更加精细,它对map中的所有桶加了锁。
所以 SynchronizedMap 在一个线程访问时,其它线程则不能访问该Map,ConcurrentHashMap 在一个线程访问Map的一个桶时,其它线程依旧可以访问map的其它桶。
所以,ConcurrentHashMap在性能以及安全性上,明显比Collections.synchronizedMap() 更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其它线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
13、什么是线程安全?
(1)概念
线程安全是指多线程访问同一段代码时,不会产生不确定的结果。
如果你的代码在多线程下执行和在单线程下执行永远都能获得同一个结果,那么这个代码是线程安全的。
(2)线程安全的级别:
- 不可变: 像String、Integer、Long这些,都是final类型的类,任何一个线程都无法改变它们的值,如果要改变则需要新创建一个对象,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
- 绝对线程安全: 不管运行环境如何,调用者不需要额外的同步措施。要做到这一点,通常需要付出额外的代价,Java中标注自己是线程安全的类,实际上绝大多数是线程不安全的,不过绝对线程安全的类,Java中也有,例如CopyOnWriteArrayList、CopyOnWriteArraySet。
- 相对线程安全: 相对线程安全也就是我们通常意义上的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此。如果有个线程在遍历某个Vector,有个线程同时在add这个Vector,大多数会出现ConcurrentModificationException,也就是fail-fast机制。
- 线程非安全: ArrayList、LinkedList、HashMap等大都是线程非安全的类。
14、Thread类中的yield方法有什么作用?
yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。
它是一个静态方法,而且只保证当前线程放弃CPU占用而不能保证其它线程一定能占用CPU,执行 yield() 的线程有可能在进入到暂停状态后马上又被执行。
@[TOC](15、Java线程池中submit() 和 execute()方法有什么区别?)
**(1)作用:**两个方法都可以向线程池提交任务。
- execute(): 返回类型是void,定义在Executor接口中。
- submit(): 可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
16、说一说自己对于 synchronized 关键字的了解
(1)synchronized关键字解决的是多线程访问资源的同步性,可以保证修饰的方法或者代码块在任意时刻只有一个线程执行。
(2)Java 6 之前版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现,Java线程是映射到OS的原生线程上的。如果要挂起或者唤醒一个线程,都需要OS帮忙完成,而OS实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本高。Java 6 之后版本JVM优化了synchronized 锁,提供了一些优化技术如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量锁等技术来减少锁操作的开销。
17、说说自己是怎么使用 synchronized 关键字?
(1)修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
(2)修饰静态方法: 作用于当前类,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是一个类成员。如果一个线程A调用一个实例对象的非静态同步锁方法,而线程B需要调用这个实例对象所属类的静态同步锁方法是允许的,不会发生互斥现象。
(3)修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结:
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是对象实例上锁。
synchronized(String a) 尽量不使用,因为JVM中字符串常量池具有缓存功能。
18、Vector是一个线程安全类吗?
Vector 是用同步方法来实现线程安全的,而和它相似的ArrayList不是线程安全的。
20、常用的线程池有哪些?
- newSingleThreadExecutor: 创建一个单线程的线程池,该线程池可以保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool: 常见固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
- newCachedThreadPool: 创建一个可缓存的线程池,此线程池不会对线程池大小做现职,线程池大小完全依赖于OS 或 JVM 能够创建的最大线程大小。
- newScheduledThreadPool: 创建一个大小无限的线程池,支持定时以及周期性执行任务的需求。
21、简述一下你对线程池的理解
(1)线程池的使用
(2)线程池的好处
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的损耗。
- 提高响应速度: 当任务到达时,能立刻执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅消耗系统资源,也会降低系统的稳定性,使用线程池可以进行统一的管理,调优和监控。
(3)线程池的启动策略
22、Java程序是如何执行的
- (1)Java 代码编译成字节码,.java 文件编译成 .class 文件。大致执行过程是:java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字节码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败。
- (2)把 class 文件放置到JVM。
- (3)JVM 使用类加载器(Class Loader)装载 class 文件。
- (4)类加载完成后进行字节码校验,字节码校验通过后 JVM 解释器将字节码翻译成机器码交给 OS。
23、锁的优化机制
从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁 、自旋锁 、锁消除 、锁粗化 、轻量级锁 和偏向锁。
锁的状态从低到高:无锁 》 偏向锁 》 轻量锁 》 重量锁,升级过程也是从低到高,降级在一定条件下也可能发生。
- 自旋锁: 由于大部分时间内,锁占用的时间很短,共享变量的锁定时间也很短,所以没必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念是让一个线程执行一个空循环,防止用户态转入内核态,自旋锁可以通过设置 -XX:+UseSpining 开启,自旋的默认次数是10次,可以使用 -XX:PreBlockSpin 设置。
- 自适应锁: 自适应锁就是自适应的自旋锁,自旋的时间不固定,而是由前一次在同一个锁的自旋时间和锁的持有者状态来决定。
- 锁消除: 锁消除指的是JVM检测到一些同步代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
- 锁粗化: 指的是很多操作都是对同一个对象进行加锁,将锁的同步范围扩展到整个操作序列之外。
- 偏向锁: 当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁,偏向锁永远偏向第一个获取锁的线程。如果没有其它线程获取锁,持有锁的线程就永远不需要进行同步,反之,当有线程竞争偏向锁时,持有偏向锁的线程会释放偏向锁,通过 -XX:UseBiaseLocking 开启偏向锁。
- 轻量锁: JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM会使用CAS方式来尝试获取锁,如果更新成功则会把对象头的状态为标记为轻量锁,如果更新失败,则尝试自旋来获得锁。
简单来说,偏向锁是通过对象头的偏向线程ID来对比,甚至不需要CAS,而轻量锁主要是通过CAS修改对象头锁记录和自旋来实现,重量锁是除拥有锁的线程,其它线程全部阻塞。
锁的优化机制图
24、说说进程和线程的区别?
- 进程是一个执行中的程序,是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其他资源(所以通信和同步等操作线程比进程更加容易)
- 线程上下文切换比进程上下文切换更快
- 进程切换时,涉及到当前进程的CPU环境保存和新调度运行进程的CPU环境设置
- 线程切换,只需要保存和设置少量寄存器内容,不涉及存储管理方面的操作。
25、产生死锁的四个必要条件?
- (1)互斥条件: 一个资源每次只能被一个线程使用
- (2)请求与保持条件: 一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- (3)不剥夺条件: 进程已经获得的资源,在未使用完之前,不能强行剥夺。
- (4)循环等待条件: 若干线程之间行测好难过头尾相接的循环等待资源关系。
26、如何避免死锁?
比如某个线程只有获得A锁和B锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
答: 获得锁的顺序是一定的,比如规定,只有获得A锁的线程才能获取B锁,按顺序获取锁就可以避免死锁。
27、线程池核心线程数怎么设置呢?
分为 CPU密集型 和 IO密集型
CPU型
这种任务消耗主要是 CPU 资源,可以将线程数设置为 N(CPU核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
IO密集型
这种任务应用起来,系统会用大部分时间来处理I/O交互,而线程在处理IO的时间段内不会占用 CPU 处理,这时就可以将 CPU 交出给其他线程使用。
所以在 IO 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是:核心线程数 = CPU核心数量*2
28、Java线程池中队列常用类型有哪些?
- ArrayBlockingQueue: 是一个基于数据结构的有界阻塞队列,此队列 FIFO 原则进行排序。
- LinkedBlockingQueue: 一个基于链表的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。
- SynchronousQueue: 一个不存储元素的阻塞队列。
- PriorityBlockingQueue: 一个具有优先级的无限阻塞队列,是基于最小二叉堆实现。
- DelayQueue: 一个没有大小限制的队列,只有延迟时间到期,才能从队列中获取元素,在队列中插入数据的操作永远不会被阻塞,而只有获取元素的操作才会被阻塞。
29、线程安全需要保证几个基本特征?
- 原子性: 相关操作中途不被其它线程干扰,一般通过同步机制实现。
- 可见性: 一个线程修改了某个共享变量,其状态能立刻被其它线程感知,通常被解释为将线程本地状态反应到主内存上,volatile 负责保证可见性(强制写入主内存)。
- 有序性: 保证线程内串行语义,避免指令重排序。
30、说一下线程之间是如何通信的?
两种方式:共享内存 和 消息传递
(1)共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
典型的共享内存通信方式,就是通过共享对象进行通信。
通信步骤
- 线程 A 把本地内存更新过得共享变量刷新到内存中。
- 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
(2)消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
在Java 中典型的消息传递方式,就是 wait
、 notify
,或者 BlockingQueque
。
31、CAS的原理
CAS(CompareAndSwap),比较并交换,主要是通过处理器的指令来保证操作的原子性,包含三个操作数:
- 变量内存地址,V
- 旧的预期值,O
- 准备设置的新值,N
当执行CAS的时候,只有V == O 时,才会用 N 去更新 V 的值,否则就不执行更新操作。
31、CAS的缺点
- (1)ABA问题: 在CAS更新的过程中,当读取到的值是A,然后准备复制的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA,只是ABA的问题大部分场景下不影响并发的最终效果。
Java中有AtomicStampedReference
来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不只检查值,还检查当前的标志是否等于预期标志,全部相等才能更新。 - (2)循环时间长,开销大: 自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。(一直循环,占用CPU执行)
- (3)只能保证一个共享变量的原子操作: 只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过
AtomicStampedReference
来处理或者使用锁synchronized
实现。
32、引用类型有哪些?有什么区别?
主要是分为**"强软弱虚"**四种
- 强引用: 普遍存在的赋值方式,永远不会被GC回收。
- 软引用: 使用SoftReference来描述,指的是那些有用但是不是必须要的对象,例如临时对象,我们创建的匿名类或匿名方法等等,GC会在内存溢出前回收。
- 弱引用: 可以用WeakReference来描述,GC检测到一定会被回收。
- 虚引用: 称为幻影引用,用 PhantomReference 描述,它必须和 ReferenceQueque 一起使用,同样当发生GC时,虚引用也被回收,可用虚引用来管理堆外内存。
33、说说ThreadLocal原理?
ThreadLocal,称为线程本地变量,在每个线程都创建一个副本,线程之间访问内部副本变量即可,做到了线程之间互相隔离,相比 synchronized 是 空间换时间。
ThreadLocal有个静态内部类 ThreadLocalMap,ThreadLocalMap 包含了一个 Entry 数组,Entry本身是一个弱引用,他的key是指向 ThreadLocal 的弱引用,Entry具备保存 key-value键值对的能力。
弱引用的目的是为了防止内存泄漏,如果是强引用,那么ThreadLocal对象除非线程结束,否则无法被回收,弱引用会在下次GC时回收。
当ThreadLocal 弱引用被GC回收了,还会存在内存泄漏问题,如果key和ThreadLocal被回收之后,entry中就存在key为null,但是value有值的entry,但是无法访问,直到线程结束。
只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上不会出现这个问题。
34、线程池原理知道吗?以及核心参数
核心参数:
- 最大线程数 maximumPoolSize
- 核心线程数 corePoolSize
- 活跃时间 keepAliveTime
- 阻塞任务队列 workQueue
- 拒绝策略 RejectedExecutionHandler
执行流程:
- 提交任务,线程池根据核心线程数大小创建若干个任务数量线程执行任务
- 当任务数量超过核心线程数时,后续任务进入阻塞队列排队。
- 当阻塞队列满了之后,那么将会继续创建(最大线程数-核心线程数)个数量的临时线程来执行任务,如果任务完成后,临时线程经过keepAliveTime之后销毁。
- 如果达到了最大线程数之后,且阻塞队列满的状态,则根据不同的拒绝策略处理。
35、线程池的拒绝策略有哪些?
- (1)AbortPolicy: 直接丢弃任务,并抛出异常(默认)。
- (2)CallerRunsPolicy: 只用调用者所在线程进行处理。
- (3)DiscardOldestPolicy: 丢弃等待队列中最久的任务,并执行当前任务。
- (4)DiscardPolicy: 直接丢弃任务,且不抛出异常。
36、说说你对JMM内存模型的理解?为什么需要JMM?
(1)JMM的由来
CPU和内存之间速度产生了差异,所以CPU加入了告诉缓存,一般分为三级缓存,会产生缓存一致性问题,所以需要加入缓存一致性协议,同时导致了内存可见性问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,通过JMM才能屏蔽不同硬件和OS内存的访问差异,这样保证了Java程序在不同的平台下达到一直的内存访问效果,同时保证了高效并发的时候,程序能够正确执行。
(2)JMM的特性:
- 原子性: Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有 lock 和 unlock,直接对应着synchronized 关键字的 monitorenter和monitorexit 字节码指令。
- 可见性: Java保证可见性可以认为通过 volatile、synchronized、final来实现。
- 有序性: 由于CPU和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
(3)happen-before规则
虽然指令重排提高了并发性能,但JVM会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行顺序,主要有以下几点:
- 单线程每个操作: happen-before于该线程中任意后续操作。
- volatile: 写happen-before于后续对这个变量的读
- synchronized解锁: happen-before 后续对这个锁的加锁
- final变量的写: happen-before 于final域对象的读或是happen-before后续对final变量的读。
- 传递性规则: A先于B,B先于C,那么A一定先于C发生
那么 工作内存和主要内存到底是什么?
- 主内存一般认为是物理内存,JMM中实际是虚拟机内存的一部分。
- 工作内存就是CPU缓存,有可能是寄存器,也可能是三级缓存。
37、多线程有什么用?
多线程为什么要用?
- (1)发挥多核CPU的优势
- (2)防止阻塞
- (3)便于建模
一个大型任务A,在单线程中,需要考虑整个程序建模的复杂度,若将任务A分解出几个小任务,并分别建立程序模型,通过多线程分别运行这个几个任务,能够下降建模难度。
38、说说CyclicBarrier和CountDownLatch的区别?
CyclicBarrier 和 CountDownLatch 都是 java.util.concurrent 下,都可以用来表示代码运行到某个点上。
- (1)CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有线程都到达了这个点,所有线程才重新运行;CountDownLatch 在某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
- (2)CyclicBarrier 只能唤起一个任务CountDownLatch可以唤起多个任务
- (3)CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为0该CountDownLatch就不可再用了
39、什么是AQS?
AQS(AbstractQueuedSychronizer)抽象队列同步器
如果说 java.util.concurrent 的基础是CAS的话,那么AQS就是整个Java并发包的核心,ReentrantLock、CountDownLatch、Semaphore等等都用到了它,AQS实际上以双向队列的形式连接所有Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的海鲜重写tryLock和tryRelease方法,以实现自己的并发功能。
40、了解Semaphore吗?
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore 有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕,下一个线程再进入。
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个 synchronized了。
41、什么是Callable和Future?
(1)Callable接口
一种类似于Runnable的任务接口,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值,可以认为是带有回调的Runnable。
(2)Future接口
表示异步任务,是还没有完成的任务给出的未来结果,所以说Callable用于产生结果,Future用于获取结果。
42、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
(1)阻塞队列(BlockQueue),是一个支持两个附加操作的队列,两个附加操作是:
- 队列空,获取元素的线程会等待队列变为非空
- 队列满,存储元素的线程等待队列可用。
(2)应用场景: 阻塞队列常用于生产者和消费者的场景,阻塞队列 就是生产者存放元素的容器,而消费者也只从容器里拿元素。生产者 是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
(3)阻塞队列类型
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
(4)阻塞队列实现生产者-消费者
通过创建两个线程:生产者线程 和消费者线程。
BlockingQueue接口是Queue的子接口,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好的控制线程之间的通信。
(5)阻塞队列的经典使用场景
阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。
43、什么是多线程中的上下文切换?
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
在程序中,上下文切换过程中的"页码"信息是保存在进程控制块(PCB)中的。PCB还经常被称
作"切换桢"(switchframe)。"页码"信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
上下文切换是多任务操作系统和多线程环境的基本特征。
44、什么是Daemon线程?它有什么意义?
后台(daemon )线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中必需的部分。
当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
注意:后台进程在不执行finally子句的情况下就会终止其run()方法。
比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程
45、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
(1)概念:
- 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁 ,表锁 等,读锁 ,写锁 等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。 - 乐观锁: 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
(2)乐观锁的实现
- 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
- java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。