31. 迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,它可以便利并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为"轻量级"对象,因为创建它的代价小。
32. Iterator 怎么使用? 有什么特点?
Java 中的 Itertor 功能比较简单,并且只能单向移动:
(1)使用方法 iterator() 要求容器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个元素。注意:iterator() 方法是 java.lang.Iterble 接口,被 Collection 继承。
(2)使用 next() 获得序列中的下一个元素。
(3)使用 hasNext() 检查序列中是否还有元素。
(4)使用 remove() 将迭代器新返回的元素删除。
Iterator 是 Java 迭代器最简单的实现,为 List 设计的 ListIterator 具有更多的功能,它可以从两个方向遍历 List,也可以从 List 中插入和删除元素。
33. Iterator 和 ListIterator 有什么区别?
- Iterator 可以用来遍历 Set 和 List 集合,但是 ListIterator 只是能用来遍历List。
- Iterator 对集合只能是向前遍历,ListIterator 既可以向前也可以向后。
- ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
多线程
34. 并行和并发有什么区别?
- 并行是指两个或多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 在一台处理器上"同时"处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目的是充分的利用处理器的每一个核,已达到最高的处理性能。
35. 线程和进程的区别?
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。
36. 守护线程是什么?
守护线程(即 daemon thread),是个服务线程,准确地来说就是服务其他的线程。
37. 创建线程有哪几种方式?
①. 继承 Thread 类创建线程类
- 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要完成的任务。因此把 run() 方法称为执行体。
- 创建 Thread 子类的实例,即创建了线程对象。
- 调用线程对象的是 start() 方法来启动该线程。
②. 通过 Runnable 接口创建线程类
- 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的执行体。
- 创建 Runnable 实现类的实例,并依次实例作为 Thread 的 target 来创建 Thread 对象, 该 Thread 对象才是真正的线程对象。
- 调用线程对象的 start() 方法来启动该线程
③. 通过 Callable 和 Future 创建线程。
- 创建 Callable 接口的实现类,并实现 call|() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
38. 说一些 runnable 和 callable 有什么区别?
有点深的问题了,也看出一个 Java 程序员学习知识的广度。
- Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹的去执行 run() 方法中的代码而已;
- Callable 接口中的 call() 方法是有返回值的,是一个泛型,和Future、FutureTask 配合可以用来获取异步执行的结果。
Runnable执行方法是run(),Callable是call()
实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果 call方法可以抛出异常,run方法若有异常只能在内部消化
39. 线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
- 创建状态:在生成线程对象,并没有调用该对象的 start 方法,这是线程。
- 就绪状态:当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该程序设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或睡眠中回来之后,也会处于就绪状态。
- 运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
- 阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep、suspend、wait 等方法都可以导致线程阻塞。
- 死亡状态:如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。
40. sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为 sleep() 是 static 静态的方法,他不能改变对象的锁机制,当一个 synchronized 块中调用了 sleep() 方法,线程虽然进入休眠,但是对象的锁机制没有被释放,其他线程依然无法访问这个对象。
wait():wait() 是 Object 类的方法,当一个线程执行到 wait 方法时,他就进入到一个和该对象相关的等待池,同时释放对象的机锁,使其他线程能够访问,可以通过 notify,notifyAll 方法来唤醒等待的线程;
41. notify() 和 notifyAll() 有什么区别?
- 如果线程调用了对象的 wait() 方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象锁。
- 当有线程调用了 notifyAll() 方法(唤醒所有 wait 线程)或 notify() 方法(只随机唤醒一个 wait 线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify 后只有一个线程会由等待池进入锁池,而 notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待竞争。
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait() 方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它才会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
42. 线程 run() 和 start() 有什么区别?
每个线程都是通过某个特定的 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。
start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态,这里方法 run() 称为线程体,它包含了要执行的这个线程的内容,Run 方法运行结束,此线程终止。然后 CPU 在调动其他线程。
run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接调用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行是要使用 start() 方法而不是 run() 方法。
43. 创建线程池有几种方式?
①. newFixedThreadPool(int nThreads) //固定线程池 [fɪkst] [θred puːl]
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束是,线程池会补充一个新的线程。
②. newCachedThreadPool() //缓存线程池 [kæʃt] [θred puːl]
创建一个可以缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
③. newSingleThreadExecutor() //单线程执行器 [ˈsɪŋɡl] [θred] [ɪɡˈzekjətər]
这是一个单线程的 Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来代替它;它的特点是能确保依照任务在队列中的顺序来串行执行。
④. newScheduledThreadPool(int corePoolSize) //计划线程池 [ˈskedʒuːld] [θred puːl]
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。
44. 线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。 //运行-关闭-停止-整理-结束
线程池各个状态切换构架图:
45. 线程池中 submit() 和 execute() 方法有什么区别?
- 接收的参数不一样
- submit 有返回值,而 execute 没有
- submit 方便 Exception 处理
46. 在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchroized);
- 可见性:一个线程对主内存的修改可以及时的被其他线程看到,(synchronizer,volatile);
- 有序性:一个线程观察其他线程中的指令顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before 原则)。
47. 多线程锁的升级原理是什么?
在 Java 中,锁共有是个状态,级别从低到高依次为:无状态锁、偏向锁、轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示过程:
48. 什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,他是计算机系统操作乃至整个并发程序设计领域最难处理的问题之一。
49. 怎么防止死锁?
死锁的四个必要条件:
- 互斥条件:进程对所分配到的资源不允许其他资源进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完后释放该资源
- 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但又对自己获得的资源保持不放
- 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
- 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相互的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发送死锁,这些条件必然成立,而只要上述条件之一不满足就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能的避免、预防和解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下暂用资源。因此,对资源的分配要给予合理的规划。
50. ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。 Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄漏的风险。
51. 说一下 synchronized 底层实现原理
synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的 class 对象
- 同步方法快,锁是括号里面的对象
52. synchronized 和 volatile 的区别是什么?
- volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要重主内存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 仅能使用在变量级别; synchronized 则可以使用在变量、方法、和类级别的。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编辑器优化。
53. 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 锁适合代码少量的同步问题。
54. synchronized 和 reentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 RenntrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,RenntrantLock 比 synchronized 的扩展性体现在几点上:
- ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
- RenntrantLock 可以获取各种锁的信息
- ReentrantLock 可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中的 mark word。
55. 说一下 atomic 的原理?
Atomic 包中的类基本特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。我们需要先知道一个东西就是 Unsafe 类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而他之所以会标记为非安全的,是告诉你这里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
反射
56. 什么是反射?
反射主要是指程序可以访问、检查和修改它本身状态或行为的一种能力。
57. Java 反射:
在 Java运行环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对任意一个对象,能否调用它的任意一个方法
Java反射机制主要提供了以下功能:
- 在运行时判断任意一个对象的所属类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法。
58. 什么是 Java 序列化?什么情况下需要序列化?
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对讲状态再读出来。输入你可以用你自己的各种各样的方法老保存 object、states,但是 Java 给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。
什么情况下需要序列化:
a)当你想把内存中的对象保存到一个文件中或者数据库中的候:
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
59. 动态代理是什么? 有哪些应用?
动态代理:
党项要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事物等。可以给这个类创建一个代理,顾名思义就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理的应用:
- Spring的AOP
- 加事物
- 加权限
- 加日志
60. 怎么实现动态代理?
首先必须定义一个借口,还要有一个 InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类 Proxy(习惯性将其称为代理类,因为调用它的 newLinstance() 可以产生代理对象,其实它只是一个产生代理对象的工具类)。利用到 InocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。