Java中用到的线程调度算法是什么
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU ,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU 。处于运行状态的线程会一直运行,直至它不得不放弃 CPU 。
线程的生命周期?
线程一共有五个状态,分别如下:
- 新建(new):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1 = new Thread() 。
- 可运行(runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。例如:t1.start() 。
运行(running):线程获得 CPU 资源正在执行任务(#run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead):当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行完 #run()方法,终止。
- 异常终止:调用 #stop() 方法,让一个线程终止运行。
堵塞(blocked):由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。直到线程进入可运行(runnable)状态,才有机会再次获得 CPU 资源,转到运行(running)状态。阻塞的情况有三种:
- 正在睡眠:调用
#sleep(long t)
方法,可使线程进入睡眠方式。 - 正在等待:调用
#wait()
方法。调用notify()
方法,回到就绪状态。 - 被另一个线程所阻塞:调用
#suspend()
方法。(调用#resume()
方法,就可以恢复。)
- 正在睡眠:调用
整体如下图所示:
- 中间一行是线程的顺畅的执行过程的四个状态。其上下两侧,是存在对应的情况,达到阻塞状态和恢复执行的过程。
- 有一点要注意,新建(new)和死亡(dead)是单向的状态,不可重复。
创建线程的方式及实现?
- 方式一,继承 Thread 类创建线程类。
- 方式二,通过 Runnable 接口创建线程类。
- 方式三,通过 Callable 和 Future 创建线程。
创建线程的三种方式的对比:
- 使用方式一
- 优点:编写简单,如果需要访问当前线程,则无需使用 Thread#currentThread() 方法,直接使用 this 即可获得当前线程。
- 缺点:线程类已经继承了 Thread 类,所以不能再继承其他父类。
- 使用方式二、或方式三
- 优点:
- 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 可以使用线程池。
- 缺点:
- 编程稍微复杂,如果要访问当前线程,则必须使用Thread#currentThread() 方法。
- 优点:
如何使用 wait + notify 实现通知机制?
sleep、join、yield 方法有什么区别?
1)sleep 方法
在指定的毫秒数内,让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有synchronized 同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常。
比如有两个线程同时执行(没有 synchronized),一个线程优先级为MAX_PRIORITY ,另一个为 MIN_PRIORITY 。
- 如果没有 sleep 方法,只有高优先级的线程执行完成后,低优先级的线程才能执行。但当高优先级的线程 #sleep(5000) 后,低优先级就有机会执行了。
- 总之,sleep 方法,可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
2)yield 方法
yield 方法和 sleep 方法类似,也不会释放“锁标志”,区别在于:
- 它没有参数,即 yield 方法只是使当前线程重新回到可执行状态,所以执行yield 的线程有可能在进入到可执行状态后马上又被执行。
- 另外 yield 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep 方法不同。
3)join 方法
Thread 的非静态方法 join ,让一个线程 B “加入”到另外一个线程 A 中。B运行完后,A才会继续执行。示例代码如下:1
2
3Thread t = new MyThread();
t.start();
t.join();
- 保证当前线程停止执行,直到该线程所加入的线程 t 完成为止。然而,如果它加入的线程 t 没有存活,则当前线程不需要停止。
sleep(0) 有什么用途?
Thread#sleep(0)
方法,并非是真的要线程挂起 0 毫秒,意义在于这次调用 Thread#sleep(0)
方法,把当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread#sleep(0)
方法,是你的线程暂时放弃 CPU ,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作
你如何确保 main 方法所在的线程是 Java 程序最后结束的线程?
考点,就是 join 方法。
我们可以使用 Thread 类的 #join()
方法,来确保所有程序创建的线程在 main 方法退出前结束。
什么叫线程安全?
线程安全,是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
多线程同步和互斥有几种实现方法,都是什么?
1)线程同步
线程同步,是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程间的同步方法,大体可分为两类:用户模式和内核模式。顾名思义:
- 内核模式,就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。内核模式下的方法有:
事件- 事件
- 信号量
- 互斥量
- 用户模式,就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:
- 原子操作(例如一个单一的全局变量)
- 临界区
2)线程互斥
线程互斥,是指对于共享的进程系统资源,在各单个线程访问时的排它性。
- 当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
- 线程互斥可以看成是一种特殊的线程同步。
什么是 ThreadLocal 变量?
ThreadLocal ,是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
synchronized 的原理是什么?
volatile 实现原理
什么是死锁
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
- 互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
死锁的解决方法:
- 撤消陷于死锁的全部进程。
- 逐个撤消陷于死锁的进程,直到死锁不存在。
- 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
- 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。
什么是活锁?
活锁,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败
死锁和活锁的区别?
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
实际上,聪慧的胖友是不是已经发现,死锁就是悲观锁可能产生的结果,而活锁是乐观锁可能产生的结果。
什么是悲观锁
悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- 再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
什么是乐观锁
乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
- 像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。例如,version 字段(比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作)
- 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁的实现方式有哪些
- 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
- Java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
什么是Java Lock 接口?
什么是可重入锁(ReentrantLock)?
已经获取到锁的对象,再次申请获取锁,也可以申请成功,就是可重入锁。
synchronized 和 ReentrantLock 异同?
相同点:
- 都实现了多线程同步和内存可见性语义。
- 都是可重入锁。
不同点:
- 同步实现机制不同
- synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。
- ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
- 可见性实现机制不同
- synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。
- ReentrantLock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性。
- 使用方式不同
- synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。
- ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。
- 功能丰富程度不同
- synchronized 不可设置等待时间、不可被中断(interrupted)。
- ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能
- 锁类型不同
- synchronized 只支持非公平锁。
- ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。
在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized 。
并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock 。
ReadWriteLock 是什么
ReadWriteLock ,读写锁是,用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。
ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。
ReadWriteLock 对程序性能的提高主要受制于如下几个因素:
- 数据被读取的频率与被修改的频率相比较的结果。
- 读取和写入的时间
- 有多少线程竞争
- 是否在多处理机器上运行
什么是 Java 内存模型?
为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果。
- 存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
什么是内存模型的 happens-before 呢?
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap
- 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map 。
ConcurrentHashMap
使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。【注意,这块是 JDK7 的实现。在 JDK8 中,具体的实现已经改变】
另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException 异常,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。
什么是 Executor 框架?
Executor 框架,是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的创建线程,会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架,可以非常方便的创建一个线程池。
创建线程池的几种方式?
Java 类库提供一个灵活的线程池以及一些有用的默认配置,我们可以通过Executors 的静态方法来创建线程池。
Executors 创建的线程池,分成普通任务线程池,和定时任务线程池。
1)普通线程池
#newFixedThreadPool(int nThreads)
方法,创建一个固定长度的线程池。- 每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
- 当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
#newCachedThreadPool()
方法,创建一个可缓存的线程池。- 如果线程池的规模超过了处理需求,将自动回收空闲线程。
- 当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制。
#newSingleThreadExecutor()
方法,创建一个单线程的线程池。- 它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
- 它的特点是,能确保依照任务在队列中的顺序来串行执行。
2)定时任务线程池
#newScheduledThreadPool(int corePoolSize)
方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。#newSingleThreadExecutor()
方法,创建了一个固定长度为 1 的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
ThreadPoolExecutor 有哪些拒绝策略?
ThreadPoolExecutor 默认有四个拒绝策略:
- ThreadPoolExecutor.AbortPolicy() ,直接抛出异常 RejectedExecutionException 。
- ThreadPoolExecutor.CallerRunsPolicy() ,直接调用 run 方法并且阻塞执行。
- ThreadPoolExecutor.DiscardPolicy() ,直接丢弃后来的任务。
- ThreadPoolExecutor.DiscardOldestPolicy() ,丢弃在队列中队首的任务。
如果我们有需要,可以自己实现 RejectedExecutionHandler 接口,实现自定义的拒绝逻辑。
什么是 Callable、Future、FutureTask ?
1)Callable
Callable 接口,类似于 Runnable ,从名字就可以看出来了,但是Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
简单来说,可以认为是带有回调的 Runnable 。
2)Future
Future 接口,表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。
3)FutureTask
在 Java 并发程序中,FutureTask 表示一个可以取消的异步运算。
- 它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。
- 一个 FutureTask 对象,可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是继承了 Runnable 接口,所以它可以提交给 Executor 来执行。
讲讲线程池的实现原理
什么是阻塞队列?有什么适用场景?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景:
生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程
阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。