一、实现多线程有几种方法
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(
JDK1.5>=
) - 线程池方式创建
二、notify()和notifyAll()有什么区别?
- 使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个
- notify可能会导致死锁,而notifyAll则不会
三、sleep()和wait() 有什么区别?
- 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是它的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
- 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
四、Thread 类中的start() 和 run() 方法有什么区别?
- start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
- 当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,
start()方法才会启动新线程。
五、有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()
方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
六、synchronized 关键字
synchronized
关键字解决的是多个线程之间访问资源的同步性synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized
属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因
JDK1.6
后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁的优化和升级机制
从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
自旋锁
由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态
自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁
自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除
锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化
锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外
偏向锁
当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。
可以用过设置-XX:+UseBiasedLocking开启偏向锁
轻量级锁
JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现
重量级锁则是除了拥有锁的线程其他全部阻塞。
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
一般会如何使用synchronized
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
总结:
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
七、volatile关键字的作用?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- 使用volatile 一般用于 状态标记量 和 单例模式的双检锁。
八、对线程池的理解
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
常用线程池
newSingleThreadExecutor
:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool
:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。newCachedThreadPool
:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。newScheduledThreadPool
:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。newSingleThreadExecutor
:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
线程池的线程数量如何设置
- 分为CPU密集型和IO密集型
- CPU这种任务消耗的主要是 CPU 资源,可以将线程数设置为
N(CPU 核心数)+1
,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 - IO密集型
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 :核心线程数=CPU核心数量*2
。
线程池中的常用阻塞队列有那些
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。SynchronousQueue
一个不存储元素的阻塞队列。PriorityBlockingQueue
一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现DelayQueue
只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue
是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。SynchronousQueue
:一个不存储元素的阻塞队列。LinkedTransferQueue
:一个由链表结构组成的无界阻塞队列。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
线程池的原理以及核心参数
线程池核心参数
首先线程池有几个核心的参数概念:
- 最大线程数
maximumPoolSize
- 核心线程数
corePoolSize
- 活跃时间
keepAliveTime
- 阻塞队列
workQueue
- 拒绝策略
RejectedExecutionHandler
线程池执行流程
当提交一个新任务到线程池时,具体的执行流程如下:
- 当我们提交任务,线程池会根据
corePoolSize
大小创建若干任务数量线程执行任务 - 当任务的数量超过
corePoolSize
数量,后续的任务将会进入阻塞队列阻塞排队 - 当阻塞队列也满了之后,那么将会继续创建(
maximumPoolSize-corePoolSize
)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize
额外创建的线程等待keepAliveTime
之后被自动销毁 - 如果达到
maximumPoolSize
,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
线程池的拒绝策略
主要有4种拒绝策略:
AbortPolicy
:直接丢弃任务,抛出异常,这是默认策略CallerRunsPolicy
:只用调用者所在的线程来处理任务DiscardOldestPolicy
:丢弃等待队列中最旧的任务,并执行当前任务DiscardPolicy
:直接丢弃任务,也不抛出异常