Java核心36问

1. 谈谈你对Java平台的理解?java

  Java 自己是一种面向对象的语言,最显著的特性有两个方面,一是所谓的“书写一次,处处运行”(Write once, run anywhere),可以很是容易地得到跨平台能力;另外就是垃圾收集(GC, Garbage Collection),Java 经过垃圾收集器(Garbage Collector)回收分配内存,大部分状况下,程序员不须要本身操心内存的分配和回收。程序员

咱们平常会接触到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。JRE,也就是 Java 运行环境,包含了 JVM 和 Java 类库,以及一些模块等。而 JDK 能够看做是JRE 的一个超集,提供了更多工具,好比编译器、各类诊断工具等。算法

对于“Java 是解释执行”这句话,这个说法不太准确。咱们开发的 Java 的源代码,首先经过Javac 编译成为字节码(bytecode),而后,在运行时,经过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。可是常见的 JVM,好比咱们大多数状况使用的 Oracle JDK提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)编译器,也就是一般所说的动态编译器,JIT 可以在运行时将热点代码编译成机器码,这种状况下部分热点代码就属于编译执行,而不是解释执行了。sql

2. Exception和Error有什么区别?数据库

  Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才能够被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception 和 Error 体现了 Java 平台设计者对不一样异常状况的分类。Exception 是程序正常运行中,能够预料的意外状况,可能而且应该被捕获,进行相应处理。编程

Error 是指在正常状况下,不大可能出现的状况,绝大部分的 Error 都会致使程序(好比 JVM自身)处于非正常的、不可恢复状态。既然是非正常状况,因此不便于也不须要捕获,常见的好比 OutOfMemoryError 之类,都是 Error 的子类。后端

Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面我介绍的不可查的 Error,是Throwable 不是 Exception。不检查异常就是所谓的运行时异常,相似 NullPointerException、ArrayIndexOutOfBoundsException 之类,一般是能够编码避免的逻辑错误,具体根据须要来判断是否须要捕获,并不会在编译期强制要求。设计模式

3. 谈谈final、finally、 finalize有什么不一样?数组

  final 能够用来修饰类、方法、变量,分别有不一样的意义,final 修饰的 class 表明不能够继承扩展,final 的变量是不能够修改的,而 final 的方法也是不能够重写的(override)。缓存

  finally 则是 Java 保证重点代码必定要被执行的一种机制。咱们可使用 try-finally 或者 try-catch-finally 来进行相似关闭 JDBC 链接、保证 unlock 锁等动做。

  finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制如今已经不推荐使用,而且在 JDK 9 开始被标记为deprecated。

4. 强引用、软引用、弱引用、幻象引用有什么区别?

  不一样的引用类型,主要体现的是对象不一样的可达性(reachable)状态和对垃圾收集的影响。所谓强引用("Strong" Reference),就是咱们最多见的普通对象引用,只要还有强引用指向一个对象,就能代表对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,若是没有其余的引用关系,只要超过了引用的做用域或者显式地将相应(强)引用赋值为 null,就是能够被垃圾收集的了,固然具体回收时机仍是要看垃圾收集策略。

  软引用(SoftReference),是一种相对强引用弱化一些的引用,可让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出OutOfMemoryError 以前,清理软引用指向的对象。软引用一般用来实现内存敏感的缓存,若是还有空闲内存,就能够暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

  弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就能够用来构建一种没有特定约束的关系,好比,维护一种非强制性的映射关系,若是试图获取时对象还在,就使用它,不然重现实例化。它一样是不少缓存实现的选择。

  对于幻象引用,有时候也翻译成虚引用,你不能经过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 之后,作某些事情的机制,好比,一般用来作所谓的 Post-Mortem 清理机制,我在专栏上一讲中介绍的 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的建立和销毁。

5. String、StringBuffer、StringBuilder有什么区别?

  String 是 Java 语言很是基础和重要的类,提供了构造和管理字符串的各类基本逻辑。它是典型的 Immutable 类,被声明成为 final class,全部属性也都是 final 的。也因为它的不可变性,相似拼接、裁剪字符串等动做,都会产生新的 String 对象。因为字符串操做的广泛性,因此相关操做的效率每每对应用性能有明显影响。

  StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,它是 Java 1.5中新增的,咱们能够用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,因此除非有线程安全的须要,否则仍是推荐使用它的后继者,也就是StringBuilder。

  StringBuilder 在能力上和 StringBuffer 没有本质区别,可是它去掉了线程安全的部分,有效减少了开销,是绝大部分状况下进行字符串拼接的首选。

6. 动态代理是基于什么原理?

  反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。经过反射咱们能够直接操做类或者对象,好比获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至能够运行时修改类定义。

  动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,不少场景都是利用相似机制作到的,好比用来包装 RPC 调用、面向切面的编程(AOP)。实现动态代理的方式不少,好比 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其余的实现方式,好比利用传说中更高性能的字节码操做机制,相似 ASM、cglib(基于 ASM)、Javassist 等。

