JRE:Java Runtime Environment(java运行时环境)。即java程序的运行时环境,包含了java虚拟机,java基础类库。 JDK:Java Development Kit(java开发工具包)。即java语言编写的程序所需的开发工具包。 JDK包含了JRE,同时还包括java源码的编译器javac、监控工具jconsole、分析工具jvisualvm等。
一、理解”==“的含义
在java中,主要有两个作用。
1、基础数据类型:比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。
2、引用数据类型:比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。
OK。到这就注意了,你会发现,我在举引用的例子的时候,使用的是User对象,而不是String。别着急接着往下看。 从这个源码中你会发现,比较的是当前对象的引用和obj的引用是否相同,也就是说比较的默认就是地址。还记的在上面我们使用的是User而不是String嘛?在这里比较的是引用的地址,equals也是比较的是引用的地址,所以他们的效果在这里是一样的。 现在你会发现好像equals的作用和没什么区别呀,那String类型那些乱七八糟的东西是什么呢?继续往下看马上揭晓。
三、重写equals
1、String中equals方法
看到这个标题相信你已经能找到答案里,Object对象里面的==和equals没有什么区别,这样一看equals方法存在的意义真的不大,不过后来String在Object的基础之上重写了equals,于是功能被大大的改变了。如何重写的呢?我们去String的源码中找寻答案:
二、理解equals的含义
先看看他的源码,equals方法是在Object中就有。注意了这里的源码是Object里面的equals。
public boolean equals(Object anObject) { if (this == anObject) { //引用比较 return true; } if (anObject instanceof String) { //判断是否为String类型 String anotherString = (String)anObject; int n = value.length; //anotherString.value.length 相当于 anotherString.length() if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) //比较每一个字符是否相等 return false; i++; } return true; } } return false; }从上面的源码,我们能够获取到的信息是:String中的equals方法其实比较的是字符串的内容是否一样。也就是说如果像String、Date这些重写equals的类,你可要小心了。使用的时候会和Object的不一样。 2、测试String 看看下面的代码: 在上面的代码中,定义了三个字符串,分别使用==和equals去比较。为什么会出现这样一个结果呢?还需要从内存的角度来解释一下。
3、内存解释
在java中我们一般把对象存放在堆区,把对象的引用放在栈区。因此在上面三个字符串的内存状态应该是下面这样的。 现在明白了吧。
(1)String str1 = "Hello"会在堆区存放一个字符串对象
(2)String str2 = new String(“Hello”)会在堆区再次存放一个字符串对象
(3)String str3 = str2这时候Str3和Str2是两个不同的引用,但是指向同一个对象。
根据这张图再来看上面的比较:
(1)str1 == str2嘛?意思是地址指向的是同一块地方吗?很明显不一样。
(2)str1 == str3嘛?意思是地址指向的是同一块地方吗?很明显不一样。
(3)str2 == str3嘛?意思是地址指向的是同一块地方吗?很明显内容一样,所以为true。
(4)str1.equals(str2)嘛?意思是地址指向的内容一样嘛?一样。
(4)str1.equals(str3)嘛?意思是地址指向的内容一样嘛?一样。
(4)str2.equals(str3)嘛?意思是地址指向的内容一样嘛?一样。
OK。现在不知道你能理解嘛?
4、总结:
(1)、基础类型比较
使用==比较值是否相等。
(2)、引用类型比较
①重写了equals方法,比如String。
第一种情况:使用==比较的是String的引用是否指向了同一块内存
第二种情况:使用equals比较的是String的引用的对象内用是否相等。
②没有重写equals方法,比如User等自定义类
==和equals比较的都是引用是否指向了同一块内存。
答案:不对 我们先看到Objec类中hashCode()方法源码
该方法是个native方法,因为native方法是由非Java语言实现的,所以这个方法的定义中也没有具体的实现。根据jdk文档,该方法的实现一般是通过将该对象的内部地址转换成一个整数来实现的,这个返回值就作为该对象的哈希码值返回。
再看equals源码,尤其要注意return的说明
hashCode值是从hash表中得来的,hash是一个函数,该函数的实现是一种算法,通过hash算法算出hash值,hash表就是hash值组成的,一共有8个位置。因此,hashCode相同的两个对象不一定equals()也为true。
final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。
特征:凡是引用final关键字的地方皆不可修改!
修饰类:表示该类不能被继承;修饰方法:表示方法不能被重写;修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。Math.round(-1.5)的返回值是-1。四舍五入的原理是在参数上加0.5然后做向下取整。 我们可以通过大量实验看下结果
答:String、StringBuffer、StringBuilder
区别:
String : final修饰,String类的方法都是返回new String。即对String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。StringBuffer : 对字符串的操作的方法都加了synchronized,保证线程安全。StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以new StringBuilder对象,调用StringBuilder对象的append、replace、delete等方法修改字符串。StringBuffer的安全性能高,适合多线程使用;Stringbuider性能更低适合单线程操作。答案是:不必须 这道题考察的是抽象类的知识:
抽象类必须有关键字abstract来修饰。 抽象类可以不含有抽象方法 如果一个类包含抽象方法,则该类必须是抽象类这个题要从流的角度去划分:
按照流的流向分,可以分为输入流和输出流; 按照操作单元划分,可以划分为字节流和字符流; 按照流的角色划分为节点流和处理流。所有流的基类
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。什么是IO
Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据传输流,字符串流,对象流等等等。比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中什么是BIO
BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解什么是NIO
NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。什么是AIO
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。.
AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现 在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作 BIO和NIO、AIO的区别
BIO是阻塞的,NIO是非阻塞的.BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成Java容器:
数组,String,java.util下的集合容器
数组长度限制为 Integer.Integer.MAX_VALUE;
String的长度限制: 底层是char 数组 长度 Integer.MAX_VALUE 线程安全的
List:存放有序,列表存储,元素可重复Set:无序,元素不可重复Map:无序,元素可重复1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
List,Set,Queue接口都继承Collection。 直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。
2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
List(对付顺序的好帮手):List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象。 Map(用Key来搜索的专家):使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
HashMap可以接受null键值和值,而Hashtable则不能。
Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。
这两个类有许多不同的地方,下面列出了一部分:
Hashtable 是 JDK 1 遗留下来的类,而 HashMap 是后来增加的。Hashtable 是同步的,比较慢,但 HashMap 没有同步策略,所以会更快。Hashtable 不允许有个空的 key,但是 HashMap 允许出现一个 null key。我们先看一下HashSet和TreeSet在整个集合框架中的位置。他们都实现了Set接口。他们之间的区别是HashSet不能保证元素的顺序,TreeSet中的元素可以按照某个顺序排列。他们的元素都不能重复。 先来看一下HashSet:
public static void main(String[] args) { Set<String> set = new HashSet<String>(); set.add("张三"); set.add("李四"); set.add("王五"); System.out.println(set); System.out.println(set.size()); System.out.println(set.contains("张三")); }打印输出的顺序是是: [李四, 张三, 王五]
可以看出和存进去的顺序不一致。
我们先看一下 Set set = new HashSet();
这行代码创建了一个HashSet,构造函数如下:
public HashSet() { map = new HashMap<>(); }
可以看到实际上是创建了一个HashMap的对象。没错,HashSet底层就是一个HashMap. 再来看一下这行代码:set.add(“张三”);
public boolean add(E e) { return map.put(e, PRESENT)==null; }
非常的简单,就是调用了一下HashMap的put方法对元素进行插入。
这里的PERSENT是什么呢?继续顺藤摸瓜:
private static final Object PRESENT = new Object(); 原来就是一个普通的Object对象前面用static final修饰说明是不可变的。 继续添加:set.add(“李四”); 可以看出来HashMap的key分别为”张三”,”李四”,“王五”, 因为HashSet用不到value,他们的value都是一样的指向同一个地方。 继续往下看:System.out.println(set.size());
public int size() { return map.size(); }
也是调用的HashMap的size方法。
System.out.println(set.contains(“张三”));
public boolean contains(Object o) { return map.containsKey(o); }
同样调用的HashMap的contains方法。
数组转 List ,使用 JDK 中 java.util.Arrays 工具类的 asList 方法
public static void testArray2List() { String[] strs = new String[] {"aaa", "bbb", "ccc"}; List<String> list = Arrays.asList(strs); for (String s : list) { System.out.println(s); } }List 转数组,使用 List 的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
public static void testList2Array() { List<String> list = Arrays.asList("aaa", "bbb", "ccc"); String[] array = list.toArray(new String[list.size()]); for (String s : array) { System.out.println(s); } }首先我们给出标准答案:
Vector是线程安全的,ArrayList不是线程安全的。ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍。 看上图Vector和ArrayList一样,都继承自List,来看一下Vector的源码 实现了List接口,底层和ArrayList一样,都是数组来实现的。分别看一下这两个类的add方法,首先来看ArrayList的add源码 再看Vector的add源码 方法实现都一样,就是加了一个synchronized的关键字,再来看看其它方法,先看ArrayList的remove方法 再看Vector的remove方法 方法实现上也一样,就是多了一个synchronized关键字,再看看ArrayList的get方法 Vector的get方法 再看看Vector的其它方法 无一例外,只要是关键性的操作,方法前面都加了synchronized关键字,来保证线程的安全性。当执行synchronized修饰的方法前,系统会对该方法加一把锁,方法执行完成后释放锁,加锁和释放锁的这个过程,在系统中是有开销的,因此,在单线程的环境中,Vector效率要差很多。(多线程环境不允许用ArrayList,需要做处理)。定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。 ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
队列是一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。 队列的两种实现方式: 1、offer()和add()的区别
add()和offer()都是向队列中添加一个元素。但是如果想在一个满的队列中加入一个新元素,调用 add() 方法就会抛出一个unchecked 异常,而调用 offer() 方法会返回 false。可以据此在程序中进行有效的判断!2、peek()和element()的区别
peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,调用element()方法会抛出NoSuchElementException异常。3、poll()和remove()的区别
poll()和remove()都将移除并且返回对头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。Iterator 的使用示例
public class TestIterator { static List<String> list = new ArrayList<String>(); static { list.add("111"); list.add("222"); list.add("333"); } public static void main(String[] args) { testIteratorNext(); System.out.println(); testForEachRemaining(); System.out.println(); testIteratorRemove(); } //使用 hasNext 和 next遍历 public static void testIteratorNext() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String str = iterator.next(); System.out.println(str); } } //使用 Iterator 删除元素 public static void testIteratorRemove() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String str = iterator.next(); if ("222".equals(str)) { iterator.remove(); } } System.out.println(list); } //使用 forEachRemaining 遍历 public static void testForEachRemaining() { final Iterator<String> iterator = list.iterator(); iterator.forEachRemaining(new Consumer<String>() { public void accept(String t) { System.out.println(t); } }); } }1) add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前 2) set(E e) 迭代器返回的最后一个元素替换参数e 3) hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素 4) previous() 迭代器当前位置,反向遍历集合,下一个元素 5) previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标 6) nextIndex() 迭代器当前位置,返回下一个元素的下标
使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类 ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能 ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以 ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以 ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java.lang.UnsupportedOperationException 异常。 同理:Collections包也提供了对list和set集合的方法。
Collections.unmodifiableList(List) Collections.unmodifiableSet(Set)拓展:
我们很容易想到用final关键字进行修饰,我们都知道 final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写, final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的, 如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的, 但是这个引用所指向的对象里面的内容还是可以改变的。三、多线程
35.并行和并发有什么区别? 并发,指的是多个事情,在同一时间段内同时发生了。 并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的、
只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。 就像上面这张图,只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。
总之,一个程序至少有一个进程,一个进程至少有一个线程
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕, 那么jvm就会退出(即停止运行),此时连jvm都停止运行了,守护线程当然也就停止执行了。 换一种通俗的说法,如果有用户自定义线程存在的话,jvm就不会退出, 此时守护线程也不能退出,也就是它还要运行,为什么呢,就是为了执行垃圾回收的任务。1.继承Thread类型重写run 方法 2.实现Runnable接口 3.实现Callable接口
主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息Java中线程的状态分为6种。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。运行(RUNNABLE):Java线程中将就绪(ready)和**运行中(running)**两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。阻塞(BLOCKED):表示线程阻塞于锁。等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。终止(TERMINATED):表示该线程已经执行完毕。线程的状态图 1. 初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。2.1. 就绪状态
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。调用线程的start()方法,此线程进入就绪状态。当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。锁池里的线程拿到对象锁后,进入就绪状态。2.2. 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。3. 阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。4. 等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。5. 超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。6. 终止状态
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。sleep()方法:
功能
sleep ( )方法是Thread类的方法,线程通过调用该方法,进入休眠状态主动让出CPU,从而CPU可以执行其他的线程。经过sleep指定的时间后,CPU回到这个线程上继续往下执行。如果当前线程进入了同步锁,sleep()方法并不会释放锁。即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。使用场合
线程的调度执行是按照其优先级的高低顺序进行的,当高级别的线程未死亡时,低级别的线程没有机会获得CPU资源。有时优先级高的线程需要优先级低的线程完成一些辅助工作或者优先级高的线程需要完成一些比较费时的工作,此时优先级高的线程应该让出CPU资源,使得优先级低的线程有机会执行。为了达到这个目的,优先级高的线程可以在自己的run()方法中调用sleep方法来使自己放弃CPU资源,休眠一段时间。 wait()方法 功能:wait()方法可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个方法的同时,应该用notifyAll()方法通知所有由于使用了这个同步方法而处于等待的线程结束等待,曾中断的线程就会从刚才中断处继续执行这个同步方法(并不是立马执行,而是结束等待),并遵循“先中断先继续”的原则。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果)使用场合:
当一个线程使用的同步方法中用到某个变量,而此变量又需要启动线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()方法。 代码示例 package thread; public class MultiThread { public static void main(String[] args) throws InterruptedException { new Thread(new Thread1()).start(); //主动让出CPU,让CPU去执行其他的线程。在sleep指定的时间后,CPU回到这个线程上继续往下执行 Thread.sleep(5000); new Thread(new Thread2()).start(); } } class Thread1 implements Runnable{ @Override public void run() { synchronized (MultiThread.class){ System.out.println("进入线程1"); try{ System.out.println("线程1正在等待"); Thread.sleep(5000); //MultiThread.class.wait(); //wait是指一个已经进入同步锁的线程内(此处指Thread1),让自己暂时让出同步锁, //以便其他在等待此锁的线程(此处指Thread2)可以得到同步锁并运行。 }catch(Exception e){ System.out.println(e.getMessage()); e.printStackTrace(); } System.out.println("线程1结束等待,继续执行"); System.out.println("线程1执行结束"); } } } class Thread2 implements Runnable{ @Override public void run() { synchronized (MultiThread.class){ System.out.println("进入线程2"); System.out.println("线程2唤醒其他线程"); //Thread2调用了notify()方法,但该方法不会释放对象锁,只是告诉调用wait方法的线程可以去 //参与获得锁的竞争了。但不会马上得到锁,因为锁还在别人手里,别人还没有释放。 //如果notify()后面的代码还有很多,需要执行完这些代码才会释放锁。 MultiThread.class.notify(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2继续执行"); System.out.println("线程2执行结束"); } } }锁池:
假设线程A已经拥有对象锁,线程B、C想要获取锁就会被阻塞,进入一个地方去等待锁的等待,这个地方就是该对象的锁池;等待池:
假设线程A调用某个对象的wait方法,线程A就会释放该对象锁,同时线程A进入该对象的等待池中,进入等待池中的线程不会去竞争该对象的锁。notify和notifyAll的区别:
notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会;notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;run()方法:
是在主线程中执行方法,和调用普通方法一样;(按顺序执行,同步执行)start()方法:
是创建了新的线程,在新的线程中执行;(异步执行)问题
面试官:请问启动线程是start()还是run()方法,能谈谈吗?
应聘者:start()方法
当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体(真正的逻辑)。
6种
1. newFixedThreadPool
定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程 public class TestThreadPool { //定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量, //这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程 static ExecutorService fixedExecutor = Executors.newFixedThreadPool(3); public static void main(String[] args) { testFixedExecutor(); } //测试定长线程池,线程池的容量为3,提交6个任务,根据打印结果可以看出先执行前3个任务, //3个任务结束后再执行后面的任务 private static void testFixedExecutor() { for (int i = 0; i < 6; i++) { final int index = i; fixedExecutor.execute(new Runnable() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " index:" + index); } }); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); fixedExecutor.shutdown(); } }2. newCachedThreadPool
可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制测试代码:
public class TestThreadPool { //可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程, //任务增加时可以自动添加新线程,线程池的容量不限制 static ExecutorService cachedExecutor = Executors.newCachedThreadPool(); public static void main(String[] args) { testCachedExecutor(); } //测试可缓存线程池 private static void testCachedExecutor() { for (int i = 0; i < 6; i++) { final int index = i; cachedExecutor.execute(new Runnable() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " index:" + index); } }); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); cachedExecutor.shutdown(); } }3. newScheduledThreadPool
定长线程池,可执行周期性的任务测试代码:
public class TestThreadPool { //定长线程池,可执行周期性的任务 static ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(3); public static void main(String[] args) { testScheduledExecutor(); } //测试定长、可周期执行的线程池 private static void testScheduledExecutor() { for (int i = 0; i < 3; i++) { final int index = i; //scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务 scheduledExecutor.scheduleWithFixedDelay(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + " index:" + index); } }, 0, 3, TimeUnit.SECONDS); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); scheduledExecutor.shutdown(); } }4. newSingleThreadExecutor
单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行测试代码:
public class TestThreadPool { //单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行 static ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); public static void main(String[] args) { testSingleExecutor(); } //测试单线程的线程池 private static void testSingleExecutor() { for (int i = 0; i < 3; i++) { final int index = i; singleExecutor.execute(new Runnable() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " index:" + index); } }); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); singleExecutor.shutdown(); } }5. newSingleThreadScheduledExecutor
单线程可执行周期性任务的线程池测试代码:
public class TestThreadPool { //单线程可执行周期性任务的线程池 static ScheduledExecutorService singleScheduledExecutor =Executors.newSingleThreadScheduledExecutor(); public static void main(String[] args) { testSingleScheduledExecutor(); } //测试单线程可周期执行的线程池 private static void testSingleScheduledExecutor() { for (int i = 0; i < 3; i++) { final int index = i; //scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务 singleScheduledExecutor.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + " index:" + index); } }, 0, 3, TimeUnit.SECONDS); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); singleScheduledExecutor.shutdown(); } }6. newWorkStealingPool
任务窃取线程池,不保证执行顺序,适合任务耗时差异较大。线程池中有多个线程队列,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,它就会去其它的线程队列中窃取任务。解决饥饿导致的效率问题。 默认创建的并行 level 是 CPU 的核数。主线程结束,即使线程池有任务也会立即停止。测试代码:
public class TestThreadPool { //任务窃取线程池 static ExecutorService workStealingExecutor = Executors.newWorkStealingPool(); public static void main(String[] args) { testWorkStealingExecutor(); } //测试任务窃取线程池 private static void testWorkStealingExecutor() { for (int i = 0; i < 10; i++) {//本机 CPU 8核,这里创建10个任务进行测试 final int index = i; workStealingExecutor.execute(new Runnable() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " index:" + index); } }); } try { Thread.sleep(4000);//这里主线程不休眠,不会有打印输出 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后..."); // workStealingExecutor.shutdown(); } }1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。 (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0! 2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的任务数量为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。 (2) 状态切换:线程池处在TIDYING状态时,执行完**terminated()**之后,就会由 TIDYING -> TERMINATED。
区别:
submit(Callable task)、submit(Runnable task, T result)、submit(Runnable task) 归属于ExecutorService接口。
execute(Runnable command)归属于Executor接口。ExecutorService继承了Executor。
submit()有返回值。
execute没有返回值。
public class ThreadPoolTest { private String taskName; public ThreadPoolTest(String taskName) { this.taskName = taskName; } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new Runnable() { @Override public void run() { System.out.println("execute任务执行中"); } }); System.out.println("----分界线----"); Future<String> future = executorService.submit(() -> { System.out.println("submit任务执行中"); return "submit任务完成,这是执行结果"; }); try { //如果future.get()返回null,任务完成 System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); System.out.println("任务失败原因:" + e.getCause().getMessage()); } executorService.shutdown(); } } //输出: ----分界线---- execute任务执行中 submit任务执行中 submit任务完成,这是执行结果 submit()方便做异常处理。通过Future.get()可捕获异常。 public class ThreadPoolTest implements Runnable { private String taskName; public ThreadPoolTest(String taskName) { this.taskName = taskName; } @Override public void run() { throw new RuntimeException("此处" + this.taskName + "抛出异常。"); } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new ThreadPoolTest("task1")); System.out.println("----分界线----"); Future<?> future = executorService.submit(new ThreadPoolTest("task2")); try { future.get();//如果future.get()返回null,任务完成 } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); System.out.println("任务失败原因:" + e.getCause().getMessage()); } executorService.shutdown(); } }锁的级别从低到高:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁锁分级别原因:
没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。无锁:
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。偏向锁:
对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。轻量级锁:
轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。重量级锁:
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
产生死锁的四个必要条件(互请不循):
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的【循环等待资源】关系。
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。 ThreadLocal 使用例子:
public class TestThreadLocal { //线程本地存储变量 private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public static void main(String[] args) { for (int i = 0; i < 3; i++) {//启动三个线程 Thread t = new Thread() { @Override public void run() { add10ByThreadLocal(); } }; t.start(); } } /** * 线程本地存储变量加 5 */ private static void add10ByThreadLocal() { for (int i = 0; i < 5; i++) { Integer n = THREAD_LOCAL_NUM.get(); n += 1; THREAD_LOCAL_NUM.set(n); System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n); } } }打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15 实现原理:
按照我们第一直觉,感觉 ThreadLocal 内部肯定是有个 Map 结构,key 存了 Thread,value 存了 本地变量 V 的值。每次通过 ThreadLocal 对象的 get() 和 set(T value) 方法获取当前线程里存的本地变量、设置当前线程里的本地变量。
而 JDK 的实现里面这个 Map 是属于 Thread,而非属于 ThreadLocal。ThreadLocal 仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。ThreadLocalMap 属于 Thread 也更加合理。
还有一个更加深层次的原因,这样设计不容易产生内存泄露。 ThreadLocal 持有的 Map 会持有 Thread 对象的引用,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往比线程要长,所以这种设计方案很容易导致内存泄露。
JDK 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。JDK 的这种实现方案复杂但更安全。
synchronized是jvm实现的一种互斥同步访问方式,底层是基于每个对象的监视器(monitor)来实现的。被synchronized修饰的代码,在被编译器编译后在被修饰的代码前后加上了一组字节指令。
在代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问。
在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念。 Java并发锁
当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获取该对象锁。
当执行monitorexit时,锁计数器-1,当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁。
作用:
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。区别:
synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。synchronized 可以保证线程间的有序性(猜测是无法保证线程内的有序性, 即线程内的代码可能被 CPU指令重排序)、原子性和可见性; volatile 只保证了可见性和有序性,无法保证原子性。synchronized 线程阻塞,volatile 线程不阻塞。执行结果:
package com.cn.test.thread.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockTest { private Lock lock = new ReentrantLock(); /* * 尝试获取锁 tryLock() 它表示用来尝试获取锁,如果获取成功,则返回true, * 如果获取失败(即锁已被其他线程获取),则返回false */ public void tryLockTest(Thread thread) { if(lock.tryLock()) { //尝试获取锁 try { System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称 Thread.sleep(2000);//为看出执行效果,是线程此处休眠2秒 } catch (Exception e) { System.out.println("线程"+thread.getName() + "发生了异常释放锁"); }finally { System.out.println("线程"+thread.getName() + "执行完毕释放锁"); lock.unlock(); //释放锁 } }else{ System.out.println("我是线程"+Thread.currentThread().getName()+"当前锁被别人占用,我无法获取"); } } public static void main(String[] args) { LockTest lockTest = new LockTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { lockTest.tryLockTest(Thread.currentThread()); } }, "thread1"); //声明一个线程 “线程二” Thread thread2 = new Thread(new Runnable() { @Override public void run() { lockTest.tryLockTest(Thread.currentThread()); } }, "thread2"); // 启动2个线程 thread2.start(); thread1.start(); } }执行结果:
package com.cn.test.thread.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockTest { private Lock lock = new ReentrantLock(); public void tryLockParamTest(Thread thread) throws InterruptedException { if(lock.tryLock(3000, TimeUnit.MILLISECONDS)) { //尝试获取锁 获取不到锁,就等3秒,如果3秒后还是获取不到就返回false try { System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称 Thread.sleep(4000);//为看出执行效果,是线程此处休眠2秒 } catch (Exception e) { System.out.println("线程"+thread.getName() + "发生了异常释放锁"); }finally { System.out.println("线程"+thread.getName() + "执行完毕释放锁"); lock.unlock(); //释放锁 } }else{ System.out.println("我是线程"+Thread.currentThread().getName()+"当前锁被别人占用,等待3s后仍无法获取,放弃"); } } public static void main(String[] args) { LockTest lockTest = new LockTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { lockTest.tryLockParamTest(Thread.currentThread()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "thread1"); //声明一个线程 “线程二” Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { lockTest.tryLockParamTest(Thread.currentThread()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "thread2"); // 启动2个线程 thread2.start(); thread1.start(); } }1、原始构成:
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。ReentrantLock是一个具体类,是API层面的锁。2、使用方法:
synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。3、等待是否可中断:
synchronized不可中断,除非抛出异常或者正常运行完成ReentrantLock可中断4、加锁是否公平:
synchronized非公平锁ReentrantLock两者都可以,默认是非公平锁。5、锁绑定多个条件Condition:
synchronized没有。ReentrantLock可用来分组唤醒需要唤醒的线程。 而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。注意事项:
某个类可以被序列化,则其子类也可以被序列化声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient表示临时数据反序列化读取序列化对象的顺序要保持一致具体使用
package constxiong.interview; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 测试序列化,反序列化 * @author ConstXiong * @date 2019-06-17 09:31:22 */ public class TestSerializable implements Serializable { private static final long serialVersionUID = 5887391604554532906L; private int id; private String name; public TestSerializable(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "TestSerializable [id=" + id + ", name=" + name + "]"; } @SuppressWarnings("resource") public static void main(String[] args) throws IOException, ClassNotFoundException { //序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TestSerializable.obj")); oos.writeObject("测试序列化"); oos.writeObject(618); TestSerializable test = new TestSerializable(1, "ConstXiong"); oos.writeObject(test); //反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TestSerializable.obj")); System.out.println((String)ois.readObject()); System.out.println((Integer)ois.readObject()); System.out.println((TestSerializable)ois.readObject()); } }答:动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。
应用场景如:
统计每个 api 的请求耗时统一的日志输出校验被调用的 api 是否已经登录和权限鉴定Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程1、JDK实现动态代理
主要使用了Proxy.newProxyInstance()方法,该方法的官方解释为:返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。下面分别举例说明。
2.CGLIB动态代理
需要引入CGLIB相关Jar包使用JDK的Proxy实现动态代理,要求目标类与代理类实现相同的接口,若目标类不存在接口,则无法使用该方式实现。对于没有接口的类,要为其创建动态代理,就要使用CGLIB来实现。CGLIB动态代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。使用CGLIB生成代理类,要求目标类必须能被继承,因此不能是final类。有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。 import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class MyUtil { private MyUtil() { throw new AssertionError(); } public static <T> T clone(T obj) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bout); oos.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bin); return (T) ois.readObject(); // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义 // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源, //这一点不同于对外部资源(如文件流)的释放 } }下面是测试代码:
import java.io.Serializable; /** * 人类 * @author * */ class Person implements Serializable { private static final long serialVersionUID = -9102017020286042305L; private String name; // 姓名 private int age; // 年龄 private Car car; // 座驾 public Person(String name, int age, Car car) { this.name = name; this.age = age; this.car = car; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Car getCar() { return car; } public void setCar(Car car) { this.car = car; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + ", car=" + car + "]"; } } /** * 小汽车类 * @author * */ class Car implements Serializable { private static final long serialVersionUID = -5713945027627603702L; private String brand; // 品牌 private int maxSpeed; // 最高时速 public Car(String brand, int maxSpeed) { this.brand = brand; this.maxSpeed = maxSpeed; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public int getMaxSpeed() { return maxSpeed; } public void setMaxSpeed(int maxSpeed) { this.maxSpeed = maxSpeed; } @Override public String toString() { return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]"; } } class CloneTest { public static void main(String[] args) { try { Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300)); Person p2 = MyUtil.clone(p1); // 深度克隆 p2.getCar().setBrand("BYD"); // 修改克隆的Person对象p2关联的汽车对象的品牌属性 // 原来的Person对象p1关联的汽车不会受到任何影响 // 因为在克隆Person对象时其关联的汽车对象也被克隆了 System.out.println(p1); } catch (Exception e) { e.printStackTrace(); } } }答:复制一个 Java 对象
浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。 深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。 假设B复制了A,当修改A时,看B是否会发生变化, 如果B也跟着变了,说明这是浅拷贝,拿人手短;如果B没变,那就是深拷贝,自食其力。