现在的面试对程序员要求越来越高,基础越扎实拿offer的概率就越高,大厂对基础还是很执着的,如果你基础功底好,面试官是会看中你的潜力,然后去培养的你的。
除了算法、网络、计算机基础等,多线程越来越被注重。废话不多说,直接把题分享给大家。
1.什么是进程?
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
2.什么是线程?
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
3.线程的实现方式?
1.继承Thread类
2.实现Runnable接口
3.使用Callable和Future
4.Thread 类中的start() 和 run() 方法有什么区别?
1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。 2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
5.线程NEW状态
new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动改线程,那么此时的状态就是新建状态。
6.线程RUNNABLE状态
线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。
7.线程的RUNNING状态
一旦cpu通过轮询货其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。
8.线程的BLOCKED状态
线程正在等待获取锁。
- 进入BLOCKED状态,比如调用了sleep,或者wait方法
- 进行某个阻塞的io操作,比如因网络数据的读写进入BLOCKED状态
- 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态
9.线程的TERMINATED状态
TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。
下面几种情况会进入TERMINATED状态:
- 线程运行正常结束,结束生命周期
- 线程运行出错意外结束
- JVM Crash 导致所有的线程都结束
10.线程状态转化图
11.i——与System.out.println()的异常
示例代码:
public class XkThread extends Thread {
private int i = 5;
@Override
public void run() {
System.out.println("i=" + (i——————) + " threadName=" + Thread.currentThread().getName());
}
public static void main(String[] args) {
XkThread xk = new XkThread();
Thread t1 = new Thread(xk);
Thread t2 = new Thread(xk);
Thread t3 = new Thread(xk);
Thread t4 = new Thread(xk);
Thread t5 = new Thread(xk);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
复制代码
结果:
i=5 threadName=Thread-1
i=2 threadName=Thread-5
i=5 threadName=Thread-2
i=4 threadName=Thread-3
i=3 threadName=Thread-4
复制代码
虽然println()方法在内部是同步的,但i——————的操作却是在进入println()之前发生的,所以有发生非线程安全的概率。
println()源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
复制代码
12.如何知道代码段被哪个线程调用?
System.out.println(Thread.currentThread().getName());
复制代码
13.线程活动状态?
public class XKThread extends Thread {
@Override
public void run() {
System.out.println("run run run is " + this.isAlive() );
}
public static void main(String[] args) {
XKThread xk = new XKThread();
System.out.println("begin ——— " + xk.isAlive());
xk.start();
System.out.println("end ————— " + xk.isAlive());
}
}
复制代码
14.sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程”休眠(暂停执行)。
15.如何优雅的设置睡眠时间?
jdk1.5 后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。
比如要表达2小时22分55秒899毫秒。
Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
复制代码
可以看到表达的含义更清晰,更优雅。
16.停止线程
run方法执行完成,自然终止。
stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。
大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记, 此方法不回去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。
17.interrupted 和 isInterrupted
interrupted : 判断当前线程是否已经中断,会清除状态。
isInterrupted :判断线程是否已经中断,不会清除状态。
18.yield
放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。
测试代码:(cpu独占时间片)
public class XKThread extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时 = " + (endTime - beginTime) + " 毫秒! ");
}
public static void main(String[] args) {
XKThread xkThread = new XKThread();
xkThread.start();
}
}
复制代码
结果:
用时 = 20 毫秒!
复制代码
加入yield,再来测试。(cpu让给其他资源导致速度变慢)
public class XKThread extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
Thread.yield();
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时 = " + (endTime - beginTime) + " 毫秒! ");
}
public static void main(String[] args) {
XKThread xkThread = new XKThread();
xkThread.start();
}
}
复制代码
结果:
用时 = 38424 毫秒!
复制代码
19.线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。
Java的优先级分为1~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:java.lang.IllegalArgumentException。
20.优先级继承特性
线程的优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。
21.谁跑的更快?
设置优先级高低两个线程,累加数字,看谁跑的快,上代码。
public class Run extends Thread{
public static void main(String[] args) {
try {
ThreadLow low = new ThreadLow();
low.setPriority(2);
low.start();
ThreadHigh high = new ThreadHigh();
high.setPriority(8);
high.start();
Thread.sleep(2000);
low.stop();
high.stop();
System.out.println("low = " + low.getCount());
System.out.println("high = " + high.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadHigh extends Thread {
private int count = 0;
public int getCount() {
return count;
}
@Override
public void run() {
while (true) {
count++;
}
}
}
class ThreadLow extends Thread {
private int count = 0;
public int getCount() {
return count;
}
@Override
public void run() {
while (true) {
count++;
}
}
}
复制代码
结果:
low = 1193854568
high = 1204372373
复制代码
22.线程种类
Java线程有两种,一种是用户线程,一种是守护线程。
23.守护线程的特点
守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。
24.Java中典型的守护线程
GC(垃圾回收器)
25.如何设置守护线程
Thread.setDaemon(true)
PS:Daemon属性需要再启动线程之前设置,不能再启动后设置。
25.Java虚拟机退出时Daemon线程中的finally块一定会执行?
Java虚拟机退出时Daemon线程中的finally块并不一定会执行。
代码示例:
public class XKDaemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(),"xkDaemonRunner");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
SleepUtils.sleep(10);
} finally {
System.out.println("Java小咖秀 daemonThread finally run …");
}
}
}
}
复制代码
结果:
复制代码
没有任何的输出,说明没有执行finally。
26.设置线程上下文类加载器
获取线程上下文类加载器
public ClassLoader getContextClassLoader()
复制代码
设置线程类加载器(可以打破Java类加载器的父类委托机制)
public void setContextClassLoader(ClassLoader cl)
复制代码
27.join
join是指把指定的线程加入到当前线程,比如join某个线程a,会让当前线程b进入等待,直到a的生命周期结束,此期间b线程是处于blocked状态。
28.什么是synchronized?
synchronized关键字可以时间一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对想的所有读写都将通过同步的方式来进行。
29.synchronized包括哪两个jvm重要的指令?
monitor enter 和 monitor exit
30.synchronized关键字用法?
可以用于对代码块或方法的修饰
31.synchronized锁的是什么?
普通同步方法 —————> 锁的是当前实力对象。
静态同步方法—————> 锁的是当前类的Class对象。
同步方法快 —————> 锁的是synchonized括号里配置的对象。
32.Java对象头
synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,用2字宽存储对象头。
Tips:32位虚拟机中一个字宽等于4字节。
33.Java对象头长度
34.Java对象头的存储结构
32位JVM的Mark Word 默认存储结构
35.Mark Word的状态变化
Mark Word 存储的数据会随着锁标志为的变化而变化。
64位虚拟机下,Mark Word是64bit大小的
36.锁的升降级规则
Java SE 1.6 为了提高锁的性能。引入了“偏向锁”和轻量级锁“。
Java SE 1.6 中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
锁只能升级不能降级。
37.偏向锁
大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 cas操作来加锁和解锁,只需测试一下对象头 Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1(表示当前时偏向锁),如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向锁只想当前线程。
38.关闭偏向锁延迟
java6和7中默认启用,但是会在程序启动几秒后才激活,如果需要关闭延迟,
-XX:BiasedLockingStartupDelay=0。
39.如何关闭偏向锁
JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
Tips:如果你可以确定程序的所有锁通常情况处于竞态,则可以选择关闭。
40.轻量级锁
线程在执行同步块,jvm会现在当前线程的栈帧中创建用于储存锁记录的空间。并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用cas将对象头中的Mark Word替换为之乡锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
41.轻量锁的解锁
轻量锁解锁时,会使原子操作cas将 displaced Mark Word 替换回对象头,如果成功则表示没有竞争发生,如果失败,表示存在竞争,此时锁就会膨胀为重量级锁。
42.锁的优缺点对比
43.什么是原子操作
不可被中断的一个或一系列操作
44.Java如何实现原子操作
Java中通过锁和循环cas的方式来实现原子操作,JVM的CAS操作利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
45.CAS实现原子操作的3大问题
ABA问题,循环时间长消耗资源大,只能保证一个共享变量的原子操作
46.什么是ABA问题
问题:
因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检测时会发现发的值没有发生变化,其实是变过的。
解决:
添加版本号,每次更新的时候追加版本号,A-B-A —> 1A-2B-3A。
从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。
47.CAS循环时间长占用资源大问题
如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。
一、它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,有些处理器延迟时间是0。
二、它可以避免在退出循环的时候因内存顺序冲突而引起的cpu流水线被清空,从而提高cpu执行效率。
48.CAS只能保证一个共享变量原子操作
一、对多个共享变量操作时,可以用锁。
二、可以把多个共享变量合并成一个共享变量来操作。比如,x=1,k=a,合并xk=1a,然后用cas操作xk。
Tips:java 1.5开始,jdk提供了AtomicReference类来保证饮用对象之间的原子性,就可以把多个变量放在一个对象来进行cas操作。
49.volatile关键字
volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性“。
Java语言规范第3版对volatile定义如下,Java允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为volatile,Java线程内存模型所有线程看到这个变量的值是一致的。
50.等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
51.wait
方法wait()的作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入“预执行队列”中,并在 wait()所在的代码处停止执行,直到接到通知或中断为止。
在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁。
52.notify
notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发出 通知 notify,并让它等待获取该对象的对象锁。
53.notify/notifyAll
notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。
54.等待/通知经典范式
等待
synchronized(obj) {
while(条件不满足) {
obj.wait();
}
执行对应逻辑
}
复制代码
通知
synchronized(obj) {
改变条件
obj.notifyAll();
}
复制代码
55.ThreadLocal
主要解决每一个线程想绑定自己的值,存放线程的私有数据。
56.ThreadLocal使用
获取当前的线程的值通过get(),设置set(T) 方式来设置值。
public class XKThreadLocal {
public static ThreadLocal threadLocal = new ThreadLocal();
public static void main(String[] args) {
if (threadLocal.get() == null) {
System.out.println("未设置过值");
threadLocal.set("Java小咖秀");
}
System.out.println(threadLocal.get());
}
}
复制代码
输出:
未设置过值
Java小咖秀
复制代码
Tips:默认值为null
57.解决get()返回null问题
通过继承重写initialValue()方法即可。
代码实现:
public class ThreadLocalExt extends ThreadLocal{
static ThreadLocalExt threadLocalExt = new ThreadLocalExt();
@Override
protected Object initialValue() {
return "Java小咖秀";
}
public static void main(String[] args) {
System.out.println(threadLocalExt.get());
}
}
复制代码
输出结果:
Java小咖秀
复制代码
58.Lock接口
锁可以防止多个线程同时共享资源。Java5前程序是靠synchronized实现锁功能。Java5之后,并发包新增Lock接口来实现锁功能。
59.Lock接口提供 synchronized不具备的主要特性
60.重入锁 ReentrantLock
支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。