java三年面试题(多线程篇)
1.进程和线程的区别?
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。一个进程至少包含一条线程。 进程拥有自己的内存空间。
线程(Thread)是进程的一部分,线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。所有线程共享进程的内存空间。
2.创建线程的方法?
- Thread
- Runnable
根据面向接口的设计原则,优先选用Runnable.
3.Runnable和Callable的区别?
Runnable重写run(),没有返回值;
Callable重写call(),有返回值,可抛出异常。
4.start()和run()的区别?
start()用于启动线程,run()定义线程执行内容;
start()启动线程,调用run()执行任务。
5.什么是线程安全?
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
5.实现锁的方式,及区别?
Synchronized和Lock锁
Lock锁相比与Synchronized区分的读写锁。
synchronized是java关键字,Lock是类;
synchronized无法获取锁状态,Lock可以;
synchronized自动释放锁,Lock手动释放;
synchronized锁可重入,不可中断,非公平,Lock锁可重入,可判断,公平;
Lock锁适合大量同步代码,synchronized适合少量同步。
6.Java中如何停止一个线程?
stop(),interrupt(),suspend(),定义停止标示
Stop:立即停止,杀死线程,不建议使用;
interrupt():设置停止表示,返回线程已停止,但进程代码继续执行,直到进程代码执行结束;
suspend():挂起线程,配合resume()恢复线程。suspend挂起的线程不会释放资源,可能会造成死锁;
定义停止标示:当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。 推荐使用
7.sleep()和wait()区别?
sleep底层使用suspend挂起,不会释放资源,根据定时器自动释放资源,重新执行。
wait():底层是使用另一个wait队列,释放资源,等待notify唤起,重新竞争获取锁。
8.一个线程运行时发生异常会怎样?
Java中Throwable分为Exception和Error:
Error程序会停止运行。
Exception分为RuntimeException和非运行时异常。
非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。
而RuntimeException可以处理也可以不处理,因为编译并不能检测该类异常.
这里存在两种情形:
① 如果该异常被捕获或抛出,则程序继续运行。
② 如果异常没有被捕获该线程将会停止执行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
9. 如何在两个线程间共享数据?
共享对象或变量
10.volatile谈一谈?
volatile “易变的”
volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
实现原理:
每一条线程都有自己的私有内存区,当一个线程修改一个volatile时会首先将值刷新到自己的内存区,然后通知所有使用该volatile的线程使其volatile修饰的变量无效;然后线程将本地内存中值刷新到内存中,然后通知所有其他线程去内存中读取该变量,刷新到各自的内存区。
volatile特性:
·可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
·原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
11.synchronized谈一谈?
在JVM规范中synchronized的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。
·对于普通同步方法,锁就是当前实例对象。
·对于静态同步方法,锁就是当前类的Class对象。
·对于同步方法块,锁是synchronized括号中配置的对象。
12. synchronized有哪几种状态?
偏向锁/轻量级锁/重量级锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
13. 什么是自旋锁?
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
14.什么是乐观锁/悲观锁?
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
15.可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
16.原子操作实现原理
处理器实现原理操作:
1)使用总线锁保证原子性
2)使用缓存锁保证原子性
17.什么是CAS?
CAS Compare and Swap , CAS操作需要输入两个数值,一个旧值和一个新值,在操作期间先比较旧值有没有发生变化,如果没有,交换新值;如果有,不交换。
18. 谈谈final?
对于final域,编译器和处理器遵循两个重排序规则。
1)在构造函数内对一个final域的写入,与随后初次读这个final域,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
任何不希望域被改变的时候都可以使用final。
19.了解重排序?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
重排序分3种类型:
1)编译器优化的重排序。
2)指令级并行的重排序。
3)内存系统的重排序。
20. 线程有那些状态?
NEW:初始状态,线程被构建,但是还没有调用start();
RUNNABLE:运行状态,java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”;
BLOCKED:阻塞状态,表示线程阻塞于锁;
WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断);
TIME_WAITING:超时等待状态,它在指定时间自行返回。
TERMINATED:终止状态,表示当前线程已经执行完毕。
21.什么是AQS?
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,同步器提供了3个方法(getState(),setState(int newState),compareAndSetState(int expect,int update))进行操作,因为它们能够保证状态的改变是安全的。同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。
同步器是实现锁的关键。锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作。
21.死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
22.活锁和死锁有什么区别?
活锁和死锁的主要区别是前者 进程的状态可以改变但是却不能继续执行。
23.Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
24. 线程安全的集合都了解那些?如何实现线程安全的?
ConcurrentHashMap :
底层使用数组+链接表+红黑树实现,多线程同步时数据进行分片存储,达到线程安全的结果。
ConcurrentLinkedQueue:
如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。
25. Fork/Join框架
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
26.原子操作类有哪些?
原子更新基本类型类:
·AtomicBoolean:原子更新布尔类型。
·AtomicInteger:原子更新整型。
·AtomicLong:原子更新长整型。
原子更新数组:
·AtomicIntegerArray:原子更新整型数组里的元素。
·AtomicLongArray:原子更新长整型数组里的元素。
·AtomicReferenceArray:原子更新引用类型数组里的元素。
·AtomicIntegerArray类主要是提供原子的方式更新数组里的整型。
原子更新引用类型:
·AtomicReference:原子更新引用类型。
·AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
·AtomicMarkableReference:原子更新带有标记位的引用类型。
原子更新字段类
·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
·AtomicLongFieldUpdater:原子更新长整型字段的更新器。
·AtomicStampedReference:原子更新带有版本号的引用类型。
27.CountDownLatch和CyclicBarrier的区别
CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
28.Semaphore谈一谈?
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
如虽然有30个线程在执行,但是只允许10个并发执行。
29.线程池有什么好处?
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
30.线程池的实现原理
当提交一个新任务到线程池时,线程池的处理流程如下。
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
30.什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
31. Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
32. java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。