Java并发复习
Java基础
1. 为什么要使用并发编程?
一般我们工作的电脑都有多核,我们创建多个线程,然后操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU使用效率。
在网络购物中,我们买了一个东西的同时,需要减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序串行会比程序更适应业务需求,而并发编程更能吻合这种业务拆分。
->
- 充分利用多核CPU的计算能力;
- 更好进行业务拆分,提升应用性能
2. 并发编程有什么缺点
并发编程并不能总是能提高程序运行速度,而且可能遇到很多问题:
内存泄漏,上下文切换,线程安全,死锁等问题。
3.并发编程三个必要因素是什么?
- 原子性:可以这么理解:这里的原子是一个不可以再分割的颗粒。->一个或多个操作要么全部完成,否则全部失败
- 可见性:一个线程对共享变量的修改,其他线程能够立即看到(sychronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器看可能会对指令进行重排序->保证在处理器看到的串行语义一致)
4.在Java程序中怎么保证多线程的运行安全?
出现线程安全问题的原因一般都是三个原因:
- 线程切换带来的原子性问题
->解决方法:使用多线程之间同步synchronized或使用锁(lock). - 缓存导致的可见性问题
->solution: sychronized,volatile,LOCK,可以解决可见性问题。、 - 重排序优化带来的有序性问题
->solution:Happen-Before规则则可以解决 有序性问题
Happen-Before规则如下:
- 程序次序规则:在一个线程内,按照程序控制流,书写在前面的操作先行发生于书写在后面的操作
- 锁操作:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作先行发生于后面这个变量的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
- 线程中断规则:对线程的Interrupt()方法的调用发生于被中断线程的代码检测到中断事件的发生。
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始
并行和并发的区别
并行:同一时间多个处理器处理多个任务
并发:一个处理器按照时间片轮转法处理多个任务
什么是进程,什么是线程
这里有两个并发,一个是进程的并发,一个是线程 的并发
进程:对运行时的程序进行封装,是系统进行资源分配的基本单位,实现了操作系统的并发;
线程:进程的子任务,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发;
什么是上下文切换?
在多线程编程中,一般线程的个数大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了能让这些线程都有效的执行,CPU采用的策略是为每个线程分配时间片并轮转的形式。当一个线程 的时间片用完的时候就会重新处于就绪状态让其他线程使用,这个过程就属于一次上下文切换。
上下文切换通常会因为相当可观的处理器i时间来切换线程而消耗大量的CPU时间,在Linux系统中,上下文切换和模式切换的时间消耗是非常少的。
守护线程和用户线程的区别?
- 用户线程:运行在前台,执行具体的任务,如程序的主线程,连接网络的子线程等都是用户线程
- 守护线程:运行在后台,为其他前台线程服务。
守护线程在JVM中,是非守护线程的“佣人”,只要所有用户线程都结束运行,守护线程会随JVM一起结束工作。
什么是线程死锁?
- 死锁是指两个或两个以上的线程 因为竞争资源 或者 由于彼此通信而造成的一种阻塞的现象,若无外力作用,他么都无法推进下去。此时称系统处于死锁状态或系统产生了死锁进程。
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源别释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
Java线程有几种状态,分别是啥
在Java thread state枚举类中定义了6种线程状态,分别是新建状态New,运行态Runnable,阻塞态Blocked,等待态waiting,延迟等待态Tiimed Waiting 和 终止状态Terminated
线程状态如何流转
新建状态(New)通过start 方法到达 运行状态 (Runnable)
运行状态(Runnable)等待锁进入阻塞状态(Blocked)
获得锁回到运行状态(Runnable)
运行状态(Runnable)等待其他线程通知进入等待状态(Waiting)
收到通知回到运行状态(Runnable)
超时等待(Time Waiting) 同理,但是这个会设置一个时间限制,超过了这个等待的最大时间,线程会自动返回。
结束进入终止状态
Java创建线程的方式
1. 继承Thread类
通过继承Thread类,并重写它的run方法,我们就可以创建一个线程。
- 首先定义一个类来继承Thread类,重写run方法
- 然后创建这个子类对象,并调用start方法启动线程
2. 实现Runnable接口
通过实现Runnable接口,并实现run方法,也可以创建一个线程。 - 首先定义一个类实现Runnable接口,并实现run方法。
- 然后创建Runnable实现类对象,并把它作为target传入Thread的构造函数中
- 最后调用start方法启动线程。
3.实现Callable接口,并结合Future实现 - 首先定义一个Callable的实现类,并实现call方法,call方法是带返回值的。
- 然后通过FutureTask的构造方法,把这个Callable实现类传进去。
- 把FutureTask作为Thread类的target,创建Thread线程对象。
- 通过FutureTask 的get方法获取线程的执行结果
说一下runnable和callable有什么区别
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动程序
主要区别:
- Runnable接口 run方法无返回值;Callable接口call方法有返回值,是个泛型,和Future,FutureTask配合可以用来获取异步执行的结果
- Runnable接口 run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息 注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主线程的继续往下执行,如果不调用不会阻塞。
什么是Callable 和 Future ? 什么是FutureTask?
- Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。【异步执行:执行一个类似于函数的东西,但是不会被这个函数这一行代码阻塞,可以在之后的代码中获取结果或处理任务完成的状态】
sleep()和wait()有什么区别?
两者都可以暂停线程的执行:
- 类的不同:sleep()是THread线程类的静态方法,wait()是Object类的方法
- 是否释放锁:sleep()不释放锁;wait()释放锁
- 用途不同:Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
- 用法不同:wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout) 超时后,线程会自动苏醒。
为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?
因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且wait(),notify()等方法用于等待对象的锁或者唤醒线程,在Java的线程类中并没有可供所有对象使用的锁,所以任意对象都可调用的方法一定定义在Object类中。
有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Theread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现
为什么wait(),notify()和notifyAll()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态。
知道其他线程调用这个对象上的notify()方法。
同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程可以得到这个对象锁。
由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
线程的sleep()方法和yield()方法有什么区别?
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法指挥给相同优先级或更高优先级线程以运行的机会;
- 线程执行sleep()方法后转入阻塞(超时等待状态),而执行yield()方法后转入就绪(ready)状态;
- sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
- sleep()方法比yield()方法(跟操作系统CPU调度相关) 具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
[sleep()方法在Java中明确定义,而且在所有JAVA平台都一致 ,无论在哪个平台使用,sleep()方法都能导致线程暂停指定的时间]
在某些系统中,yield可能不能短暂放弃CPU ->Expand
如何停止一个正在运行的线程?
1.run()方法完成之后->使用退出标志,是线程正常退出
2.使用stop方法强行终止,但是不推荐这个方法,这是一个过时的方法
3.使用interrupt方法中断线程:这个需要被中断的线程的中断标识位不会因为sleep(),wait()而抹除
需要重复标记->需要有个catch块中不断标记
catch (InterruptedException e) {
System.out.println("Thread was interrupted during sleep.");
// 重新中断线程,以确保中断状态被正确标记
Thread.currentThread().interrupt();
}
注意:这个线程是否真正中断,需要看操作系统以及JVM的实现
Java中interrupted和 isInterrupted方法的区别?
interrupt:用于中断线程。调用该方法的线程的状态将设置为“中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了
线程池的拒绝策略有哪些?
AbortPolicy: 该线程是线程池的默认策略。使用该策略时,如果线程队列满了****丢掉这个任务并且抛出RejectedExecutionException异常。
DiscardPolicy :
队列满了,丢弃这个任务,不抛出异常
DiscardOldestPolicy:
丢弃最早将进入队列的任务,再尝试加入队列,移除队头,再让新加入的任务加入队尾
CallerRunsPolicy:
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。**
CountDownLatch,CyclicBarrier,Semapher,Exchanger说一下:
- CountDownLatch:倒计数器。允许一个或多个线程等待其他线程完成操作。
- CyclicBarrier:可循环使用(Cyclic)的屏障(Barrier)。让一组线程都到达屏障点的时候,所有被屏障拦截的线程才会运行。
- Exchanger:是一个用于线程间协作的工具类。Exchanger用于进行线程缉拿的数据交换。它提供了一个同步点,在这个同步点,两个线程可以交换彼此的数据。
CyclicBarrier和CountDownLatch有什么区别?
- CyclicBarrier是可重用的,其中的线程会等待所有的线程完成任务。CountDownLatch是一次性的,不同的线程在同一个计数器上工作,知道计数器为0.
- CyclicBarrier面向的是线程数;CountDownLatch面向的是任务数。
这个就是说他们两个都会设置变量,假如变量是10,如果CyclicBarrier中的线程到达barrier-> -1
CountDownLatch中的任务完成->-1