多线程
1.什么是进程?什么是线程?
线程是处理器任务调度和执行的基本单位,进程是操作系统资源分配的基本单位。
进程(process):是程序的一次执行过程,是系统运行的基本单位。
线程(thread):是一个比进程更小的执行单位,一个进程可以包含多个线程。
2.多线程优缺点?
优点:
(1)多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
(2)当前没有进行处理的任务时可以将处理器时间让给其它任务;
(3)占用大量处理时间的任务可以定期将处理器时间让给其它任务;
(4)可以随时停止任务;
(5)可以分别设置各个任务的优先级以优化性能
缺点:
(1) 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
(2)多线程需要协调和管理,所以需要CPU时间跟踪线程;
(3)线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
(4)线程太多会导致控制太复杂,最终可能造成很多Bug;
3.什么是并发?什么是并行?
并发:一个处理器同时处理多个任务。
并行:多个处理器或者是多核的处理器同时处理多个不同的任务。
两者区别:并发是交替执行,并行是同时执行。
4. Java中实现线程方式?【重点】
Java多线程实现的四种方式:
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
3.通过Callable创建线程
4.通过线程池创建线程
class MyThread01 extends Thread{
@Override
public void run() {
System.out.println(2);
}
}
class MyRun implements Runnable{
public void run() {
System.out.println(2);
}
}
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
//创建线程并启动
new Thread(futureTask).start();
//等我执行完毕返回结果
int result = futureTask.get();
System.out.println(result);
}
}
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
return 100;
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService service=Executors.newFixedThreadPool(5);//线程池容量,自己定
service.execute(new Ticket());//execute(Runnable)
service.submit(new Animal());//submit(Callable<T>)
}
}
//class Ticket implements Runnable {
// private int num=1;
// @Override
// public void run() {
// while(num<=100) {
// System.out.println(Thread.currentThread().getName()+"正在出售第 "+(num++)+" 张火车票");
// }
// }
//}
//class Animal implements Callable<String>{
//
// @Override
// public String call() throws Exception {
// System.out.println("跑了100步");
// return "100";
// }
//}
5. runnable和callable区别?
相同点:
两者都是接口,两者都需要调用start()启动线程
不同点:
callable的核心是ca11()方法,允许返回值,runnable的核心是run()方法,没有返回值
ca11()方法可以抛出异常,但是run()方法不行
6.守护线程和普通线程区别?
守护线程又称为“服务线程”,在没有用户线程可服务时会自动离开。
7.线程常用方法
currentThread():返回正在执行的线程对象
sleep():使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。
join():等待该线程执行结束
yield():放弃被CPU调度权,让CPU重新调用线程(自己和其他线程)
start():启动线程
stop():结束线程
setPrority(n):设置线程优先级
8. run和start区别?
run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。
run()方法可以重复调用,start()方法只能调用一次。
9.线程状态是什么?
1. 新建状态(NEW):新创建了一个线程对象。
2. 就绪状态(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于就绪线程池中,等待被线程调度选中,获取cpu的使用权。
3. 运行状态(RUNNING):就绪状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
4. 阻塞状态(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu使用权,即让出了cpu时间片,暂时停止运行。直到线程进入就绪状态(runnable),才有机会再次获得cpu时间片转到运行状态(running)。
阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪(runnable)状态。
5. 死亡状态(DEAD):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
10.线程的sleep()方法和yield()方法有什么不同?
1.sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
2.线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
3.sleep()方法抛出InterruptedException异常,而yield()方法无需抛出异常;
11.什么是死锁、如何避免
死锁:多个进程或线程互相等待对方的资源,在得到新的资源之前不会释放自己的资源,这样就形成了循环等待,这种现象被称为死锁。
产生死锁的四大必要条件:
1.资源互斥:资源只有两种状态,只有可用和不可用两状态,不能同时使用,同一时刻只能被一个进程或线程使用。
2.占有且请求:已经得到资源的进程或线程,继续请求新的资源,并持续占有旧的资源。
3.资源不可剥夺:资源已经分配进程或线程后,不能被其它进程或线程强制性获取,除非资源的占有者主动释放。
4.环路等待:死锁发生时,系统中必定有两个或两个以上的进程或线程组成一条等待环路。
注意:死锁一旦产生基本无解,现在的操作系统无法解决死锁,因此只能防止死锁产生。
防止死锁产生的方法:
1.破坏占用且请求条件:采用预先静态分配的方法,进程或线程在运行前一次申请所有资源,在资源没有满足前不投入运行。
缺点:系统资源会被严重浪费,因为有些资源可能开始时使用,而有些资源结束时才使用。
2.破坏不可剥夺条件:当一个进程或线程已经占有一个不可剥夺的资源时,请求新资源时无法满足,则释放已经占有的资源,一段时间后再重新申请。
缺点:该策略实现起来比较复杂,释放已经获取资源可能会导致前一阶段的工作失效,反复的申请释放资源会增加系统开销,占用CPU和寄存器、内存等资源。
3.破坏循环等待条件:给每个资源进行编号,进程或线程按照顺序请求资源,只有拿到前一个资源,才能继续请求下一个资源。
缺点:资源的编号必须相对稳定,资源添加或销毁时会受到影响。
12. sleep和wait区别?线程协作(线程经典案例–Object(wait(n),notify(),notifiyAll()))
1. sleep是线程中的方法,但是wait是Object中的方法。
2. sleep方法不会释放锁,但是wait会释放,使线程加入到等待队列中。
3. sleep不依赖于同步器 synchronized,但是 wait 需要依赖 synchronized 关键字。
4. sleep不需要被唤醒(休眠之后从阻塞转为就绪状态),调用wait的线程需要调用notify/notifyAll唤醒。
13.什么是 ThreadLocal?【重点】
ThreadLocal叫做本地线程变量,即ThreadLocal中存储的是当前线程的私有变量,该变量对其他线程而言是封闭且隔离的,但每个线程都可以访问自己内部的私有变量。
14. ThreadLocal是否会内存泄漏?
会。
当仅仅只有 ThreadLocalMap 中的 Entry 的 key 指向 ThreadLocal 的时候,ThreadLocal 会进行回收。
ThreadLocal 被回收后,在 ThreadLocalMap 里对应的 Entry 的键值会变成 null,但是 Entry 是强引用,那么 Entry 里面存储的 Object,并没有办法进行回收,所以有内存泄漏的风险。
15.解释一下强引用、软引用、弱引用、虚引用?
强引用(StrongReference):一般指的是对像被new出来,强引用一般不会被jvm收回
如:Object obj = new Object();
软引用(SoftReference):软引用相对来说弱于强引用,当内存足够的时候不会被GC回收,但内存不足时,再试图回收软引用,通过软引用可以做数据缓存。
SoftReference<String> softRef = new SoftReference<String>();
弱引用(WeakReference):区别于软件引用是生命周期更短,当GC回收启动发现弱引用不管内存满不满,都会被直接回收。
Object obj = new Object();
WeakReference<Object> weekRef =new WeakReference<Object>(obj);
obj = null;
System.gc();
wf.get();//null;
虚引用(PhantomReference):这个引用也有人叫幻引用,也很明显,引用一个不存在,随时会被干掉,算是所有引用中最容易被干掉的。
Object obj = new Object();
ReferenceQueue rq = new ReferenceQueue();
PhantomReference<Object> pr =new PhantomReference<Object>(rq,obj);
pr.get();//null;
16.为什么要使用线程池?
降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度,当任务到达时,任务可以不需要等到线程创建就立即执行。
提高线程的可管理性,线程是稀缺资源,无限制创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以统一分配。
17. execute()和submit()区别?
execute()方法只能执行 Runnable 类型的任务。
submit () 方法可以执行 Runnable 和 ca11ab1e 类型的任务。
submit()方法可以返回持有计算结果的 Future 对象,同时还可以抛出异常,而 execute() 方法不可以。
18. shutdown()和 shutdownNow()区别?
shutdown() 会将线程池状态置为 SHUTDOWN,不再接受新的任务,同时会等待线程池中已有的任务执行完成再结束。
shutdownNow()会将线程池状态置为SHUTDOWN,对所有线程执行interrupt()操作,清空队列,并将队列中的任务返回回来。
19. volatile的作用和特性?【重点】
volatile作用:
volatile 是一个轻量级的 synchronized,一般作用于变量,在多处理器开发的过程中保证了内存的可见性。相比于 synchronized 关键字,volatile 关键字的执行成本更低,效率更高。
volatile变量具有以下三个特性:
1.可见性:即当一个线程修改了声明为volatile变量的值,新值对于其他要读该变量的线程来说是立即可见的。
2.有序性:被声明为volatile的变量的临界区代码的执行是有顺序的,即禁止指令重排序。
3.原子性:volatile变量的原子性与synchronized的原子性是不同的,synchronized的原子性是指只要声明为synchronized的方法或代码块儿在执行上就是原子操作的。而volatile是不修饰方法或代码块儿的,它用来修饰变量,对于单个volatile变量的读/写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。
20. volatile和synchronized区别?【重点】
要想保证数据安全、必须具备三个条件:原子性、有序性、可见性
1.volatile只能作用于变量,使用范围较小。
synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
2.volatile只能保证可见性和有序性。
而可见性、有序性、原子性synchronized都可以包证。
3.volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
4.在性能方面synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率。
volatile是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,在某些情况下性能要优于synchronized。
21.什么是乐观锁?什么是悲观锁?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候
都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字
或Lock接口来实现的。
乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的
时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己
修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
(例如报错或者自动重试)。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
22.什么是GC、作用是什么
GC,垃圾回收机制。
作用:当程序向操作系统申请的内存不再需要时,垃圾回收器主动将其回收并归还给操作系统。
23. GC回收的区域在哪里
JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭。
GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,
所以其不在JVM GC的管理范围内。
24.GC的时机是什么?
(1) 程序调用 System.gc 时可以触发。
(2) 系统自身来决定 GC 触发的时机
25. JVM内存结构
1.JVM分为五个区域:虚拟机栈、本地方法栈、方法区、堆、程序计数器。
2.JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。
3.JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。
1,堆的GC操作采用分代收集算法。
2,堆区分了新生代(1/3)和老年代(2/3);
3,新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。
分配比例:8:1:1
1.在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。
2.当Eden区满了的时候,minor garbage(新⽣生代GC) 被触发 。
3.经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁
4.在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,
存活的对象被复制到survivor区。
当Eden 和 s0区空间满了,S0的所有的数据都被复制到S1
5.经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold
默认是15),
就会被从年轻代Promotion到老年代。
6.随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。
7.虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于0,
则直接触发一次Full GC(老年代GC)
26. GC常用算法
GC 常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。目前主流的 JVM
(HotSpot)采用的是分代收集算法。
a、标记-清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,
这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,
该阶段对死亡的对象进行清除,执行 GC 操作。
b、标记-压缩算法(标记-整理)
标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为
存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,
而是将所有存活的对象整理一下,放到另一处空间, 然后把剩下的所有对象全部清除。这样就达到了
标记-整理的目的。
c、复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,
将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,
循环下去。
d、分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和
老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,
那么这时就采用复制算法。老年代里的对象存活率较高, 没有额外的空间进行分配担保,
所以可以使用标记-整理 或者 标记-清除。
27.常用的 JVM调优的参数都有哪些?(了解)
XX 比 X 的稳定性更差,并且版本更新不会进行通知和说明
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小,单位:m,g |
-Xmx | 堆内存最大允许大小,一般不要大于物理内存的80%。(一般来说-Xms 和-Xmx 的设置为相同大小,因为当 heap 自动扩容时,会发生内存抖动,影响程序的稳定性)。 |
-Xmn | n 为 new,表示新生代大小(-Xss:规定了每个线程虚拟机栈(堆栈)的大小) |
-Xss | 每个线程虚拟机栈(堆栈)的大小 |
-XX:PermSize | 非堆内存初始大小,一般使用设置初始化200m,最大1024m就够了。 |
-XX:MaxPermSize | 非堆内存最大允许大小 |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比值,默认为8,即8:1 |
-XX:PretenureSizeThreshold=3145728 | 当创建(new)的对象大于3M 的时候直接进入 |
-XX:MaxTenuringThreshold=15 | 当对象的存活的年龄(minor gc 一次加 1)大于多少时,进入老年代 |
-XX:-DisableExplicirGC | 是否(+表示是,-表示否)打开 GC 日志 |