7. int和Integer有什么区别?

  int 是咱们常说的整形数字,是 Java 的 8 个原始数据类型(Primitive Types,boolean、byte、short、char、int、float、double、long)之一。Java 语言虽然号称一切都是对象,但原始数据类型是例外。

  Integer 是 int 对应的包装类,它有一个 int 类型的字段存储数据,而且提供了基本操做,好比数学运算、int 和字符串之间转换等。在 Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java 能够根据上下文,自动进行转换,极大地简化了相关编程。

  关于 Integer 的值缓存,这涉及 Java 5 中另外一个改进。构建 Integer 对象的传统方式是直接调用构造器,直接 new 一个对象。可是根据实践,咱们发现大部分数据操做都是集中在有限的、较小的数值范围,于是,在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc,这个值默认缓存是 -128 到 127 之间。

8. 对比Vector、ArrayList、LinkedList有何区别?

  这三者都是实现集合框架中的 List,也就是所谓的有序集合,所以具体功能也比较近似,好比都提供按照位置进行定位、添加或者删除的操做,都提供迭代器以遍历其内容等。但由于具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不一样。

  Verctor 是 Java 早期提供的线程安全的动态数组,若是不须要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,能够根据须要自动的增长容量,当数组已满时,会建立新的数组,并拷贝原有数组数据。

  ArrayList 是应用更加普遍的动态数组实现,它自己不是线程安全的,因此性能要好不少。与Vector 近似,ArrayList 也是能够根据须要调整容量,不过二者的调整逻辑有所区别,Vector在扩容时会提升 1 倍,而 ArrayList 则是增长 50%。

  LinkedList 顾名思义是 Java 提供的双向链表,因此它不须要像上面两种那样调整容量,它也不是线程安全的。

9.对比Hashtable、HashMap、TreeMap有什么不一样?

  Hashtable、HashMap、TreeMap 都是最多见的一些 Map 实现,是以键值对的形式存储和操做数据的容器类型。

  Hashtable 是早期 Java 类库提供的一个哈希表实现,自己是同步的,不支持 null 键和值,因为同步致使的性能开销,因此已经不多被推荐使用。

  HashMap 是应用更加普遍的哈希表实现,行为上大体上与 HashTable 一致,主要区别在于HashMap 不是同步的,支持 null 键和值等。一般状况下,HashMap 进行 put 或者 get 操做,能够达到常数时间的性能,因此它是绝大部分利用键值对存取场景的首选,好比,实现一个用户 ID 和用户信息对应的运行时存储结构。

  TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不一样,它的 get、put、remove 之类操做都是 O(log(n))的时间复杂度,具体顺序能够由指定的 Comparator 来决定,或者根据键的天然顺序来判断。

10. 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

  Java 提供了不一样层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),咱们能够调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),可是它们都是利用很是粗粒度的同步方式,在高并发状况下,性能比较低下。另外,更加广泛的选择是利用并发包提供的线程安全容器类,它提供了:

  •   各类并发容器,好比 ConcurrentHashMap、CopyOnWriteArrayList。
  •  各类线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
  •  各类有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,好比基于分离锁实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,整体来讲,并发包内提供的容器通用场景,远优于早期的简单同步实现。

11. Java提供了哪些IO方式? NIO如何实现多路复用?

  首先,传统的 java.io 包,它基于流模型实现,提供了咱们最熟知的一些 IO 功能,好比 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动做完成以前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。

12. Java有几种文件拷贝方式?哪种最高效?

  Java 有多种比较典型的文件拷贝实现方式,好比:利用 java.io 类库,直接为源文件构建一个FileInputStream 读取,而后再为目标文件构建一个FileOutputStream,完成写入工做。或者,利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。固然,Java 标准类库自己已经提供了几种 Files.copy 的实现。对于 Copy 的效率,这个其实与操做系统和配置等状况相关,整体上来讲,NIO  transferTo/From 的方式可能更快,由于它更能利用现代操做系统底层机制,避免没必要要拷贝和上下文切换。

13. 谈谈接口和抽象类有什么区别?

  接口是对行为的抽象,它是抽象方法的集合,利用接口能够达到 API 定义和实现分离的目的。

接口,不能实例化;不能包含任何很是量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了很是多的接口,好比 java.util.List。

  抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和通常的 Java 类并无太大区别,能够有一个或者多个抽象方法,也能够没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,而后经过继承的方式达到代码复用的目的。Java 标准库中,好比 collection 框架,不少通用部分就被抽取成为抽象类,例如 java.util.AbstractList。

  Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,咱们能够参考 Java 标准库中的 ArrayList。

14. 谈谈你知道的设计模式?

  大体按照模式的应用目标分类,设计模式能够分为建立型模式、结构型模式和行为型模式。

  • 建立型模式,是对对象建立过程的各类问题和解决方案的总结,包括各类工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
  • 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
  • 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。

15. synchronized和ReentrantLock有什么区别呢?

  synchronized 是 Java 内建的同步机制,因此也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其余试图获取的线程只能等待或者阻塞在那里。

  在 Java 5 之前,synchronized 是仅有的同步手段,在代码中, synchronized 能够用来修饰方法,也可使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法所有语句用synchronized 块包起来。

  ReentrantLock,一般翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。再入锁经过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了不少实用的方法,可以实现不少 synchronized 没法作到的细节控制,好比能够控制 fairness,也就是公平性,或者利用定义条件等。可是,编码中也须要注意,必需要明确调用 unlock() 方法释放,否则就会一直持有该锁。

  synchronized 和 ReentrantLock 的性能不能一律而论,早期版本 synchronized 在不少场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。

16. synchronized底层如何实现?什么是锁的升级、降级?

  在回答这个问题前,先简单复习一下上一讲的知识点。synchronized 代码块是由一对儿monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。

  在 Java 6 以前,Monitor 的实现彻底是依靠操做系统内部的互斥锁,由于须要进行用户态到内核态的切换,因此同步操做是一个无差异的重量级操做。现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不一样的 Monitor 实现,也就是常说的三种不一样的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

  所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不一样的竞争情况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操做(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁。这样作的假设是基于在不少应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁能够下降无竞争开销。

  若是有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就须要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操做 Mark Word 来试图获取锁,若是重试成功,就使用普通的轻量级锁;不然,进一步升级为重量级锁。

  我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,而后试图进行降级。

17. 一个线程两次调用start()方法会出现什么状况?

  Java 的线程是不容许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,屡次调用 start 被认为是编程错误。关于线程生命周期的不一样状态,在 Java 5 之后,线程状态被明肯定义在其公共内部枚举类型java.lang.Thread.State 中,分别是:

  •   新建(NEW),表示线程被建立出来还没真正启动的状态,能够认为它是个 Java 内部状态。
  •   就绪(RUNNABLE),表示该线程已经在 JVM 中执行,固然因为执行须要计算资源,它多是正在运行,也可能还在等待系统分配给它 CPU 片断,在就绪队列里面排队。在其余一些分析中,会额外区分一种状态 RUNNING,可是从 Java API 的角度,并不能表示出来。
  •  阻塞(BLOCKED),这个状态和咱们前面两讲介绍的同步很是相关,阻塞表示线程在等待Monitor lock。好比,线程试图经过 synchronized 去获取某个锁,可是其余线程已经独占了,那么当前线程就会处于阻塞状态。
  • 等待(WAITING),表示正在等待其余线程采起某些操做。一个常见的场景是相似生产者消费者模式,发现任务条件还没有知足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,而后经过相似 notify 等动做,通知消费线程能够继续工做了。Thread.join() 也会令线程进入等待状态。
  • 计时等待(TIMED_WAIT),其进入条件和等待状态相似,可是调用的是存在超时条件的方法,好比 wait 或 join 等方法的指定超时版本,以下面示例:
    public final native void wait(long timeout) throws InterruptedException;
  • 终止(TERMINATED),无论是意外退出仍是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫做死亡。

  在第二次调用 start() 方法的时候,线程可能处于终止或者其余(非 NEW)状态,可是不论如何,都是不能够再次启动的。

18. 什么状况下Java程序会产生死锁?如何定位、修复?

  死锁是一种特定的程序状态,在实体之间,因为循环依赖致使彼此一直处于等待之中,没有任何个体能够继续前进。死锁不只仅是在线程之间会发生,存在资源独占的进程之间一样也可能出现死锁。一般来讲,咱们大可能是聚焦在多线程场景中的死锁,指两个或多个线程之间,因为互相持有对方须要的锁,而永久处于阻塞的状态。定位死锁最多见的方式就是利用 jstack 等工具获取线程栈,而后定位互相之间的依赖关系,进而找到死锁。若是是比较明显的死锁,每每 jstack 等就能直接定位,相似 JConsole 甚至能够在图形界面进行有限的死锁检测。若是程序运行时发生了死锁,绝大多数状况下都是没法在线解决的,只能重启、修正程序自己问题。因此,代码开发阶段互相审查,或者利用工具进行预防性排查,每每也是很重要的。

19. Java并发包提供了哪些并发工具类?

  咱们一般所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各类基础工具类,具体主要包括几个方面:

  • 提供了比 synchronized 更加高级的各类同步结构,包括 CountDownLatch、CyclicBarrier、Sempahore 等,能够实现更加丰富的多线程操做,好比利用 Semaphore 做为资源控制器,限制同时进行工做的线程数量。
  • 各类线程安全的容器,好比最多见的 ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者经过相似快照机制,实现线程安全的动态数组CopyOnWriteArrayList 等。
  • 各类并发队列实现,如各类 BlockedQueue 实现,比较典型的 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue 等。
  • 强大的 Executor 框架,能够建立各类不一样类型的线程池,调度任务运行等,绝大部分状况下,再也不须要本身从头实现线程池和任务调度器。

20. 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

  有时候咱们把并发包下面的全部容器都习惯叫做并发容器,可是严格来说,相似ConcurrentLinkedQueue 这种“Concurrent*”容器,才是真正表明并发。关于问题中它们的区别:

  • Concurrent 类型基于 lock-free,在常见的多线程访问场景,通常能够提供较高吞吐量。
  • 而 LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。

  不知道你有没有注意到,java.util.concurrent 包提供的容器(Queue、List、Set)、Map,从命名上能够大概区分为 Concurrent、CopyOnWrite和 Blocking* 等三类,一样是线程安全容器,能够简单认为:

  • Concurrent 类型没有相似 CopyOnWrite 之类容器相对较重的修改开销。
  • 可是,凡事都是有代价的,Concurrent 每每提供了较低的遍历一致性。你能够这样理解所谓的弱一致性,例如,当利用迭代器遍历时,若是容器发生修改,迭代器仍然能够继续进行遍历。
  • 与弱一致性对应的,就是我介绍过的同步容器常见的行为“fast-fail”,也就是检测到容器在遍历过程当中发生了修改,则抛出 ConcurrentModificationException,再也不继续遍历。
  • 弱一致性的另一个体现是,size 等操做准确性是有限的,未必是 100% 准确。
  • 与此同时,读取的性能具备必定的不肯定性。

21. Java并发类库提供的线程池有哪几种? 分别有什么特色?

  一般开发者都是利用 Executors 提供的通用线程池建立方法,去建立不一样配置的线程池,主要区别在于不一样的 ExecutorService 类型或者不一样的初始参数。Executors 目前提供了 5 种不一样的线程池建立配置:

  • newCachedThreadPool(),它是一种用来处理大量短期工做任务的线程池,具备几个鲜明特色:它会试图缓存线程并重用,当无缓存线程可用时,就会建立新的工做线程;若是线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 做为工做队列。
  • newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工做队列,任什么时候候最多有 nThreads 个工做线程是活动的。这意味着,若是任务数量超过了活动队列数目,将在工做队列中等待空闲线程出现;若是有工做线程退出,将会有新的工做线程被建立,以补足指定的数目 nThreads。
  • newSingleThreadExecutor(),它建立的是个 ScheduledExecutorService,也就是能够进行定时或周期性的工做调度。工做线程数目被限制为 1,因此它保证了全部任务的都是被顺序执行,最多会有一个任务处于活动状态,而且不容许使用者改动线程池实例,所以能够避免其改变线程数目。
  • newScheduledThreadPool(int corePoolSize),一样是 ScheduledExecutorService,区别在于它会保持 corePoolSize 个工做线程。
  • newWorkStealingPool(int parallelism),这是一个常常被人忽略的线程池,Java 8 才加入这个建立方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

22. AtomicInteger底层实现原理是什么?如何在本身的产品代码中应用CAS操做?

  AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操做,其原子性操做的实现是基于 CAS(compare-and-swap)技术。

  所谓 CAS,表征的是一些列操做的集合,获取当前数值,进行一些运算,利用 CAS 指令试图进行更新。若是当前数值未变,表明没有其余线程进行并发修改,则成功更新。不然,可能出现不一样的选择,要么进行重试,要么就返回一个成功或者失败的结果。

  从 AtomicInteger 的内部属性能够看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操做;以 volatile 的 value 字段,记录数值,以保证可见性。

具体的原子操做细节,能够参考任意一个原子更新方法,好比下面的 getAndIncrement。Unsafe 会利用 value 字段的内存地址偏移,直接完成操做。

  由于 getAndIncrement 须要返归数值,因此须要添加失败重试逻辑。

  而相似 compareAndSet 这种返回 boolean 类型的函数,由于其返回值表现的就是成功与否,因此不须要重试。

  CAS 是 Java 并发中所谓 lock-free 机制的基础。

23. 请介绍类加载过程,什么是双亲委派模型?

  通常来讲,咱们把 Java 的类加载过程分为三个主要步骤:加载、连接、初始化,具体行为在Java 虚拟机规范里有很是详细的定义。

首先是加载阶段(Loading),它是 Java 将字节码数据从不一样的数据源读取到 JVM 中,并映射为 JVM 承认的数据结构(Class 对象),这里的数据源多是各类各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;若是输入数据不是 ClassFile 的结构,则会抛出ClassFormatError。

加载阶段是用户参与的阶段,咱们能够自定义类加载器,去实现本身的类加载过程。

第二阶段是连接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM 运行的过程当中。这里可进一步细分为三个步骤:

  • 验证(Verification),这是虚拟机安全的重要保障,JVM 须要核验字节信息是符合 Java 虚拟机规范的,不然就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害JVM 的运行,验证阶段有可能触发更多 class 的加载。
  • 准备(Preparation),建立类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所须要的内存空间,不会去执行更进一步的 JVM 指令。
  • 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java 虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。
  • 最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动做,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

  再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,不然尽可能将这个任务代理给当前加载器的父加载器去作。使用委派模型的目的是避免重复加载 Java 类型。

24. 有哪些方法能够在运行时动态生成一个Java类?

  咱们能够从常见的 Java 类来源分析,一般的开发过程是,开发者编写 Java 代码,调用 javac编译成 class 文件,而后经过类加载机制载入 JVM,就成为应用运行时可使用的 Java 类了。从上面过程获得启发,其中一个直接的方式是从源码入手,能够利用 Java 程序生成一段源码,而后保存到文件等,下面就只须要解决编译问题了。

  有一种笨办法,直接用 ProcessBuilder 之类启动 javac 进程,并指定上面生成的文件做为输入,进行编译。最后,再利用类加载器,在运行时加载便可。

  前面的方法,本质上仍是在当前程序进程以外编译的,那么还有没有不这么 low 的办法呢?你能够考虑使用 Java Compiler API,这是 JDK 提供的标准 API,里面提供了与 javac 对等的编译器功能,具体请参考java.compiler相关文档。进一步思考,咱们一直围绕 Java 源码编译成为 JVM 能够理解的字节码,换句话说,只要是符合 JVM 规范的字节码,无论它是如何生成的,是否是均可以被 JVM 加载呢?咱们能不能直接生成相应的字节码,而后交给类加载器去加载呢?

  固然也能够,不过直接去写字节码难度太大,一般咱们能够利用 Java 字节码操纵工具和类库来实现,好比在专栏第 6 讲中提到的ASM、Javassist、cglib 等。

25. 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?

  一般能够把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程惟一的。

  首先,程序计数器(PC,Program Counter Register)。在 JVM 规范中,每一个线程都有它本身的程序计数器,而且任什么时候间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,若是是在执行本地方法,则是未指定值(undefined)。

  第二,Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每一个线程在建立时都会建立一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,一般叫做当前帧,方法所在的类叫做当前类。若是在该方法中调用了其余方法,对应的新的栈帧会被建立出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操做只有两个,就是对栈帧的压栈和出栈。栈帧中存储着局部变量表、操做数(operand)栈、动态连接、方法正常退出或者异常退出的定义等。

  第三,堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎全部建立的 Java 对象实例都是被直接分配在堆上。堆被全部的线程共享,在虚拟机启动时,咱们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。理所固然,堆也是垃圾收集器重点照顾的区域,因此堆内空间还会被不一样的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。

  第四,方法区(Method Area)。这也是全部线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。因为早期的 Hotspot JVM 实现,不少人习惯于将方法区称为永久代(PermanentGeneration)。Oracle JDK 8 中将永久代移除,同时增长了元数据区(Metaspace)。

  第五,运行时常量池(Run-Time Constant Pool),这是方法区的一部分。若是仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各类信息,还有一项信息就是常量池。Java 的常量池能够存放各类常量信息,无论是编译期生成的各类字面量,仍是须要在运行时决定的符号引用,因此它比通常语言的符号表存储的信息更加宽泛。

  第六,本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是很是类似的,支持对本地方法的调用,也是每一个线程都会建立一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一起区域,这彻底取决于技术实现的决定,并未在规范中强制。

26. 如何监控和诊断JVM堆内和堆外内存使用?

  了解 JVM 内存的方法有不少,具体能力范围也有区别,简单总结以下:

  可使用综合性的图形化工具,如 JConsole、VisualVM(注意,从 Oracle JDK 9 开始,VisualVM 已经再也不包含在 JDK 安装包中)等。这些工具具体使用起来相对比较直观,直接链接到 Java 进程,而后就能够在图形化界面里掌握内存使用状况。

  以 JConsole 为例,其内存页面能够显示常见的堆内存和各类堆外部分使用状态。

  • 也可使用命令行工具进行运行时查询,如 jstat 和 jmap 等工具都提供了一些选项,能够查看堆、方法区等使用数据。
  • 或者,也可使用 jmap 等提供的命令,生成堆转储(Heap Dump)文件,而后利用 jhat或 Eclipse MAT 等堆转储分析工具进行详细分析。若是你使用的是 Tomcat、Weblogic 等 Java EE 服务器,这些服务器一样提供了内存管理相关的功能。
  • 另外,从某种程度上来讲,GC 日志等输出,一样包含着丰富的信息。

  这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可使用JDK 自带的 Native Memory Tracking(NMT)特性,它会从 JVM 本地内存分配的角度进行解读。

27. Java常见的垃圾收集器有哪些?

  实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不一样厂商(IBM、Oracle),不一样版本的 JVM,提供的选择也不一样。接下来,我来谈谈最主流的 OracleJDK。

Serial GC,它是最古老的垃圾收集器,“Serial”体如今其收集工做是单线程的,而且在进行垃圾收集过程当中,会进入臭名昭著的“Stop-The-World”状态。固然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,因此一直是 Client 模式下 JVM 的默认选项。

从年代的角度,一般将其老年代实现单独称做 Serial Old,它采用了标记 - 整理(Mark- Compact)算法,区别于新生代的复制算法。

Serial GC 的对应 JVM 参数是:-XX:+UseSerialGC

  • ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最多见的应用场景是配合老年代的 CMS GC 工做,下面是对应参数-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
  • CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽可能减小停顿时间,这一点对于 Web 等反应时间敏感的应用很是重要,一直到今天,仍然有不少系统使用 CMS GC。可是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,因此难以免在长时间运行等状况下发生 full GC,致使恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。
  • Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称做是吞吐量优先的 GC。它的算法和 Serial GC 比较类似,尽管实现要复杂的多,其特色是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。开启选项是:-XX:+UseParallelGC另外,Parallel GC 引入了开发者友好的配置项,咱们能够直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例以下面参数:-XX:MaxGCPauseMillis=value -XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1) 
  • G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 之后的默认 GC 选项。G1 能够直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能作到 CMS 在最好状况下的延时停顿,可是最差状况要好不少。G1 GC 仍然存在着年代的概念,可是其内存结构并非简单的条带式划分,而是相似棋盘的一个个 region。Region 之间是复制算法,但总体上实际可看做是标记 - 整理(Mark-Compact)算法,能够有效地避免内存碎片,尤为是当 Java 堆很是大的时候,G1 的优点更加明显。G1 吞吐量和停顿表现都很是不错,而且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),因此 G1 GC 值得你深刻掌握。

28. 谈谈你的GC调优思路?

  谈到调优,这必定是针对特定场景、特定目的的事情, 对于 GC 调优来讲,首先就须要清楚调优的目标是什么?从性能的角度看,一般关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput),大多数状况下调优会侧重于其中一个或者两个方面的目标,不多有状况能够兼顾三个不一样的角度。固然,除了上面一般的三个方面,也可能须要考虑其余 GC 相关的场景,例如,OOM 也可能与不合理的 GC 相关参数有关;或者,应用启动速度方面的需求,GC 也会是个考虑的方面。基本的调优思路能够总结为:

  理解应用需求和问题,肯定调优目标。假设,咱们开发了一个应用服务,但发现偶尔会出现性能抖动,出现较长的服务停顿。评估用户可接受的响应时间和业务量,将目标简化为,但愿 GC 暂停尽可能控制在 200ms 之内,而且保证必定标准的吞吐量。

  • 掌握 JVM 和 GC 的状态,定位具体的问题,肯定真的有 GC 调优的必要。具体有不少方法,好比,经过 jstat 等工具查看 GC 等相关状态,能够开启 GC 日志,或者是利用操做系统提供的诊断工具等。例如,经过追踪 GC 日志,就能够查找是否是 GC 在特定时间发生了长时间的暂停,进而致使了应用响应不及时。
  • 这里须要思考,选择的 GC 类型是否符合咱们的应用特征,若是是,具体问题表如今哪里,是 Minor GC 过长,仍是 Mixed GC 等出现异常停顿状况;若是不是,考虑切换到什么类型,如 CMS 和 G1 都是更侧重于低延迟的 GC 选项。
  • 经过分析肯定具体调整的参数或者软硬件配置。
  • 验证是否达到调优目标,若是达到目标,便可以考虑结束调优;不然,重复完成分析、调整、验证这个过程。

29. Java内存模型中的happen-before是什么?

  Happen-before 关系,是 Java 内存模型中保证多线程操做可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精肯定义。它的具体表现形式,包括但远不止是咱们直觉中的 synchronized、volatile、lock 操做顺序等方面,例如:

  • 线程内执行的每一个操做,都保证 happen-before 后面的操做,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
  • 对于 volatile 变量,对它的写操做,保证 happen-before 在随后对该变量的读取操做。
  • 对于一个锁的解锁操做,保证 happen-before 加锁操做。
  • 对象构建完成,保证 happen-before 于 finalizer 的开始动做。
  • 甚至是相似线程内部操做的完成,保证 happen-before 其余 Thread.join() 的线程等。

  这些 happen-before 关系是存在着传递性的,若是知足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。

  前面我一直用 happen-before,而不是简单说先后,是由于它不只仅是对执行时间的保证,也包括对内存读、写操做顺序的保证。仅仅是时钟顺序上的前后,并不能保证线程交互的可见性。

30. Java程序运行在Docker等容器环境有哪些新问题?

  对于 Java 来讲,Docker 毕竟是一个较新的环境,例如,其内存、CPU 等资源限制是经过CGroup(Control Group)实现的,早期的 JDK 版本(8u131 以前)并不能识别这些限制,进而会致使一些基础问题:

  • 若是未配置合适的 JVM 堆和元数据区、直接内存等参数,Java 就有可能试图使用超过容器限制的内存,最终被容器 OOM kill,或者自身发生 OOM。
  • 错误判断了可获取的 CPU 资源,例如,Docker 限制了 CPU 的核数,JVM 就可能设置不合适的 GC 并行线程数等。

  从应用打包、发布等角度出发,JDK 自身就比较大,生成的镜像就更为臃肿,当咱们的镜像很是多的时候,镜像的存储等开销就比较明显了。若是考虑到微服务、Serverless 等新的架构和场景,Java 自身的大小、内存占用、启动速度,都存在必定局限性,由于 Java 早期的优化大可能是针对长时间运行的大型服务器端应用。

