目录
5.synchronized 和 volatile 的区别是什么?
7.synchronized 和 ReentrantLock 区别是什么?
8.wait()、notify()、notifyAll()多线程通讯三大法器
20.线程池中 submit()和 execute()方法有什么区别?
多线程
1. 并行和并发有什么区别?
- 并行(Parallel Programming)多个CPU实例或者多台机器同时执行一段逻辑,是真正的同时
- 并发(Concurrent)通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,并发往往在场景中有公共的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力
- 并行是指俩个或者多个事件在同一时刻发生;而并发是指俩个或者多个事件在同一时间间隔发送
- 并行在部在不同实体上的多个事件,并发实在同一实体的多个事件
- 在一台处理器上"同时"处理多个任务,在多台处理器上同时处理多个任务。如hadoop集群
多以并发编程的目标是充分利用处理器的每一核,以达到最高处理性能
2.线程和进程的区别?
- 一个进程可以包含多个线程。
- 线程的划分尺度小于进程,使得多线程程序的并发性提高
- 进程在执行过程中拥有独立的内存单元,而多个线程共享此内存,从而极大提高了程序的运行效率
- 线程不能独立执行
- 进程出现的目的,是为了更好的利用CPU资源
- 线程就是运行在进程上下文中的逻辑流。线程是操作系统能够进行运算调度的最小单位。
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是CPU调度和分配的基本单位,是比程序更小的独立运行的基本单位。同一个进程的多个线程可以并发执行。
3.线程和协程的区别
说到协程,我们首先回顾以下线程与进程这两个概念。在操作系统(os)级别,有进程(process)和线程(thread)两个我们看不到但又实际存在的“东西”,这两个东西都是用来模拟“并行”的,写操作系统的程序员通过用一定的策略给不同的进程和线程分配CPU计算资源,来让用户“以为”几个不同的事情在“同时”进行“。在单CPU上,是os代码强制把一个进程或者线程挂起,换成另外一个来计算,所以,实际上是串行的,只是“概念上的并行”。在现在的多核的cpu上,线程可能是“真正并行的”。
- 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
- 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
- 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
- 一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
- 协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
- 因此,协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。
- 线程是操作系统调度,协程是用户态调度。
4.Synchronized 底层实现原理及用法
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
-
普通同步方法,锁是当前实例对象
-
静态同步方法,锁是当前类的class对象
-
同步方法块,锁是括号里面的对象
5.synchronized 和 volatile 的区别是什么?
-
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
-
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
-
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
-
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
6.synchronized 和 Lock 有什么区别?
-
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
-
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
-
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
-
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
-
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
-
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
7.synchronized 和 ReentrantLock 区别是什么?
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
-
ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
-
ReentrantLock可以获取各种锁的信息
-
ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
8.wait()、notify()、notifyAll()多线程通讯三大法器
- wait:让持有该对象锁的线程等待;
- notify: 唤醒任何一个持有该对象锁的线程;
- notifyAll: 唤醒所有持有该对象锁的线程;
- 它们 3 个的关系是,调用对象的 wait 方法使线程暂停运行,通过 notify/ notifyAll 方法唤醒调用 wait 暂时的线程。
- 然而,它们并不是 Thread 类中的方法,而是 Object 类中的,为什么呢? 因为每个对象都有监视锁,线程要操作某个对象当然是要获取某个对象的锁了,而不是线程的锁。
- 调用对象的 wait, notify, notifyAll 方法需要拥有对象的监视器锁,即它们只能在同步方法(块)中使用;
- 调用 wait 方法会使用线程暂停并让出 CPU 资源,同时释放持有的对象的锁;
- 多线程使用 notify 容易发生死锁,一般使用 notifyAll;
9.sleep()与wait()的区别
- sleep is the method of Thread,but the wait is the method of Object.
- sleep will not release the object monitor(Lock),but the wait will be release the monitor and add to the Object monitor waiting queue.
- use sleep not depend on the monitor,but wait need.
- The sleep method not need be wakeup,but wait need.(wait(10))
- 使用限制:sleep 在任何地方都能使用,使用 wait 方法则必须放在 synchronized 块里面,都需要捕获 InterruptedException 异常,sleep休眠时间到了,继续执行当前任务,wait需要notify/notifyAll唤醒,或者设置时间,时间到科重新去竞争锁。
- 使用场景:sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
- 所属类:sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。为什么要这样设计呢?因为 sleep 是让当前线程休眠,不涉及到对象类,也不需要获得对象的锁,所以是线程类的方法。wait 是让获得对象锁的线程实现等待,前提是要楚获得对象的锁,所以是类的方法。
- 释放锁:wait 可以释放当前线程对 lock 对象锁的持有,而 sleep 则不会。
- 线程切换:sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的
10.yield 和 sleep 的异同
- yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
- yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。
- yield 不能被中断,而 sleep 则可以接受中断。
yield 即 “谦让”,也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
在多线程的情况可以配合优先级使用
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
11.start和run的区别
- 其中 Thread 类也是实现了 Runnable 接口,而 Runnable 接口定义了唯一的一个 run() 方法,所以基于 Thread 和 Runnable 创建多线程都需要实现 run() 方法,是多线程真正运行的主方法。
- 而 start() 方法则是 Thread 类的方法,用来异步启动一个线程,然后主线程立刻返回。该启动的线程不会马上运行,会放到等待队列中等待 CPU 调度,只有线程真正被 CPU 调度时才会调用 run() 方法执行。
- 所以 start() 方法只是标识线程为就绪状态的一个附加方法,其中 start0() 是一个本地 native 方法。
-
请注意,start() 方法被标识为 synchronized 的,即为了防止被多次启动的一个同步操作。
-
那么你会问了,为什么要有两个方法,直接用一个 run() 方法不就行了吗!? 还真不行,如果直接调用 run() 方法,那就等于调用了一个普通的同步方法,达不到多线程运行的异步执行。
12.线程组
- 线程组使用 java.lang.ThreadGroup 类定义,它有两个构造方法,第二个构造方法允许线程组有父类线程组,也就是说一个线程组可以多个子线程组。
- java.lang.ThreadGroup#activeCount() 获取当前线程组内的运行线程数
- java.lang.ThreadGroup#interrupt () 中断线程组内的所有线程
- java.lang.ThreadGroup#list() 使用 System.out 打印出所有线程信息
13.线程中断
- java.lang.Thread -> interrupt(),中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。
- java.lang.Thread -> isInterrupted(),判断目标线程是否被中断,不会清除中断标记。
- java.lang.Thread -> interrupted,判断目标线程是否被中断,会清除中断标记。
- thread.interrupt()给程序发出中断信息,有响应中断信号的逻辑程序中断,没有的话会中断失败
- 可以看出 sleep() 方法被中断后会清除中断标记,所以循环会继续运行。
- 在 sleep() 方法被中断并清除标记后手动重新中断当前线程,然后程序接收中断信号返回退出。
14.如何监控 Java 线程池运行状态
总线程数 = 排队线程数 + 活动线程数 + 执行完成的线程数。
ThreadPoolExecutor
提供了相应的API
private static ExecutorService es = new ThreadPoolExecutor(50, 100, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(100000));
int queueSize = tpe.getQueue().size();当前排队线程数
int activeCount = tpe.getActiveCount();当前活动线程数
long completedTaskCount = tpe.getCompletedTaskCount();"执行完成线程数
long taskCount = tpe.getTaskCount();总线程数
15.jion
- join()是线程类
Thread
的方法 - Waits for this thread to die.等待这个线程结束,也就是说当前线程等待这个线程结束后再继续执行,下面来看这个示例就明白了。
- 底层使用wait方法实现的
16.说一下runnable和callable的区别?
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
- allable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
17. 线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
- 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
-
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
-
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
-
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
-
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
18.创建线程池有哪几种方式?
- newFixedThreadPool(int nThreads)创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
- newCachedThreadPool()创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
- newSingleThreadExecutor()这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
- newScheduledThreadPool(int corePoolSize)创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
- newWorkStealingPool(),Runtime.getRuntime().availableProcessors(),根据CPU的核数,JDK1.8
19.线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
20.线程池中 submit()和 execute()方法有什么区别?
-
接收的参数不一样
-
submit有返回值,而execute没有
-
submit方便Exception处理
21.在 java 程序中怎么保证多线程的运行安全?
线程安全主要在体现在这三个方面:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的 修改i可以及时的别其他线程看到,(synchronized,volatile)
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察一般结果一般杂乱无序(happens-before)
22. 什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
- 尽量一个线程只获取一个锁
- 一个线程只占用一个资源
- 尝试使用定时锁,至少能保证锁最终会被释放
23.ThreadLocal 是什么?有哪些使用场景
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
24. 说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。