⭐什么是进程?什么是线程?
进程是一个程序在系统运行的基本单位,而线程是进程的更小执行单位,又称轻量级进程。一个进程可以产生多个线程,同类线程共享堆和方法区资源,但是每个线程都有自己的程序计数器、本地方法栈、虚拟机栈。
⭐简述线程与进程的关系、区别以及优缺点。
一个进程可以拥有多个线程,每个线程在共享进程的堆和方法区的同时,又有自己的程序计数器、本地方法栈、虚拟机栈。主要区别是:每个进程之间相互独立,而线程不一定,他们可能会相互影响。线程的优点是开销小,缺点是不利于管理,而进程刚好相反。

⭐说说线程的生命周期和状态?
首先线程被创建(new Thread() )处于NEW状态->然后start()进入运行状态RUNNABLE,这个时候线程有可能被wait()进入等待状态,等待状态有俩种 普通等待WAITING和超时等待TIME_WAITING,超时等待时间结束后返回运行状态,如sleep()。还有可能锁被占用,处于阻塞状态BLOCKED。最后就是run()方法运行结束,进入终止状态TERMINATED。
线程是如何被创建的?
一般来说线程可以通过继承Thread类,实现Runnable接口、Callable接口,使用线程池等方法创建,但是严格上来说这些都是属于多线程的使用方法,创建方法只有一种——new Thread().start()创建。
为什么要使用多线程呢?
从硬件上来说:现在的cpu都是多核的,多线程可以被分给不同的核心并行运行,减少了上下文切换的开销。
从需求上来说:现在的系统并发量高,多线程可以提升系统的性能。
⭐使用多线程可能会带来那些问题?
可能会导致内存泄漏、死锁、线程不安全等等
如何判断线程是否是安全的?
有一份数据,多线程同时去访问它,如果该数据不会出现错误、混乱、丢失等问题,线程就是安全的。
⭐什么是死锁,简单聊聊你对死锁的认识?
死锁就是多个线程同时被阻塞,其中一个或全部线程都在等待某一个资源被释放,导致线程一直被阻塞。
产生死锁有四个必要条件:
- 互斥条件:该资源任意时刻只能被一个线程占用
- 请求与保持条件:一个线程因请求资源而被阻塞,却继续占用已有的资源
- 不可剥夺条件:线程在未使用完已有的资源时,不可以被其他线程强制占用,只有自己使用完才会释放
- 循环等待条件:若干线程形成头尾相连的循环等待资源状态
⭐如何检测死锁?
1.使用jps命令
jps
输出:
➜ ~ jps
14736 KotlinCompileDaemon
10720 Launcher
10721 Bootstrap
15462 Jps
3608
15017 Launcher
15019 DeathLockDemo
13471
找到目标线程(DeathLockDemo类)PID
2.
jstack -l 15019
然后输出线程信息,如果有死锁会有下面的提示:
Found one Java-level deadlock:
=============================
"mythread2":
waiting for ownable synchronizer 0x000000076aea4848, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "mythread1"
"mythread1":
waiting for ownable synchronizer 0x000000076aea4878, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "mythread2"
3.结果分析:
线程mythread2在等待正在由mythread1持有的锁0x000000076aea4848。
而线程mythread1同时又在等待正在由mythread2持有的锁0x000000076aea4878。造成了死锁问题。
⭐如何预防和避免死锁?
预防死锁就是破坏死锁的产生条件即可,比如破坏请求与保持条件:一次性申请所有资源,还有破坏不可剥夺条件:线程占有一些资源(没使用完)进一步申请资源,如果被拒绝了,就释放自己的资源等等
避免死锁则可以利用算法去实现,比如银行家算法,提前对资源分配进行估算,使线程进入安全状态。
⭐你知道JMM吗?谈谈你对JMM的理解?
首先JMM是Java内存模型,它是一种规范,定义了JVM与计算机内存的工作方式,把内存分成俩个部分:主内存和工作内存(也就是缓存,属于线程内部),并且规定了操作共享变量时,对其加锁,然后把共享变量复制到工作内存中,处理完后修改工作内存中的值,再写入主内存中,然后解锁。保证了一个线程什么时候和怎么样可以看到其他线程修改过共享变量的值,以及如何同步访问共享变量。
补充说明:一个共享变量如何拷贝到工作内存,又怎么从工作内存同步到主内存中,使用的是八大原子性内存操作。通过内存屏蔽规定了不可以做什么,保障了内存的有序性和可见性。
总结一下JMM主要解决了什么问题:
可见性(volatile)、有序性(ps:保证指令不被重排)、原子性(synchronied)
谈及JMM肯定离不开happens-before规则:
规定了一个线程对共享变量的写操作对其他线程的读操作可见,以及保障执行顺序。
举个例子:A线程在B线程执行前执行,可以写成A happens-before B ,这个时候就要保证可见性,确保B能读到A修改的最新值,然后就是保证顺序性,编译器如何优化,处理器如何重排,都不能影响A先执行,B后执行的顺序。
⭐Volatile关键字:保证了可见性、有序性
如何确保可见性的?又是如何确保有序性的?
将变量声明为volatile后,JVM就知道这个变量共享且不稳定,每次需要从主内存中读取,确保了可见性;
有序性是通过内存屏蔽实现的。
⭐谈谈你对悲观锁和乐观锁的理解?
它们都是一种设计理念,不是真正的锁

悲观锁是最坏的情况下,认为每次访问共享资源时,都需要加锁,其他线程想要拿到这个资源时就被阻塞,一直等到资源被释放。场景:多写场景。(synchronized和ReentrantLock)
乐观锁是最好的情况下,认为每次访问共享资源时,不需要加锁也不需要等待,只需要在提交修改的时候验证对应的资源是否被其他线程修改了(CAS Compare And Swap)。场景:多读场景
synchronized关键字有什么用?
synchronized主要用于解决多线程之间访问资源的同步性问题,确保被其修饰的方法或代码块在任意时刻只能有一个线程执行。
如何使用synchronized?
1.修饰一般方法,锁该对象,一个对象一把锁
synchronized void method() {
//业务代码
}
2.修饰静态方法,锁整个类对象,该类所有对象共用一把锁
synchronized static void method() {
//业务代码
}
3.修饰代码块,锁代码块内容
synchronized(this) {
//业务代码
}
⭐synchrozied底层原理?
synchronized的实现使用monitorenter和monitorexit指令。当线程去获取锁时,先执行monitorenter指令,如果锁的计数器为0,则可以获取锁,获取锁后锁计数器+1,该线程使用完后,执行monitorexit指令,将锁的计数器变成0,释放锁;如果获取锁失败,则被阻塞,一直等到锁被释放为止。
⭐锁的升级
过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
轻量级锁场景:线程不多,线程持有锁的时间不长,采用CAS+自旋(让线程不被阻塞,一直处于抢锁状态,因为持有锁时间不长,所以上下文切换的消耗比CPU执行自旋的消耗低)
重量级锁:线程多且执行时间长
⭐synchronized和volatile的区别?
volatile是线程同步的轻量级实现,所有性能要比synchronized好,但是只能修饰变量,而synchronized可以修饰方法以及代码块。volatile可以保障数据的可见性、有序性,synchronized可以保障数据的可见性、有序性、原子性。
ReentrantLock是什么?
ReentrantLock与synchronized类似,不过ReentrantLock功能更强,还有中断、公平锁、非公平锁等功能
ps:公平锁:锁被释放后,线程先到先得
非公平锁:线程随机获得或者按照某种优先级获得锁