31. 你了解Java应用开发中的注入攻击吗?

  注入式(Inject)攻击是一类很是常见的攻击方式,其基本特征是程序容许攻击者将不可信的动态内容注入到程序中,并将其执行,这就可能彻底改变最初预计的执行过程,产生恶意效果。

下面是几种主要的注入式攻击途径,原则上提供动态执行能力的语言特性,都须要提防发生注入攻击的可能。

  首先,就是最多见的 SQL 注入攻击。一个典型的场景就是 Web 系统的用户登陆功能,根据用户输入的用户名和密码,咱们须要去后端数据库核实信息。假设应用逻辑是,后端程序利用界面输入动态生成相似下面的 SQL,而后让 JDBC 执行。

Select * from use_info where username = “input_usr_name” and password = “input_pwd”

  可是,若是我输入的 input_pwd 是相似下面的文本,“ or “”=”那么,拼接出的 SQL 字符串就变成了下面的条件,OR 的存在致使输入什么名字都是复合条件的。

Select * from use_info where username = “input_usr_name” and password = “” or “” = “”

  这里只是举个简单的例子,它是利用了指望输入和可能输入之间的误差。上面例子中,指望用户输入一个数值,但实际输入的则是 SQL 语句片断。相似场景能够利用注入的不一样 SQL 语句,进行各类不一样目的的攻击,甚至还能够加上“;delete xxx”之类语句,若是数据库权限控制不合理,攻击效果就多是灾难性的。

  第二,操做系统命令注入。Java 语言提供了相似 Runtime.exec(…) 的 API,能够用来执行特定命令,假设咱们构建了一个应用,以输入文本做为参数,执行下面的命令:ls –la input_file_name,可是若是用户输入是 “input_file_name;rm –rf /*”,这就有可能出现问题了。固然,这只是个举例,Java 标准类库自己进行了很是多的改进,因此相似这种编程错误,未必能够真的完成攻击,但其反映的一类场景是真实存在的。

  第三,XML 注入攻击。Java 核心类库提供了全面的 XML 处理、转换等各类 API,而 XML 自身是能够包含动态内容的,例如 XPATH,若是使用不当,可能致使访问恶意内容。

  还有相似 LDAP 等容许动态内容的协议,都是可能利用特定命令,构造注入式攻击的,包括XSS(Cross-site Scripting)攻击,虽然并不和 Java 直接相关,但也可能在 JSP 等动态页面中发生。

32. 如何写出安全的Java代码?

  这个问题可能有点宽泛,咱们能够用特定类型的安全风险为例,如拒绝服务(DoS)攻击,分析Java 开发者须要重点考虑的点。

DoS 是一种常见的网络攻击,有人也称其为“洪水攻击”。最多见的表现是,利用大量机器发送请求,将目标网站的带宽或者其余资源耗尽,致使其没法响应正经常使用户的请求。

我认为,从 Java 语言的角度,更加须要重视的是程序级别的攻击,也就是利用 Java、JVM 或应用程序的瑕疵,进行低成本的 DoS 攻击,这也是想要写出安全的 Java 代码所必须考虑的。

例如:

  • 若是使用的是早期的 JDK 和 Applet 等技术,攻击者构建合法但恶劣的程序就相对容易,例如,将其线程优先级设置为最高,作一些看起来无害但空耗资源的事情。幸运的是相似技术已经逐步退出历史舞台,在 JDK 9 之后,相关模块就已经被移除。
  • 上一讲中提到的哈希碰撞攻击,就是个典型的例子,对方能够轻易消耗系统有限的 CPU 和线程资源。从这个角度思考,相似加密、解密、图形处理等计算密集型任务,都要防范被恶意滥用,以避免攻击者经过直接调用或者间接触发方式,消耗系统资源。
  • 利用 Java 构建相似上传文件或者其余接受输入的服务,须要对消耗系统内存或存储的上限有所控制,由于咱们不能将系统安全依赖于用户的合理使用。其中特别注意的是涉及解压缩功能时,就须要防范Zip bomb等特定攻击。
  • 另外,Java 程序中须要明确释放的资源有不少种,好比文件描述符、数据库链接,甚至是再入锁,任何状况下都应该保证资源释放成功,不然即便平时可以正常运行,也可能被攻击者利用而耗尽某类资源,这也算是可能的 DoS 攻击来源。

  因此能够看出,实现安全的 Java 代码,须要从功能设计到实现细节,都充分考虑可能的安全影响。

33. 后台服务出现明显“变慢”,谈谈你的诊断思路?

  首先,须要对这个问题进行更加清晰的定义:

  • 服务是忽然变慢仍是长时间运行后观察到变慢?相似问题是否重复出现?
  • “慢”的定义是什么,我可以理解是系统对其余方面的请求的反应延时变长吗?

  第二,理清问题的症状,这更便于定位具体的缘由,有如下一些思路:

  • 问题可能来自于 Java 服务自身,也可能仅仅是受系统里其余服务的影响。初始判断能够先确认是否出现了意外的程序错误,例如检查应用自己的错误日志。对于分布式系统,不少公司都会实现更加系统的日志、性能等监控系统。一些 Java 诊断工具也能够用于这个诊断,例如经过 JFR(Java Flight Recorder),监控应用是否大量出现了某种类型的异常。若是有,那么异常可能就是个突破点。若是没有,能够先检查系统级别的资源等状况,监控 CPU、内存等资源是否被其余进程大量占用,而且这种占用是否不符合系统正常运行情况。
  • 监控 Java 服务自身,例如 GC 日志里面是否观察到 Full GC 等恶劣状况出现,或者是否Minor GC 在变长等;利用 jstat 等工具,获取内存使用的统计信息也是个经常使用手段;利用jstack 等工具检查是否出现死锁等。
  • 若是还不能肯定具体问题,对应用进行 Profiling 也是个办法,但由于它会对系统产生侵入性,若是不是很是必要,大多数状况下并不建议在生产系统进行。
  • 定位了程序错误或者 JVM 配置的问题后,就能够采起相应的补救措施,而后验证是否解决,不然还须要重复上面部分过程。

34. 有人说“Lambda能让Java程序慢30倍”,你怎么看?

  我认为,“Lambda 能让 Java 程序慢 30 倍”这个争论实际反映了几个方面:

  第一,基准测试是一个很是有效的通用手段,让咱们以直观、量化的方式,判断程序在特定条件下的性能表现。

  第二,基准测试必须明肯定义自身的范围和目标,不然颇有可能产生误导的结果。前面代码片断自己的逻辑就有瑕疵,更多的开销是源于自动装箱、拆箱(auto-boxing/unboxing),而不是源自 Lambda 和 Stream,因此得出的初始结论是没有说服力的。

  第三,虽然 Lambda/Stream 为 Java 提供了强大的函数式编程能力,可是也须要正视其局限性:

  • 通常来讲,咱们能够认为 Lambda/Stream 提供了与传统方式接近对等的性能,可是若是对于性能很是敏感,就不能彻底忽视它在特定场景的性能差别了,例如:初始化的开销。Lambda 并不算是语法糖,而是一种新的工做机制,在首次调用时,JVM 须要为其构建CallSite实例。这意味着,若是 Java 应用启动过程引入了不少 Lambda 语句,会致使启动过程变慢。其实现特色决定了 JVM 对它的优化可能与传统方式存在差别。
  • 增长了程序诊断等方面的复杂性,程序栈要复杂不少,Fluent 风格自己也不算是对于调试很是友好的结构,而且在可检查异常的处理方面也存在着局限性等。

35. JVM优化Java代码时都作了什么?

  JVM 在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,好比说锁机制(如偏斜锁)、内存分配机制(如TLAB)等。除此以外,还有一些专门用于优化解释执行效率的,好比说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。

  JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?好比我有一条 instanceof 指令,在编译以前的执行过程当中,测试对象的类一直是同一个,那么即时编译器能够假设编译以后的执行过程当中还会是这一个类,而且根据这个类直接返回instanceof 的结果。若是出现了其余类,那么就抛弃这段编译后的机器码,而且切换回解释执行。

  固然,JVM 的优化方式仅仅做用在运行应用代码的时候。若是应用代码自己阻塞了,好比说并发时等待另外一线程的结果,这就不在 JVM 的优化范畴啦。

36. 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

  所谓隔离级别(Isolation Level),就是在数据库事务中,为保证并发数据读写的正确性而提出的定义,它并非MySQL专有的概念,而是源于ANSI/ISO制定的SQL-92标准。

每种关系型数据库都提供了各自特点的隔离级别实现,虽然在一般的 定义 中是以锁为实现单元,但实际的实现千差万别。以最多见的MySQL InnoDB引擎为例,它是基于MVCC(Multi-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高,MySQL事务隔离级别分为四个不一样层次:

  • 读未提交(Read uncommitted),就是一个事务可以看到其余事务还没有提交的修改,这是最低的隔离水平,容许 脏读 出现。
  • 读已提交(Read committed),事务可以看到的数据都是其余事务已经提交的修改,也就是保证不会看到任何中间性状态,固然脏读也不会出现。读已提交仍然是比较低级别的
  • 隔离,并不保证再次读取时可以获取一样的数据,也就是容许其余事务并发修改数据,容许不可重复读和幻象读( Phantom Read )出现。
  • 可重复读(Repeatable reads),保证同一个事务中屡次读取的数据是一致的,这是MySQL InnoDB引擎的默认隔离级别,可是和一些其余数据库实现不一样的是,能够简单认为 MySQL 在可重复读级别不会出现幻象读。

  串行化(Serializable),并发事务之间是串行化的,一般意味着读取须要获取共享读锁,更新须要获取排他写锁,若是SQL使用WHERE语句,还会获取区间锁( MySQL 以 GAP 锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。

  至于悲观锁和乐观锁,也并非 MySQL 或者数据库中独有的概念,而是并发编程的基本概念。主要区别在于,操做共享数据时, “ 悲观锁 ” 即认为数据出现冲突的可能性更大,而 “ 乐观锁 ” 则是认为大部分状况不会出现冲突,进而决定是否采起排他性措施。反映到 MySQL 数据库应用开发中,悲观锁通常就是利用相似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其余事务意外修改数据。乐观锁则与 Java 并发包中的 AtomicFieldUpdater 相似,也是利用 CAS 机制,并不会对数据加锁,而是经过对比数据的时间戳或者版本号,来实现乐观锁须要的版本判断。

相关文章
相关标签/搜索