1. 多线程有什么用?
发挥多核CPU 的优势
随着工业的进步, 现在的笔记本、 台式机乃至商用的应用服务器至少也都是双核的, 4 核、 8 核甚至 16 核的也都不少见, 如果是单线程的程序, 那么在双核 CPU 上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。 单核 CPU 上所谓的"多线程"那是假的多线程, 同一时间处理器只会处理一段逻辑, 只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。 多核 CPU 上的多线程才是真正的多线程, 它能让你的多段逻辑同时工作, 多线程,可以真正发挥出多核CPU 的优势来, 达到充分利用CPU 的目的。
防止阻塞
从程序运行效率的角度来看, 单核 CPU 不但不会发挥出多线程的优势, 反而会因为在单核CPU 上运行多线程导致线程上下文的切换, 而降低程序整体的效率。 但是单核 CPU 我们还是要应用多线程, 就是为了防止阻塞。 试想, 如果单核 CPU 使用单线程, 那么只要这个线程阻塞了, 比方说远程读取某个数据吧, 对端迟迟未返回又没有设置超时时间, 那么你的整个程序在数据返回回来之前就停止运行了。 多线程可以防止这个问题, 多条线程同时运行, 哪怕一条线程的代码执行读取数据阻塞, 也不会影响其它任务的执行。
便于建模
这是另外一个没有这么明显的优点了。 假设有一个大的任务 A, 单线程编程, 那么
就要考虑很多, 建立整个程序模型比较麻烦。 但是如果把这个大的任务 A 分解成
几个小任务, 任务B、 任务 C、 任务 D, 分别建立程序模型, 并通过多线程分别运
行这几个任务, 那就简单很多了。
2. 线程和进程的区别是什么?
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。 进程有独立的地址空间, 一个进程崩溃后, 在保护模式下不会对其它进程产生影响, 而线程只是一个进程中的不同执行路径。 线程有自己的堆栈和局部变量, 但线程之间没有单独的地址空间, 一个线程死掉就等于整个进程死掉, 所以多进程的程序要比多线程的程序健壮, 但在进程切换时, 耗费资源较大, 效率要差一些。 但对于一些要求同时进行并且又要共享某些变量的并发操作, 只能用线程, 不能用进程。
3. Java 实现线程有哪几种方式?
- 继承 Thread 类实现多线程
- 实现 Runnable 接口方式实现多线程
- 使用 ExecutorService、 Callable、 Future 实现有返回结果的多线程
4. 启动线程方法 start()和 run()有什么区别?
只有调用了 start()方法, 才会表现出多线程的特性, 不同线程的 run()方法里面的代码交替执行。 如果只是调用 run()方法, 那么代码还是同步执行的, 必须等待一个线程的 run()方法里面的代码全部执行完毕之后, 另外一个线程才可以执行其 run()方法里面的代码。
5. 一个线程的生命周期有哪几种状态?它们之间如何流转的?
NEW: 毫无疑问表示的是刚创建的线程, 还没有开始启动。
RUNNABLE: 表示线程已经触发 start()方式调用, 线程正式启动, 线程处于运行中状态。
BLOCKED: 表示线程阻塞, 等待获取锁, 如碰到 synchronized、 lock 等关键字等占用临界区的情况, 一旦获取到锁就进行 RUNNABLE 状态继续运行。
WAITING: 表示线程处于无限制等待状态, 等待一个特殊的事件来重新唤醒, 如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法, 通过 join()方法进行等待的线程等待目标线程运行结束而唤醒, 一旦通过相关事件唤醒线程, 线程就进入了 RUNNABLE 状态继续运行。
TIMED_WAITING: 表示线程进入了一个有时限的等待, 如 sleep(3000), 等待 3 秒后线程重新进行 RUNNABLE 状态继续运行。
TERMINATED: 表示线程执行完毕后, 进行终止状态。 需要注意的是, 一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态, 线程终止后也不能再回到RUNNABLE 状态。
6. 线程中的 wait()和 sleep()方法有什么区别?
两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(longtimeout)超时后线程会自动苏醒。
7. violatile 关键字的作用?
volatile 关键字的作用主要有两个:
- 多线程主要围绕可见性和原子性两个特性而展开, 使用 volatile 关键字修饰的变量, 保证了其在多线程之间的可见性, 即每次读取到 volatile 变量, 一定是最新的数据。
- 代码底层执行不像我们看到的高级语言----Java 程序这么简单, 它的执行是 Java代码–>字节码–>根据字节码执行对应的 C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互, 现实中, 为了获取更好的性能 JVM 可能会对指令进行重排序, 多线程下可能会出现一些意想不到的问题。 使用 volatile 则会对禁止语义重排序, 当然这也一定程度上降低了代码执行效率从实践角度而言, volatile 的一个重要 作用就是和 CAS 结 合 , 保证了原子性。
8. 为什么要使用线程池?
我们知道不用线程池的话, 每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程, 线程少的话这不会是问题, 而真实环境可能会开启多个线程让系统和程序达到最佳效率, 当线程数达到一定数量就会耗尽系统的 CPU 和内存资源, 也会造成 GC频繁收集和停顿, 因为每次创建和销毁一个线程都是要消耗系统资源的, 如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。 所以,线程池中的线程复用极大节省了系统资源, 当线程一段时间不再有任务处理时它也会自动销毁, 而不会长驻内存。
9. 常用的几种线程池并讲讲其中的工作原理。
什么是线程池?
很简单, 简单看名字就知道是装有线程的池子, 我们可以把要执行的多线程交给线程池来处理, 和连接池的概念一样, 通过维护一定数量的线程池来达到多个线程的复用。
线程池的好处
我们知道不用线程池的话, 每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程, 线程少的话这不会是问题, 而真实环境可能会开启多个线程让系统和程序达到最佳效率, 当线程数达到一定数量就会耗尽系统的 CPU 和内存资源, 也会造成 GC频繁收集和停顿, 因为每次创建和销毁一个线程都是要消耗系统资源的, 如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。 所以,线程池中的线程复用极大节省了系统资源, 当线程一段时间不再有任务处理时它也会自动销毁, 而不会长驻内存。
线程池核心类
在 java.util.concurrent 包中我们能找到线程池的定义, 其中 ThreadPoolExecutor 是我们线程池核心类, 首先看看线程池类的主要参数有哪些。
如何提交线程
如可以先随便定义一个固定大小的线程池ExecutorService es = Executors.newFixedThreadPool(3);提交一个线程
es.execute(xxRunnble);
submit 和 execute 分别有什么区别呢?
- execute 没有返回值, 如果不需要知道线程的结果就使用 execute 方法, 性能会好很多。
- submit 返回一个 Future 对象, 如果想知道线程结果就使用 submit 提交, 而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。
如何关闭线程池
es.shutdown();不再接受新的任务, 之前提交的任务等执行结束再关闭线程池。
es.shutdownNow();不再接受新的任务, 试图停止池中的任务再关闭线程池, 返回所有未处理的线程list 列表。
10. 线程池启动线程 submit()和 execute()方法有什么不同?
- execute 没有返回值, 如果不需要知道线程的结果就使用 execute 方法, 性能会好很多。
- submit 返回一个 Future 对象, 如果想知道线程结果就使用 submit 提交, 而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。
11. CyclicBarrier 和 CountDownLatch 的区别?
两个看上去有点像的类, 都在 java.util.concurrent 下, 都可以用来表示代码运行到某个点上, 二者的区别在于:
- CyclicBarrier 的某个线程运行到某个点上之后, 该线程即停止运行, 直到所有的线程都到达了这个点, 所有线程才重新运行; CountDownLatch 则不是, 某线程运行到某个点上之后, 只是给某个数值-1 而已, 该线程继续运行。
- CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
- CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
12. 什么是活锁、饥饿、无锁、死锁?
死锁、 活锁、 饥饿是关于多线程是否活跃出现的运行阻塞障碍问题, 如果线程出现了这三种情况, 即线程不再活跃, 不能再正常地执行下去了。
死锁
死锁是多线程中最差的一种情况, 多个线程相互占用对方的资源的锁, 而又相互等对方释放锁, 此时若无外力干预, 这些线程则一直处理阻塞的假死状态, 形成死锁。举个例子, A 同学抢了 B 同学的钢笔, B 同学抢了 A 同学的书, 两个人都相互占用对方的东西, 都在让对方先还给自己自己再还, 这样一直争执下去等待对方还而又得不到解决, 老师知道此事后就让他们相互还给对方, 这样在外力的干预下他们才解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念, 而在多线程中这确实存在。活锁恰恰与死锁相反, 死锁是大家都拿不到资源都占用着对方的资源, 而活锁是拿到资源却又相互释放不执行。 当多线程中出现了相互谦让, 都主动将资源释放给别的线程使用, 这样这个资源在多个线程之间跳动而又得不到执行, 这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西, 优先级高的线程能够插队并优先执行, 这样如果优先级高的线程一直抢占优先级低线程的资源, 导致低优先级线程无法得到执行, 这就是饥饿。 当然还有一种饥饿的情况, 一个线程一直占着一个资源不放而导致其他线程得不到执行, 与死锁不同的是饥饿在以后一段时间内还是能够得到执行的, 如那个占用资源的线程结束了并释放了资源。
无锁
无锁, 即没有对资源进行锁定, 即所有的线程都能访问并修改同一个资源, 但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源, 如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。 所以, 如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。 之前的文章我介绍过 JDK 的 CAS 原理及应用即是无锁的实现。可以看出, 无锁是一种非常良好的设计, 它不会出现线程出现的跳跃性问题, 锁使用不当肯定会出现系统性能问题, 虽然无锁无法全面代替有锁, 但无锁在某些场合下是非常高效的。
13. 什么是原子性、可见性、有序性?
原子性
原子性是指一个线程的操作是不能被其他线程打断, 同一时间只有一个线程对一个变量进行操作。 在多线程情况下, 每个线程的执行结果不受其他线程的干扰, 比如说多个线程同时对同一个共享成员变量 n++100 次, 如果 n初始值为 0, n 最后的值应该是 100, 所以说它们是互不干扰的, 这就是传说的中的原子性。 但 n++并不是原子性的操作, 要使用 AtomicInteger 保证原子性。
可见性
可见性是指某个线程修改了某一个共享变量的值, 而其他线程是否可以看见该共享变量修改后的值。 在单线程中肯定不会有这种问题, 单线程读到的肯定都是最新的值, 而在多线程编程中就不一定了。 每个线程都有自己的工作内存, 线程先把共享变量的值从主内存读到工作内存, 形成一个副本, 当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程, 当还没刷回主内存的时候这时候对其他线程是不可见的, 所以其他线程从主内存读到的值是修改之前的旧值。 像CPU 的缓存优化、 硬件优化、 指令重排及对 JVM 编译器的优化, 都会出现可见性的问题。
有序性
我们都知道程序是按代码顺序执行的, 对于单线程来说确实是如此, 但在多线程情况下就不是如此了。 为了优化程序执行和提高 CPU 的处理性能, JVM 和操作系统都会对指令进行重排, 也就说前面的代码并不一定都会在后面的代码前面执行, 即后面的代码可能会插到前面的代码之前执行, 只要不影响当前线程的执行结果。 所以,指令重排只会保证当前线程执行结果一致, 但指令重排后势必会影响多线程的执行结果。 虽然重排序优化了性能, 但也是会遵守一些规则的, 并不能随便乱排序,只是重排序会影响多线程执行的结果。
14. 什么是守护线程?有什么用?
什么是守护线程? 与守护线程相对应的就是用户线程, 守护线程就是守护用户线程, 当用户线程全部执行完结束之后, 守护线程才会跟着结束。 也就是守护线程必须伴随着用户线程, 如果一个应用内只存在一个守护线程,没有用户线程, 守护线程自然会退出。
15. 一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。 Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。 当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
16. 线程 yield()方法有什么用?
Yield 方法可以暂停当前正在执行的线程对象, 让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU, 执行yield()的线程有可能在进入到暂停状态后马上又被执行。
17. 什么是重入锁?
所谓重入锁, 指的是以线程为单位, 当一个线程获取对象锁之后, 这个线程可以再次获取本对象上的锁, 而其他的线程是不可以的。
18. 多线程之间如何进行通信?
多线程之间的通信可以通过多种方式实现,包括使用共享数据结构、使用锁、使用信号量、使用管道和队列等。下面是一些常用的通信方式:
- 共享数据结构:线程可以通过共享数据结构(如数组、列表或映射)进行通信。但是,这种方法需要谨慎处理同步问题,以避免出现竞态条件和死锁。
- 锁:Java提供了多种锁机制,如
synchronized
关键字、ReentrantLock
等,可以用于保护共享数据,并控制对共享资源的访问。 - 信号量:Java中的
Semaphore
类是一个计数信号量,可以用于控制对共享资源的访问。 - 管道和队列:Java提供了多种线程安全的队列实现,如
ArrayBlockingQueue
、LinkedBlockingQueue
等,可以用于在多个线程之间传递数据。另外,Java的PipedInputStream
和PipedOutputStream
类可以用于线程之间的输入/输出通信。 - 等待/通知机制:Java中的
Object
类提供了等待/通知机制,可以用于线程之间的同步。一个线程可以在某个对象上等待,直到另一个线程通知它继续执行。 - Future和Callable:Java的
Future
和Callable
接口可以用于异步计算,并允许在计算完成时获取结果。 - 线程局部变量:Java中的
ThreadLocal
类可以用于为每个线程提供独立的变量副本,从而实现线程之间的隔离。
19. Synchronized 有哪几种用法
- 同步普通方法:只要涉及线程安全,就给方法加个synchronized关键字,这是最简单的方法。但只能作用在单例上面,如果不是单例,同步方法锁将失效。
- 同步静态方法:不管有多少个类实例,同时只有一个线程能获取锁进入这个方法。同步静态方法是类级别的锁,一旦任何一个线程进入这个方法,其他所有线程将无法访问这个类的任何同步类锁的方法。
- 同步代码块:这需要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要获得指定对象的锁。
请注意,使用synchronized关键字可以确保线程安全,但也会导致性能开销。因此,在使用synchronized时,应权衡线程安全和性能的需求。
20. Fork/Join 框架是干什么的?
java中的Fork/Join框架是一种用于实现并行处理的框架,它基于分治策略,将一个大的任务分解为若干个较小的子任务,然后递归地处理这些子任务,并将结果合并得到最终的结果。
Fork/Join框架主要由两部分组成:Fork和Join。在Fork阶段,将任务分解成若干个子任务,这些子任务可以被分配给不同的处理器(线程)执行。在Join阶段,处理器将执行完毕的子任务结果返回给父任务,然后父任务将子任务的结果进行合并,得到最终的结果。
Fork/Join框架的主要优点是可以充分利用多核处理器的计算能力,提高程序的执行效率。同时,它还提供了一些工具类和接口,使得开发人员可以更加方便地编写并行处理程序。
需要注意的是,使用Fork/Join框架需要谨慎处理线程安全问题,并确保任务分解和合并的正确性。此外,由于Fork/Join框架是基于分治策略的,因此对于一些递归算法(如快速排序、归并排序等)来说,使用Fork/Join框架可以更加方便地实现并行化处理。
21. 线程数过多会造成什么异常
- OutOfMemoryError:如果创建了大量的线程,每个线程都需要一定的内存空间来存储其栈信息。当系统内存不足以支持更多的线程时,就可能抛出
OutOfMemoryError
异常。 - ThreadDeath:这是表示线程终止的异常。当线程无法继续执行时,会抛出
ThreadDeath
异常。这通常发生在线程被其他线程中断了,或者由于其他原因导致线程无法继续执行。 - StackOverflowError:当线程尝试在其栈上分配更多空间,但栈空间不足以支持更多操作时,就会抛出
StackOverflowError
异常。如果线程深度递归调用或者创建了过多的局部变量,就可能导致栈溢出。 - IllegalThreadStateException:如果一个线程在已经终止或尚未开始执行的情况下调用了某些方法(如
Thread.currentThread().join()
),就会抛出IllegalThreadStateException
异常。 - RejectedExecutionException:在某些情况下,当线程池拒绝执行新提交的任务时,会抛出
RejectedExecutionException
异常。这通常发生在线程池已满或关闭时提交任务。
22. 说说线程安全的和不安全的集合
线程安全的集合:
Java提供了一些线程安全的集合类,如Vector
和Hashtable
。这些集合类在内部实现了同步机制,可以保证在多线程环境下对集合的访问是安全的。例如,Vector
类的所有公共方法都是同步的,Hashtable
类的所有公共方法也是同步的。
线程不安全的集合:
Java中大多数集合类都是线程不安全的,如ArrayList
、LinkedList
、HashSet
和HashMap
等。这些集合类没有内置的同步机制,因此在多线程环境下使用时需要额外的同步措施来保证线程安全。
如何保证线程安全:
对于线程不安全的集合,可以使用同步块或同步方法来保证线程安全。例如,可以使用Collections.synchronizedList()
方法将一个非线程安全的列表转换为线程安全的列表,然后在访问该列表时使用同步块来保证线程安全。
另外,Java并发包(java.util.concurrent)提供了一些线程安全的集合类,如ConcurrentHashMap
、CopyOnWriteArrayList
和BlockingQueue
等。这些集合类内部实现了复杂的同步机制,可以更好地处理并发访问的情况。
23.什么是 CAS 算法?在多线程中有哪些应用。
CAS, 全称为 Compare and Swap, 即比较-替换。 假设有三个操作数: 内存值 V、旧的预期值 A、 要修改的值 B, 当且仅当预期值 A 和内存值 V 相同时, 才会将内存值修改为 B 并返回 true, 否则什么都不做并返回 false。 当然 CAS 一定要 volatile变量配合, 这样才能保证每次拿到的变量是主内存中最新的那个值, 否则旧的预期值 A 对某条线程来说, 永远是一个不会变的值 A, 只要某次 CAS 操作失败, 永远都不可能成功。java.util.concurrent.atomic 包下面的 Atom****类都有 CAS 算法的应用。
24. 怎么检测一个线程是否拥有锁
可以使用Thread
类的holdsLock()
方法来检查当前线程是否持有某个对象的锁。
Object lock = new Object();
// ... 在某个线程中
if (Thread.holdsLock(lock)) {
System.out.println("当前线程持有锁");
} else {
System.out.println("当前线程不持有锁");
}
25. 线程同步需要注意什么?
- 尽量缩小同步的范围, 增加系统吞吐量。
- 分布式同步锁无意义, 要使用分布式锁。
- 防止死锁, 注意加锁顺序。
26. 线程 wait()方法使用有什么前提
- 对象必须拥有锁:在调用
wait()
方法之前,当前线程必须拥有对象的内部锁。这是为了确保线程安全,避免出现竞态条件。 - 在同步块或同步方法中:
wait()
方法通常位于同步块或同步方法中。这是因为wait()
方法本身也是需要同步的,如果在没有同步的情况下调用,可能会出现不可预期的结果。 - 不能在中断检查块中使用:在Java中,有些代码块被称为中断检查块,这些代码块不会被中断处理机制所覆盖。由于
wait()
方法可以被中断,因此不能在这些代码块中使用。 - 必须被正确地管理:当一个线程进入等待状态后,它需要被其他线程唤醒。这通常通过其他线程调用该对象的
notify()
或notifyAll()
方法来实现。如果一个线程进入了等待状态,但没有其他线程来唤醒它,那么它将会永远地等待下去,直到程序结束。因此,必须正确地管理线程的等待和唤醒。 - 异常处理:当一个线程在等待期间被中断时,
wait()
方法会抛出InterruptedException
异常。因此,在使用wait()
方法时,必须处理这个异常。
27. ThreadLocal 是什么?有什么应用场景?
ThreadLocal 的作用是提供线程内的局部变量, 这种变量在线程的生命周期内起作用, 减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。 用来解决数据库连接、 Session 管理等。
28. ReadWriteLock 有什么用?
ReadWriteLock 是一个读写锁接口, ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现, 实现了读写的分离, 读锁是共享的, 写锁是独占的, 读和读之间不会互斥, 读和写、 写和读、 写和写之间才会互斥, 提升了读写的性能。
29. FutureTask 是什么?
FutureTask 表示一个异步运算的任务, FutureTask 里面可以传入一个 Callable 的具体实现类, 可以对这个异步运算的任务的结果进行等待获取、 判断是否已经完成、取消任务等操作。
30. 怎么唤醒一个阻塞的线程?
如果线程是因为调用了 wait()、 sleep()或者 join()方法而导致的阻塞, 可以中断线程,并且通过抛出InterruptedException 来唤醒它; 如果线程遇到了 IO 阻塞, 无能为力,因为 IO是操作系统实现的, Java 代码并没有办法直接接触到操作系统。
31.不可变对象对多线程有什么帮助?
不可变对象保证了对象的内存可见性, 对不可变对象的读取不需要进行额外的同步手段, 提升了代码执行效率。
32.多线程上下文切换是什么意思?
- 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
- 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
- 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU时间,事实上,可能是操作系统中时间消耗最大的操作。
- Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
33.Java 中用到了什么线程调度算法?
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度)
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
34. Thread.sleep(0)的作用是什么?
由于 Java 采用抢占式的线程调度算法, 因此可能会出现某条线程常常获取到 CPU控制权的情况, 为了让某些优先级比较低的线程也能获取到 CPU 控制权, 可以使用 Thread.sleep(0)手动触发一次操作系统分配时间片的操作, 这也是平衡 CPU 控制
权的一种操作。
35.Java内存模型是什么,哪些区域是线程共享的,哪些是不共享的?
我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享。线程不共享区域每个线程的数据区域包括程序计数器、虚拟机栈和本地方法栈,它们都是在新线程创建时才创建的。
程序计数器(ProgramCounterRerister)
程序计数器区域一块内存较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。
虚拟机栈(VMStack)
虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(NativeMethodStack)
本地方法栈用于支持本地方法(native标识的方法,即非Java语言实现的方法)。虚拟机栈和本地方法栈,当线程请求分配的栈容量超过JVM允许的最大容量时抛出StackOverflowError异常。
线程共享区域
线程共享区域包含:堆和方法区。
堆(Heap)
堆是最常处理的区域,它存储在JVM启动时创建的数组和对象,JVM垃圾收集也主要是在堆上面工作。如果实际所需的堆超过了自动内存管理系统能提供的最大容量时抛出OutOfMemoryError异常。
方法区(MethodArea)
方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(RuntimeConstantPool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内存空间后就会抛出OutOfMemoryError。
运行时常量池(RuntimeConstantPool)
运行时常量池是方法区的一部分,每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到JVM后,对应的运行时常量池就被创建。运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常。栈包含Frames,当调用方法时,Frame被推送到堆栈。一个Frame包含局部变量数组、操作数栈、常量池引用。
36. 什么是乐观锁和悲观锁?
乐观锁: 对于并发间操作产生的线程安全问题持乐观状态, 乐观锁认为竞争不总是会发生, 因此它不需要持有锁, 将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量, 如果失败则表示发生冲突, 那么就应该有相应的重试逻辑。
悲观锁: 对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生, 因此每次对某资源进行操作时, 都会持有一个独占的锁, 就像synchronized, 不管三七二十一, 直接上了锁就操作资源了。
37. Hashtable 的 size()方法为什么要做同步?
同一时间只能有一条线程执行固定类的同步方法, 但是对于类的非同步方法, 可以多条线程同时访问。 所以, 这样就有问题了, 可能线程 A 在执行 Hashtable 的 put方法添加数据, 线程 B 则可以正常调用 size()方法读取 Hashtable 中当前元素的个数, 那读取到的值可能不是最新的, 可能线程 A 添加了完了数据, 但是没有对size++, 线程 B 就已经读取 size了, 那么对于线程 B 来说读取到的 size 一定是不准确的。 而给 size()方法加了同步之后, 意味着线程 B 调用 size()方法只有在线程 A调用 put 方法完毕之后才可以调用, 这样就保证了线程安全性CPU 执行代码, 执行的不是 Java 代码, 这点很关键, 一定得记住。 Java 代码最终是被翻译成机器码执行的, 机器码才是真正可以和硬件电路交互的代码。 即使你看到 Java 代码只有一行, 甚至你看到 Java 代码编译之后生成的字节码也只有一行, 也不意味着对于底层来说这句语句的操作只有一个。 一句"return count"假设被翻译成了三句汇编语句执行, 一句汇编语句和其机器码做对应, 完全可能执行完第一句, 线程就切换了。
38. 同步方法和同步块,哪种更好?
同步块, 这意味着同步块之外的代码是异步执行的, 这比同步整个方法更提升代码的效率。
请知道一条原则: 同步的范围越小越好。
39. 什么是自旋锁?
自旋锁是采用让当前线程不停地的在循环体内执行实现的, 当循环的条件被其他线程改变时才能进入临界区。
40. Runnable 和 Thread 用哪个好?
Java 不支持类的多重继承, 但允许你实现多个接口。 所以如果你要继承其他类, 也为了减少类之间的耦合性, Runnable 会更好。
41. Java 中 notify 和 notifyAll 有什么区别?
notify()方法不能唤醒某个具体的线程, 所以只有一个线程在等待的时候它才有用武之地。而 notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
42. 为什么 wait/notify/notifyAll 这些方法不在 thread 类里面?
这是个设计相关的问题, 它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。 回答这些问题的时候, 你要说明为什么把这些方法放在 Object类里是有意义的, 还有不把它放在 Thread 类里的原因。 一个很明显的原因是JAVA 提供的锁是对象级的而不是线程级的, 每个对象都有锁, 通过线程获得。 如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。 如果 wait()方法定义在 Thread 类中, 线程正在等待的是哪个锁就不明显了。 简单的说, 由于 wait,notify 和 notifyAll 都是锁级别的操作, 所以把他们定义在 Object 类中因为锁属于对象。
42. 为什么 wait 和 notify 方法要在同步块中调用?
主要是因为JavaAPI强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
43.为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒, 如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 因此, 当一个等待线程醒来时, 不能认为它原来的等待状态仍然是有效的, 在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。 这就是在循环中使用 wait()方法效果更好的原因, 你可以在 Eclipse 中创建模板调用 wait和 notify 试一试。
44. Java 中堆和栈有什么不同?
每个线程都有自己的栈内存, 用于存储本地变量, 方法参数和栈调用, 一个线程中存储的变量对其它线程是不可见的。 而堆是所有线程共享的一片公用内存区域。 对象都在堆里创建, 为了提升效率线程会从堆中弄一个缓存到自己的栈, 如果多个线程使用该变量就可能引发问题, 这时 volatile 变量就可以发挥作用了, 它要求线程从主存中读取变量的值。
45. 如何创建线程安全的单例模式?
单例模式即一个 JVM 内存中只存在一个类的对象实例分类
1、 懒汉式:类加载的时候就创建实例
2、 饿汉式:使用的时候才创建实例
46. 什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情, ServerSocket 的 accept()方法就是一直等待客户端连接。 这里的阻塞是指调用结果返回之前,当前线程会被挂起, 直到得到结果之后才会返回。 此外, 还有异步和非阻塞式方法在任务完成前就返回。
47. 提交任务时线程池队列已满会时发会生什么?
当线程数小于最大线程池数 maximumPoolSize 时就会创建新线程来处理, 而线程数大于等于最大线程池数 maximumPoolSize 时就会执行拒绝策略。
48. 多线程同步有哪几种方法?
- synchronized关键字:这是最基本的多线程同步方法。它可以应用于方法或代码块,确保一次只有一个线程可以访问被保护的资源。
- ReentrantLock:这是Java 5之后引入的一个更灵活的同步机制,它提供了与
synchronized
关键字类似的功能,但是提供了更多的控制,比如可以中断等待中的线程。 - Semaphore:这是一种计数信号量,可以控制对资源的并发访问数量。如果信号量计数为0,则线程将阻塞,直到计数增加。
- CountDownLatch:这是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。
- CyclicBarrier:这允许一组线程互相等待,直到所有线程都达到某个状态后再一起执行。
- Phaser:这是Java 7引入的一个类,用于替代
CyclicBarrier
和CountDownLatch
,它提供了更复杂的同步原语。 - Exchanger:这是一个同步点,允许两个线程交换对象。当两个线程都到达交换点时,它们会交换对象并继续执行。
- Lock和Condition:这是Java 5之后引入的更复杂的同步机制。
Lock
是一个接口,提供了比synchronized
更灵活的同步控制;Condition
则是一个类,它提供了线程之间的协调机制。
49. 什么是死锁?如何避免死锁?
死锁(Deadlock)指在两个或多个进程或线程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,这些进程或线程都将无法向前推进。
死锁通常发生在以下四个条件同时满足的情况下,这些条件又被称为Coffman条件或死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
为了避免死锁,可以采取以下几种策略:
- 破坏互斥条件:这个条件通常很难破坏,因为许多资源(如打印机、文件等)本身就是互斥的。不过,对于某些资源类型,可以通过允许多个进程同时访问来避免死锁。例如,某些数据库系统允许多个读取者同时访问数据,但只允许一个写入者。
- 破坏请求与保持条件:可以要求进程在运行前一次性地请求所有所需要的资源,而不是在需要时才请求。这种方法称为静态分配,也称为预分配。然而,这种方法可能导致资源利用率低下,并且需要程序员准确地知道每个进程可能需要的最大资源量。
- 破坏不剥夺条件:允许操作系统剥夺已分配给进程的资源。这种方法需要额外的硬件和软件支持来跟踪资源的使用情况,并在必要时进行资源的重新分配。这种策略的实现复杂度较高,并可能导致进程的执行被意外中断。
- 破坏循环等待条件:通过对资源进行编号,并要求进程按照编号的顺序请求资源,可以破坏循环等待条件。这种方法称为顺序资源分配法或银行家算法。然而,这种方法可能导致资源的利用率降低,并且需要程序员或系统管理员对资源进行适当的编号。
- 使用锁顺序:当多个线程需要获取多个锁时,确保它们总是以相同的顺序获取锁。这样可以防止发生循环等待的情况。
- 使用定时锁:尝试获取锁时设置一个超时时间,超过这个时间则放弃获取锁并释放已持有的锁,然后重试。这种方法不能保证一定能够避免死锁,但可以降低死锁发生的概率。
- 死锁检测与恢复:通过算法定期检查系统中是否存在死锁,并在检测到死锁时采取措施进行恢复。这通常需要操作系统的支持,并且可能会增加系统的开销。
50. 线程怎样拿到返回结果
Future
和Callable
是Java并发库中的两个接口,它们可以让你获取异步计算的结果。你可以在一个线程中执行一个Callable
任务,然后获取其返回的Future
对象。然后,你可以调用Future
对象的get()
方法来获取结果。这种方式也可以处理多个线程之间的数据交换。
51. 新建三个线程,如何保证它们按顺序执行?
join()方法会等待当前线程完成,也就是说,它会阻塞当前线程,直到被调用join()
的线程执行完毕。如果当前线程在等待过程中被其他线程中断,那么会抛出InterruptedException
。
还有一个重载版本的join(long millis)
方法,允许你指定一个超时时间。如果在这个超时时间内,被调用的线程还没有执行完毕,那么当前线程就会继续执行。如果在等待过程中当前线程被中断,那么也会抛出InterruptedException
。
52. 怎么控制同一时间只有3个线程运行
可以使用Semaphore
类来限制同一时间运行的线程数量。Semaphore
是一个计数信号量,可以用来控制对资源的访问。每个线程在运行之前都会尝试获取信号量。如果信号量不可用(即当前已经有3个线程在运行),那么线程将会等待,直到信号量变得可用。一旦线程获取到信号量,它就可以运行。当线程完成其工作后,它会释放信号量,允许其他线程获取信号量并运行。
53. 创建线程的有哪些方式?
- 继承 Thread 类创建线程类
- 通过 Runnable 接口创建线程类
- 通过 Callable 和 Future 创建线程
- 通过线程池创建
54. 创建线程的三种方式的对比
采用实现 Runnable、Callable 接口的方式创建多线程。
优势是: 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是: 编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。
使用继承 Thread 类的方式创建多线程
优势是: 编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。
劣势是: 线程类已经继承了 Thread 类,所以不能再继承其他父类。
Runnable 和 Callable 的区别
- Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。
- Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。
- Call 方法可以抛出异常,run 方法不可以。
- 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
55. 并行和并发有什么区别?
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:
- 并发 = 俩个人用一台电脑。
- 并行 = 俩个人分配了俩台电脑。
- 串行 = 俩个人排队使用一台电脑。
56. 线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
(2)线程体中调用了 sleep 方法使线程进入睡眠状态
(3)线程由于 IO 操作受到阻塞
(4)另外一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完
57. 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
- 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
- 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。
- 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
58. 如何停止一个正在运行的线程?
在java中有以下3种方法可以终止正在运行的线程:
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
使用interrupt方法中断线程。
59.什么是线程同步和线程互斥,有哪几种实现方式?
当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法:
- 同步代码方法:sychronized 关键字修饰的方法
- 同步代码块:sychronized 关键字修饰的代码块
- 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
- 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
60. 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同 步?
- 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
- 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码
- 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案。
61. 什么叫线程安全?servlet 是线程安全吗?
- 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
- Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
- Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
- SpringMVC 的 Controller 不是线程安全的,和 Servlet 类似的处理流程。
- Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
62. synchronized 底层实现原理?
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成,每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。
63. synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
64.多线程中 synchronized 锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻
塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
65. synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
66.synchronized 和 Lock 有什么区别?
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
67.synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量。
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
68. Java 中能创建 volatile 数组吗?
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
69. volatile 变量和 atomic 变量有什么不同?
-
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
-
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会
原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
70. volatile 能使得一个非原子操作变成原子操作吗?
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。
所以从Oracle Java Spec里面可以看到:
- 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
- 如果使用volatile修饰long和double,那么其读写都是原子操作
- 对于64位的引用地址的读写,都是原子操作
- 在实现JVM时,可以自由选择是否把读写long和double作为原子操作
- 推荐JVM实现为原子操作
71.synchronized 和 volatile 的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
72. final不可变对象,它对写并发应用有什么帮助?
- 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
- 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger 和 BigDecimal 等。
- 只有满足如下状态,一个对象才是不可变的;
- 它的状态不能在创建后再被修改;
- 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
73. Lock 接口和synchronized 对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的择。
74. CAS 的会产生什么问题?
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
75. 什么是原子类
- java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。
- 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。
简单来说就是原子类来实现CAS无锁模式的算法
76.说一下 Atomic的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
77. 构造参数参数介绍:
- Executor工厂类如何创建线程池图:
- corePoolSize 核心线程数量
- maximumPoolSize 最大线程数量
- keepAliveTime 线程保持时间,N个时间单位
- unit 时间单位(比如秒,分)
- workQueue 阻塞队列
- threadFactory 线程工厂
- handler 线程池拒绝策略
78. 线程池四种创建方式?
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
-
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
-
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
-
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
-
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
79.四种构建线程池的区别及特点?
newCachedThreadPool
特点:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制
缺点:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值
总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
newFixedThreadPool
特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)
总结:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类)
缺点:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
newSingleThreadExecutor
特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力
总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的
线程来替代它
80. 什么是线程组,为什么在 Java 中不推荐使用?
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。
81. ThreadPoolExecutor饱和策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
82. 为什么HashTable是线程安全的?
因为HasTable的内部方法都被synchronized修饰了,所以是线程安全的。其他的都和HashMap一样。
83. 讲一下ConcurrentHashMap和HashTable的不同之处?
ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
总结:
-
HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全
-
ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
-
并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
-
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
-
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。
84. 什么是并发容器的实现?
何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。
85. SynchronizedMap 和 ConcurrentHashMap 有什么区别?
- SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。
- ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
- ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。
- 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。
- 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
86. CopyOnWriteArrayList 是什么?
- CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,但前提是在非复合场景下操作,合适读多写少的场景。
- CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
87. CopyOnWriteArrayList 的缺点?
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc 或者 full gc。
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
- 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障
88. CopyOnWriteArrayList 的设计思想?
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突
89. 什么是并发队列:
消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信
并发队列是什么:并发队列多个线程以有次序共享数据的重要组件
90. 阻塞队列和非阻塞队列区别
-
当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。
-
或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。
-
或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元
-
素。
-
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲
起来。
91. 常用并发列队的介绍:
非堵塞队列:
-
ArrayDeque, (数组双端队列):ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存
储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队
列、栈的绝佳选择,性能比LinkedList还要好。 -
PriorityQueue, (优先级队列):PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自
然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造
方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 -
ConcurrentLinkedQueue, (基于链表的并发队列):ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方
式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接
口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允
许null元素。
堵塞队列:
-
DelayQueue(基于时间优先级的队列,延期阻塞队列):DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
-
ArrayBlockingQueue, (基于数组的并发阻塞队列):ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据
-
LinkedBlockingQueue, (基于链表的FIFO阻塞队列):LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
-
LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列):LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认
容量大小为Integer.MAX_VALUE。 -
PriorityBlockingQueue, (带优先级的无界阻塞队列):priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
-
SynchronousQueue (并发同步阻塞队列):SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
并发队列的常用方法
方法名 | 描述 |
---|---|
add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true, 如果队列满了就抛出异常。 |
offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插 入指定元素,成功时返回true,如果此队列已满,则返回false。 |
put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 |
take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有 值,并且该方法取得了该值。 |
poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 |
remainingCapacity() | 获取队列中剩余的空间。 |
remove(Object o) | 从队列中移除指定的值。 |
contains(Object o) | 判断队列中是否拥有该值。 |
drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 |
92. 常用的并发工具类有哪些?
- CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
- CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
- Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许
自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。