多线程的几种实现方式
继承Thread类
实现Runnable接口或者实现Callable接口
线程池创建方式
Callable定义方法的返回值,可以声明试抛出异常
如何停止一个正在运行的线程
可以使用退出标志,使线程正常退出,也就是run方法执行完成后线程终止。
使用stop方法强制终止,但不推荐该方法。
使用interrupt方法中断线程。
notify和notifyAll有什么区别
a. ⾸先最好说⼀下 锁池 和 等待池 的概念
ⅰ. 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,⽽其它的线程想要调⽤这个对象的某个synchronized⽅法(或者synchronized块),由于这些线程在进⼊对象的synchronized⽅法之前必须先获得该对象的锁的拥有权,但是该对象的锁⽬前正被线程A拥有,所以这些线程就进⼊了该对象的锁池中。
ⅱ. 等待池:假设⼀个线程A调⽤了某个对象的wait()⽅法,线程A就会释放该对象的锁(因为wait()⽅法必须出现在synchronized中,这样⾃然在执⾏wait()⽅法之前线程A就已经拥有了该对象的锁),同时线程A就进⼊到了该对象的等待池中。如果另外的⼀个线程调⽤了相同对象的notifyAll()⽅法,那么处于该对象的等待池中的线程就会全部进⼊该对象的锁池中,准备争夺锁的拥有权。如果另外的⼀个线程调⽤了相同对象的notify()⽅法,那么仅仅有⼀个处于该对象的等待池中的线程(随机)会进⼊该对象的锁池.
b. 如果线程调⽤了对象的 wait()⽅法,那么线程便会处于该对象的等待池中,等待池中的线程不
会去竞争该对象的锁
c. 当有线程调⽤了对象的 notifyAll()⽅法(唤醒所有 wait 线程)或 notify()⽅法(只随机唤醒⼀个 wait 线程),被唤醒的的线程便会进⼊该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调⽤了notify后只要⼀个线程会由等待池进⼊锁池,⽽notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
d. 所谓唤醒线程,另⼀种解释可以说是将线程由等待池移动到锁池,notifyAll调⽤后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执⾏,如果不成功则留在锁池等待锁被释放后再次参与竞争。⽽notify只会唤醒⼀个线程。
sleep()和wait()有什么区别
sleep()方法属于Thread类,而wait()属于Object类。
sleep()方法导致程序暂停执行指定时间,让出cpu执行其他线程,当线程睡眠时间到了又自己恢复运行状态,在这个过程中不会释放对象锁。
wait()方法,线程会放弃对象锁,然后该线程进入等待状态,只有调用notify方法过后,才进入获取对象锁准备,然后进入运行状态。
volatile是怎么用的
第一个是禁止指令重排序(变量定义顺序不会重新排序)
保证了不同线程对该变量的可见性,即某个线程修改了这个变量的值,其他线程立马可见。
使用valatile用于状态标记量和单例模式的双检锁
volatile只能使用在静态变量级别,synchronized可以使用在变量,方法,类级别。
Thread类中start()和run()有什么区别
start()方法用来启动新创建的线程,而且start()内部调用了run()方法。
当执行run()的时候,只会在原来的线程中调用,没有新的线程启动,start()方法才会启动新的线程。
为什么 wait notify notifyAll都不在Thread类里面
Java提供的锁是对象级别的,而不是线程级别的,每个对象都有锁,通过线程获得,都是锁级别的操作,定义在Object里面是因为锁属于对象。
为什么 wait()notify()nofityall()要在同步代码块中执行?
只有调用线程拥有某个对象的独占锁时,才能调用这个对象的这些方法。
代码会抛出相应的异常
避免wait notify产生竞态条件。
Java中interrupted 和 isInterruptedd方法的区别
interrupted会中断状态清除,而 isInterrupted 不会
一个线程的中断有可能被其他线程调用中断而改变。
Java中synchronized 和 ReentrantLock (可重入锁)有什么不同?
ReentrantLock :可中断,可重入,可设置公平/非公平,jvm接口,必须手动释放锁
Synchronized:不可中断,可重入,非公平锁,java关键字
线程T1、T2、T3、如何保证顺序执行
1、 可以采用线程类的join()方法,在一个线程中启动另外一个线程,另外一个线程完成该线程继续执行。为了确保三个线程顺序执行(T3调用T2、T2调用T1),这样T1就会先完成 T3最后完成。
2、CountDownLatch:定义三个CountDownLatch c1,c2,c3, T2,T3启动的时候分别调用c2.await()c3.await(),然后在T1线程里面调用c2.countDown(),T2线程里面调用c3.countDown();
3、ReentrantLock:利用ReentrantLock的lock.newCondition()初始化三个Condition c1,c2,c3; T2,T3启动的时候分别调用c2.await()c3.await(),然后在T1线程里面调用c2.signal(),T2线程里面调用c3.signal();
4、利用单线程线程池newSingleThreadExcutor。
Java中的线程池 submit()和execute()区别
两个方法都可以向线程池提交任务,execute()返回的类型是void,而submit()持有的是有返回对象的Future结果。
常见的线程池有哪些
newSingleThreadExcutor:创建一个单线程的线程池,此线程池保证所有的任务执行顺序按照任务提交顺序进行执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程池,知道线程到达线程池的最大大小
newCacheThreadPool:创建可缓存的线程池,不对大小做限制,依赖于操作系统所能创建的最大值
newScheduledThreadPool:此线程池定时或者周期执行任务。
newSingleThreadScheduledExecutor:只有⼀个线程,⽤来调度任务在指定时间执⾏。
线程池参数:核心线程数、最大线程数、超时时间、超时时间单位、工作队列、线程工厂,抛弃策略。
对线程池的理解
降低资源消耗,通过重复利用已创建的线程来降低资源消耗
提高响应速度,当任务到达时,任务无需等待线程创建直接执行。
提高线程管理性,线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还好损耗系统的稳定性,使用线程池进行统一的分配、调度、监控、调优。
线程池的原理
线程池核心参数:最大线程数、核心线程数、活跃时间、活跃时间单位、工作队列、线程工厂、拒绝策略。
⾸先检测线程池运⾏状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执⾏任务。
如果workerCount < corePoolSize,则创建并启动⼀个线程来执⾏新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动⼀个线程来执⾏新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理⽅式是直接抛异常。
四种拒绝策略:丢弃任务并抛异常(默认)、直接丢弃不抛异常、移出最早进入队列的线程尝试把当前任务加入队列、直接执行
产生死锁的必备条件
互斥条件:一个资源只能被一个线程使用
请求与保持条件:一个资源因请求资源而阻塞时,保持对已获得的资源不放。
不剥夺条件:已获得资源,再未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头围相接的循环等待资源。
如何避免死锁
由于资源互斥是资源使⽤的固有特性,⽆法改变,我们不讨论
破坏不可剥夺条件
⼀个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加⼊到系统的资源列表中,可以被其他的进程使⽤,⽽等待的进程只有重新获得⾃⼰原有的资源以及新申请的资源才可以重新启动,执⾏
破坏请求与保持条件
第⼀种⽅法静态分配即每个进程在开始执⾏时就申请他所需要的全部资源,
第⼆种是动态分配即每个进程在申请所需要的资源时他本身不占⽤系统资源
破坏循环等待条件
采⽤资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采⽤较⼤的编号,在申请资源时必须按照编号的顺序进⾏,⼀个进程只有获得较⼩编号的进程才能申请较⼤编号的进程。
线程池核心线程数如何设置
CPU密集型:这种任务主要消耗CPU资源,核心线程数=CPU+1、多出一个来是为了防止偶发的缺页中断,或者其他原因导致任务的暂停,充分利用资源。
IO密集型:大部分任务都是通过IO交互处理,这个时候我们可以多配置一些核心线程数,核心线程数=CPU核心数*2
Java线程池常用队列有哪些
ArrayBlockingQueue:基于数组的有界阻塞队列,按照先进先出的顺序进行排序。
LinkedBlockingQueue:基于链表的阻塞队列,按照先进先出的顺序,吞吐量要高于ArrayBlockingQueue。
DelayQueue :延迟队列,只有指定的延迟时间到了,才从队列中获取元素。
对比:LinkedBlockingQueue可以指定大小,也可以不指定大小,不指定就是Integer的最大值,ArrayBlockingQueue必须指定大小。
对比:LinkedBlockingQueue读和写分别持有一把锁,实现了锁分离,并发性能更好
线程安全需要保持几个特征:
原子性:相关操作中途不受其他线程的干扰,一般采用同步方法实现。
可见性:某个线程修改了共享变量,其状态能够被其他线程知晓,采用valatile实现
有序性:保证线程类串行语义,避免指令重排序,采用valatile实现。
线程之间是如何通信的
共享内存:在共享内存的并发模式中,通过线程之间内存的公共状态来隐式进行通信,典型的就是通过共享对象来通信。
消息传递:显式进行通信:wait()、notify()
CAS原理
cas叫做CompareAndSwap,比较并交换,通过执行来保证操作的原子性。
cas的缺点:ABA问题,大部分场景下ABA问题不影响最终的效果。
cas缺点:自旋长时间不成功,大量消耗资源。
说说ThreadLocal的原理
可以理解为线程本地变量,它会为每个变量创建副本,那么线程之间访问内部变量副本就可以了,做到了线程之间的互相隔离,相比synchronized的做法是用空间换时间。
举例:动态切换数据源,通过切面 和 ThreadLocal来实现。
什么是AQS?
AQS全称为AbstractQueuedSychronizer ,抽象队列的同步器。AQS定义了一套多线程访问共享资源的同步器框架。
ReentrantLock、CountDownLatch、Semaphore 都是基于AQS来实现的。
AQS定义两种资源共享方式:
Exclusive:独占,只有一个线程能够执行【ReentrantLock】
Share:共享,多个线程可同时执行【Semaphore、CountDownLatch】
ReentrantLock:state初始化为0,表示未锁定状态,A线程调用lock()的时候,进行state+1,其他线程进行获得锁的时候就会失败,只有等 state = 0 的时候,也就是释放锁的时候,其他线程才能获得锁成功。当然,释放锁之前,A线程可以重复获得此锁,state + 1,这就是可重入锁的概念,当然释放的时候,也要保证 state = 0 。
CountdownLatch:用于实现线程间的等待,某个线程A等待若干个线程执行完成过后它才执行,处理的是 线程和线程之间执行顺序的问题。
Semaphore:控制某组资源线程并发数。
了解Semaphore吗?
semaphore:限制某段代码的并发数,是一个构造函数,可以传入一个int型的整数N,表示某段代码只能有N个线程进行访问,如果超出N,则直接等待,如果N传入 1 ,则表示和synchronized结果一致。
什么是Callable和Future
Callable类似于Runnable,Runnable没有返回结果,并且无法抛出返回结果的异常,而Callable被执行后,可以有返回值,这个值可以被Future拿到,可以拿到异步执行任务的返回值,可以认为是带有回调的Runnable。
Callable用于产生结果,Future用于获取结果
什么是阻塞队列,原理是什么,
阻塞队列(BlockingQueue):是一个支持两个附加操作的队列
当队列为空时,获取元素的线程会等待队列变成非空,当队列满时,存储元素的队列会等待队列可用。常用于生产者和消费者。
ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue
什么是多线程中的上下文切换
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行,上下文切换是多线任务操作系统和多线程的必备特征之一。
悲观锁和乐观锁的理解和实现方式
悲观锁:假设最坏的情况,每次去拿锁都认为别人会修改,所以每次拿锁的时候都会上锁,其他来获取锁就会阻塞。
在传统的关系型数据库中,行锁、表锁、都是在操作之前上锁、Java中的synchronized也是悲观锁。
乐观锁:顾名思义,每次去拿数据都会认为别人不会修改,所以拿数据不会上锁,但是更新的时候会判断在此期间有没有更新过数据,可使用版本号机制。
乐观锁使用于多读的类型,这样可以提高吞吐量。
在Java中 并发包 java.util.concurrent.atomic 目录下,原子变量就是采用乐观锁的实现方式 CAS来实现的。
乐观锁的实现方式:使用版本标识来确定读到的数据和提交的数据是否一致,提交后修改版本标识,不一致可以采取丢弃或者重试。