本文已经收录自笔者开源的 JavaGuide: https://github.com/Snailclimb (【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识)若是以为不错的还,不妨去点个Star,鼓励一下!
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法能够修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各类程序设计语言(不仅是 Java)中方法参数传递方式。html
Java 程序设计语言老是采用按值调用。也就是说,方法获得的是全部参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。java
下面经过 3 个例子来给你们说明c++
example 1
public static void main(String[] args) { int num1 = 10; int num2 = 20; swap(num1, num2); System.out.println("num1 = " + num1); System.out.println("num2 = " + num2); } public static void swap(int a, int b) { int temp = a; a = b; b = temp; System.out.println("a = " + a); System.out.println("b = " + b); }
结果:git
a = 20 b = 10 num1 = 10 num2 = 20
解析:程序员
在 swap 方法中,a、b 的值进行交换,并不会影响到 num一、num2。由于,a、b 中的值,只是从 num一、num2 的复制过来的。也就是说,a、b 至关于 num一、num2 的副本,副本的内容不管怎么修改,都不会影响到原件自己。github
经过上面例子,咱们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用做为参数就不同,请看 example2.面试
example 2
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); } public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; }
结果:算法
1 0
解析:spring
array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 所以,外部对引用对象的改变会反映到所对应的对象上。数据库
经过 example2 咱们已经看到,实现一个改变对象参数状态的方法并非一件难事。理由很简单,方法获得的是对象引用的拷贝,对象引用及其余的拷贝同时引用同一个对象。
不少程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的做者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。因为这种误解具备必定的广泛性,因此下面给出一个反例来详细地阐述一下这个问题。
example 3
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Student s1 = new Student("小张"); Student s2 = new Student("小李"); Test.swap(s1, s2); System.out.println("s1:" + s1.getName()); System.out.println("s2:" + s2.getName()); } public static void swap(Student x, Student y) { Student temp = x; x = y; y = temp; System.out.println("x:" + x.getName()); System.out.println("y:" + y.getName()); } }
结果:
x:小李 y:小张 s1:小张 s2:小李
解析:
交换以前:
交换以后:
经过上面两张图能够很清晰的看出: 方法并无改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝
总结
Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按
值传递的。
下面再总结一下 Java 中方法参数的使用状况:
参考:
《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节
== : 它的做用是判断两个对象的地址是否是相等。即,判断两个对象是否是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
equals() : 它的做用也是判断两个对象是否相等。但它通常有两种使用状况:
举个例子:
public class test1 { public static void main(String[] args) { String a = new String("ab"); // a 为一个引用 String b = new String("ab"); // b为另外一个引用,对象的内容同样 String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 if (aa == bb) // true System.out.println("aa==bb"); if (a == b) // false,非同一对象 System.out.println("a==b"); if (a.equals(b)) // true System.out.println("aEQb"); if (42 == 42.0) { // true System.out.println("true"); } } }
说明:
面试官可能会问你:“你重写过 hashcode 和 equals 么,为何重写 equals 时必须重写 hashCode 方法?”
hashCode() 的做用是获取哈希码,也称为散列码;它其实是返回一个 int 整数。这个哈希码的做用是肯定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外须要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法一般用来将对象的 内存地址 转换为整数以后返回。
/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * <p> * As much as is reasonably practical, the hashCode method defined by * class {@code Object} does return distinct integers for distinct * objects. (This is typically implemented by converting the internal * address of the object into an integer, but this implementation * technique is not required by the * Java™ programming language.) * * @return a hash code value for this object. * @see java.lang.Object#equals(java.lang.Object) * @see java.lang.System#identityHashCode */ public native int hashCode();
散列表存储的是键值对(key-value),它的特色是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(能够快速找到所须要的对象)
咱们以“HashSet 如何检查重复”为例子来讲明为何要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其余已经加入的对象的 hashcode 值做比较,若是没有相符的 hashcode,HashSet 会假设对象没有重复出现。可是若是发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。若是二者相同,HashSet 就不会让其加入操做成功。若是不一样的话,就会从新散列到其余位置。(摘自个人 Java 启蒙书《Head fist java》第二版)。这样咱们就大大减小了 equals 的次数,相应就大大提升了执行速度。
在这里解释一位小伙伴的问题。如下内容摘自《Head Fisrt Java》。
由于 hashCode() 所使用的杂凑算法也许恰好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不一样的对象获得相同的 hashCode)。
咱们刚刚也提到了 HashSet,若是 HashSet 在对比的时候,一样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
可变性
简单的来讲:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,因此 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
可是没有用 final 关键字修饰,因此这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,你们能够自行查阅源码。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; }
线程安全性
String 中的对象是不可变的,也就能够理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操做,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,因此是线程安全的。StringBuilder 并无对方法进行加同步锁,因此是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,而后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象自己进行操做,而不是生成新的对象并改变对象引用。相同状况下使用 StringBuilder 相比使用 StringBuffer 仅能得到 10%~15% 左右的性能提高,但却要冒多线程不安全的风险。
对于三者使用的总结:
简单来讲就是 String 类利用了 final 修饰的 char 类型数组存储字符,源码以下图因此:
/** The value is used for character storage. */ private final char value[];
我以为若是别人问这个问题的话,回答不可变就能够了。
下面只是给你们看两个有表明性的例子:
1) String 不可变但不表明引用不能够变
String str = "Hello"; str = str + " World"; System.out.println("str=" + str);
结果:
str=Hello World
解析:
实际上,原来 String 的内容是不变的,只是 str 由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。
2) 经过反射是能够修改所谓的“不可变”对象
// 建立字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); // Hello World // 获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); // 改变value属性的访问权限 valueFieldOfString.setAccessible(true); // 获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); // 改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); // Hello_World
结果:
s = Hello World s = Hello_World
解析:
用反射能够访问私有成员, 而后反射出 String 对象中的 value 属性, 进而改变经过得到的 value 引用改变数组的结构。可是通常咱们不会这么作,这里只是简单提一下有这个东西。
JAVA 反射机制是在运行状态中,对于任意一个类,都可以知道这个类的全部属性和方法;对于任意一个对象,都可以调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
反射是框架设计的灵魂。
在咱们平时的项目开发过程当中,基本上不多会直接使用到反射机制,但这不能说明反射机制没有用,实际上有不少设计、开发都与反射机制有关,例如模块化的开发,经过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有咱们平常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:① 咱们在使用 JDBC 链接数据库时使用 Class.forName()
经过反射加载数据库的驱动程序;②Spring 框架也用到不少反射机制,最经典的就是 xml 的配置模式。Spring 经过 XML 配置模式装载 Bean 的过程:1) 将程序内全部 XML 或 Properties 配置文件加载入内存中;
2)Java 类里面解析 xml 或 properties 里面的内容,获得对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串得到某个类的 Class 实例; 4)动态配置实例的属性
推荐阅读:
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不一样系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 能够理解的代码就叫作字节码
(即扩展名为.class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言经过字节码的方式,在必定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特色。因此 Java 程序运行时比较高效,并且,因为字节码并不针对一种特定的机器,所以,Java 程序无须从新编译即可在多种不一样操做系统的计算机上运行。
Java 程序从源代码到运行通常有下面 3 步:
咱们须要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,而后经过解释器逐行解释执行,这种方式的执行速度会相对比较慢。并且,有些方法和代码块是常常须要被调用的(也就是所谓的热点代码),因此后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次能够直接使用。而咱们知道,机器码的运行效率确定是高于 Java 解释器的。这也解释了咱们为何常常会说 Java 是编译与解释共存的语言。
HotSpot 采用了惰性评估(Lazy Evaluation)的作法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所须要编译的部分。JVM 会根据代码每次被执行的状况收集信息并相应地作出一些优化,所以执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协做使用。可是 ,AOT 编译器的编译质量是确定比不上 JIT 编译器的。
总结:
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不一样系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不一样系统的 JVM 实现是 Java 语言“一次编译,随处能够运行”的关键所在。
JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它可以建立和编译程序。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的全部内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其余的一些基础构件。可是,它不能用于建立新程序。
若是你只是为了运行一下 Java 程序的话,那么你只须要安装 JRE 就能够了。若是你须要进行一些 Java 编程方面的工做,那么你就须要安装 JDK 了。可是,这不是绝对的。有时,即便您不打算在计算机上进行任何 Java 开发,仍然须要安装 JDK。例如,若是要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为何须要 JDK 呢?由于应用程序服务器会将 JSP 转换为 Java servlet,而且须要使用 JDK 来编译 servlet。
先看下 java 中的编译器和解释器:
Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只须要面向虚拟机,生成虚拟机可以理解的代码,而后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫作字节码
(即扩展名为.class
的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不一样的,可是实现的虚拟机是相同的。Java 源程序通过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,而后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特色。
Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。
采用字节码的好处:
Java 语言经过字节码的方式,在必定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特色。因此 Java 程序运行时比较高效,并且,因为字节码并不专对一种特定的机器,所以,Java 程序无须从新编译即可在多种不一样的计算机上运行。
注意:Java8 后接口能够有默认实现( default )。
发生在同一个类中,方法名必须相同,参数类型不一样、个数不一样、顺序不一样,方法返回值和访问修饰符能够不一样。
下面是《Java 核心技术》对重载这个概念的介绍:
重写是子类对父类的容许访问的方法的实现过程进行从新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,若是父类方法访问修饰符为 private 则子类就不能重写该方法。也就是说方法提供的行为改变,而方法的外貌并无改变。
封装把一个对象的属性私有化,同时提供一些能够被外界访问的属性的方法,若是属性不想被外界访问,咱们大可没必要提供方法给外界访问。可是若是一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承是使用已存在的类的定义做为基础创建新类的技术,新类的定义能够增长新的数据或新的功能,也能够用父类的功能,但不能选择性地继承父类。经过使用继承咱们可以很是方便地复用之前的代码。
关于继承以下 3 点请记住:
所谓多态就是指程序中定义的引用变量所指向的具体类型和经过该引用变量发出的方法调用在编程时并不肯定,而是在程序运行期间才肯定,即一个引用变量到底会指向哪一个类的实例对象,该引用变量发出的方法调用究竟是哪一个类中实现的方法,必须在由程序运行期间才能决定。
在 Java 中有两种形式能够实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
进程是程序的一次执行过程,是系统运行程序的基本单位,所以进程是动态的。系统运行一个程序便是一个进程从建立,运行到消亡的过程。
在 Java 中,当咱们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
以下图所示,在 windows 中经过查看任务管理器的方式,咱们就能够清楚看到 window 当前运行的进程(.exe 文件的运行)。
线程与进程类似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程当中能够产生多个线程。与进程不一样的是同类的多个线程共享进程的堆和方法区资源,但每一个线程有本身的程序计数器、虚拟机栈和本地方法栈,因此系统在产生一个线程,或是在各个线程之间做切换工做时,负担要比进程小得多,也正由于如此,线程也被称为轻量级进程。
Java 程序天生就是多线程程序,咱们能够经过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码以下。
public class MultiThread { public static void main(String[] args) { // 获取 Java 线程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不须要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍历线程信息,仅打印线程 ID 和线程名称信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }
上述程序输出以下(输出内容可能不一样,不用太纠结下面每一个线程的做用,只用知道 main 线程执行 main 方法便可):
[5] Attach Listener //添加事件 [4] Signal Dispatcher // 分发处理给 JVM 信号的线程 [3] Finalizer //调用对象 finalize 方法的线程 [2] Reference Handler //清除 reference 线程 [1] main //main 线程,程序入口
从上面的输出内容能够看出:一个 Java 程序的运行是 main 线程和多个其余线程同时运行。
从 JVM 角度说进程和线程之间的关系
下图是 Java 内存区域,经过下图咱们从 JVM 的角度来讲一下线程和进程之间的关系。若是你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话能够阅读一下这篇文章:《多是把 Java 内存区域讲的最清楚的一篇文章》
<div align="center">
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM运行时数据区域.png" width="600px"/>
</div>
从上图能够看出:一个进程中能够有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 以后的元空间)资源,可是每一个线程有本身的程序计数器、虚拟机栈 和 本地方法栈。
总结: 线程 是 进程 划分红的更小的运行单位。线程和进程最大的不一样在于基本上各进程是独立的,而各线程则不必定,由于同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
下面是该知识点的扩展内容!
下面来思考这样一个问题:为何程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为何堆和方法区是线程共享的呢?
程序计数器主要有下面两个做用:
须要注意的是,若是执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
因此,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
因此,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区是全部线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新建立的对象 (全部对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
多线程编程中通常线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能获得有效执行,CPU 采起的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会从新处于就绪状态让给其余线程使用,这个过程就属于一次上下文切换。
归纳来讲就是:当前任务在执行完 CPU 时间片切换到另外一个任务以前会先保存本身的状态,以便下次再切换回这个任务时,能够再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换一般是计算密集型的。也就是说,它须要至关可观的处理器时间,在每秒几十上百次的切换中,每次切换都须要纳秒量级的时间。因此,上下文切换对系统来讲意味着消耗大量的 CPU 时间,事实上,多是操做系统中时间消耗最大的操做。
Linux 相比与其余操做系统(包括其余类 Unix 系统)有不少的优势,其中有一项就是,其上下文切换和模式切换的时间消耗很是少。
多个线程同时被阻塞,它们中的一个或者所有都在等待某个资源被释放。因为线程被无限期地阻塞,所以程序不可能正常终止。
以下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,因此这两个线程就会互相等待而进入死锁状态。
下面经过一个例子来讲明线程死锁,代码模拟了上图的死锁的状况 (代码来源于《并发编程之美》):
public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } }
Output
Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1
线程 A 经过 synchronized (resource1) 得到 resource1 的监视器锁,而后经过Thread.sleep(1000);
让线程 A 休眠 1s 为的是让线程 B 获得执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,而后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
学过操做系统的朋友都知道产生死锁必须具有如下四个条件:
咱们只要破坏产生死锁的四个条件中的其中一个就能够了。
破坏互斥条件
这个条件咱们没有办法破坏,由于咱们用锁原本就是想让他们互斥的(临界资源须要互斥访问)。
破坏请求与保持条件
一次性申请全部的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其余资源时,若是申请不到,能够主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
咱们对线程 2 的代码修改为下面这样就不会产生死锁了。
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 2").start();
Output
Thread[线程 1,5,main]get resource1 Thread[线程 1,5,main]waiting get resource2 Thread[线程 1,5,main]get resource2 Thread[线程 2,5,main]get resource1 Thread[线程 2,5,main]waiting get resource2 Thread[线程 2,5,main]get resource2 Process finished with exit code 0
咱们分析一下上面的代码为何避免了死锁的发生?
线程 1 首先得到到 resource1 的监视器锁,这时候线程 2 就获取不到了。而后线程 1 再去获取 resource2 的监视器锁,能够获取到。而后线程 1 释放了对 resource一、resource2 的监视器锁的占用,线程 2 获取到就能够执行了。这样就破坏了破坏循环等待条件,所以避免了死锁。
这是另外一个很是经典的 java 多线程面试问题,并且在面试中会常常被问到。很简单,可是不少人都会答不上来!
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就能够开始运行了。 start() 会执行线程的相应准备工做,而后自动执行 run() 方法的内容,这是真正的多线程工做。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,因此这并非多线程工做。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,仍是在主线程里执行。
做者的其余开源项目推荐: