Java核心技术36讲(个人整理)

今天我要问你的问题是,谈谈你对 Java 平台的理解? “Java 是解释执行”,这句话正确吗?

Java特性:
面向对象(封装,继承,多态)
平台无关性(JVM运行.class文件) 书写一次,到处运行”(Write once, run anywhere)

语言(泛型,Lambda)
类库(集合,并发,网络,IO/NIO)
JRE(Java运行环境,JVM,类库)
JDK(Java开发工具,包括JRE,javac,诊断工具)

Java 语言特性,包括泛型、Lambda 等语言特性;基础类库,包括集合、IO/NIO、网络、并发、安全等基础类库


Java是解析运行吗?
不正确!
1,Java源代码经过Javac编译成.class文件
2,.class文件经JVM解析或编译运行。
(1)解析:.class文件经过JVM内嵌的解析器解析执行。
(2)编译:存在JIT编译器(Just In Time Compile 即时编译器)把经常运行的代码作为"热点代码"编译与本地平台相关的机器码,并进行各种层次的优化。
(3)AOT编译器: Java 9提供的直接将所有代码编译成机器码执行。

类加载大致过程:加载、验证、链接、初始化

Exception和Error有什么区别?

在Java世界里,异常的出现让我们编写的程序运行起来更加的健壮,同时为程序在调试、运行期间发生的一些意外情况,提供了补救机会;即使遇到一些严重错误而无法弥补,异常也会非常忠实的记录所发生的这一切。以下是文章心得感悟:

1 不要推诿或延迟处理异常,就地解决最好,并且需要实实在在的进行处理,而不是只捕捉,不动作。

2 一个函数尽管抛出了多个异常,但是只有一个异常可被传播到调用端。最后被抛出的异常时唯一被调用端接收的异常,其他异常都会被吞没掩盖。如果调用端要知道造成失败的最初原因,程序之中就绝不能掩盖任何异常。

3 不要在finally代码块中处理返回值。

4 按照我们程序员的惯性认知:当遇到return语句的时候,执行函数会立刻返回。但是,在Java语言中,如果存在finally就会有例外。除了return语句,try代码块中的break或continue语句也可能使控制权进入finally代码块。

5 请勿在try代码块中调用return、break或continue语句。万一无法避免,一定要确保finally的存在不会改变函数的返回值。

6 函数返回值有两种类型:值类型与对象引用。对于对象引用,要特别小心,如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上。

7 勿将异常用于控制流。

8 如无必要,勿用异常。

其中有些子类型,最好重点理解一下,比如 NoClassDefFoundError 和 ClassNotFoundException 有什么区别:

NoClassDefFoundError是一个错误(Error),而ClassNOtFoundException是一个异常,在Java中对于错误和异常的处理是不同的,我们可以从异常中恢复程序但却不应该尝试从错误中恢复程序。
ClassNotFoundException的产生原因:

Java支持使用Class.forName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。

NoClassDefFoundError产生的原因在于:
如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError.
造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。

参考:https://blog.csdn.net/mudeer2012/article/details/48477965

try-catch 代码段会产生额外的性能开销,所以尽可能的try小范围的代码

Objects. requireNonNull(filename);   针对空指针异常的处理,让异常暴露更清晰

用于暴露异常,易于查找程序哪里出现问题,结合具体的日志工具

1.异常:这种情况下的异常,可以通过完善任务重试机制,当执行异常时,保存当前任务信息加入重试队列。重试的策略根据业务需要决定,当达到重试上限依然无法成功,记录任务执行失败,同时发出告警。
2.日志:类比消息中间件,处在不同线程之间的同一任务,简单高效一点的做法可能是用traceId/requestId串联。有些日志系统本身支持MDC/NDC功能,可以串联相关联的日志。

响应式编程,我可以泛化为异步编程的概念嘛?一般各种异步编程框架都会对异常的传递和堆栈信息做处理吧?比如promise/future风格的。本质上大致就是把lambda中的异常捕获并封装,再进一步延续异步上下文,或者转同步处理时拿到原始的错误和堆栈信息

谈谈final、finally、 finalize有什么不同?

1。final修饰的类,不可被继承,修饰的方法不可被重写,修饰的变量不可多次赋值。通过final能够得到性能上的优化,但是不明显,如果大量使用可能会干扰代码,不能表达出本来具有的含义。故不使用。匿名内部类,访问局部变量要求传入的参数,必须是final是要保证数据一致性问题。
2。finally。代码中总是会执行的代码段。除了退出虚拟机外。
3。finalize。在虚拟机回收改对象前进行调用。此种方式不可取。因为java虚拟机不知道在什么时候才对对象进行回收。

final修饰类,变量,方法等;目的是不可变,这样做使得安全、提高性能

copy-on-write 原则:

链接:http://www.cnblogs.com/hapjin/p/4840107.html

String类为什么是final的:

1、从设计安全)上讲, 
1)、确保它们不会在子类中改变语义。String类是final类,这意味着不允许任何人定义String的子类。
换言之,
如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。 
2)、String 一旦被创建是不能被修改的,
因为 java 设计者将 String 为可以共享的

2、从效率上讲: 
1)、设计成final,JVM才不用对相关方法在虚函数表中查询,而直接定位到String类的相关方法上,提高了执行效率。 
2)、Java设计者认为共享带来的效率更高。
 

总而言之,就是要保证 java.lang.String 引用的对象一定是 java.lang.String的对象,而不是引用它的子孙类,这样才能保证它的效率和安全。

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

不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。

1. 对象可达性状态流转分析

对象生命周期和不同可达性状态

牛人总结:

在Java语言中,除了基本数据类型外,其他的都是指向各类对象的对象引用;Java中根据其生命周期的长短,将引用分为4类。

1 强引用

特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

2 软引用

特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

3 弱引用

弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

应用场景:弱应用同样可用于内存敏感的缓存。

4 虚引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue); 
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

------------------------------------------------------------------------------------------------------------------------------------------------------------------

1. 强引用:项目中到处都是。

2. 软引用:图片缓存框架中,“内存缓存”中的图片是以这种引用来保存,使得JVM在发生OOM之前,可以回收这部分缓存

3. 虚引用:在静态内部类中,经常会使用虚引用。例如,一个类发送网络请求,承担callback的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏

4. 幽灵引用:这种引用的get()方法返回总是null,所以,可以想象,在平常的项目开发肯定用的少。但是根据这种引用的特点,我想可以通过监控这类引用,来进行一些垃圾清理的动作。不过具体的场景,还是希望峰哥举几个稍微详细的实战性的例子?

String、StringBuffer、StringBuilder有什么区别?

今日String/StringBuffer/StringBuilder心得:


1 String

(1) String的创建机理
由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。上述原则只适用于通过直接量给String对象引用赋值的情况。

举例:String str1 = "123"; //通过直接量赋值方式,放入字符串常量池
String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池

注意:String提供了inter()方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并且返回此池中对象的引用。


(2) String的特性
[A] 不可变。是指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式。

[B] 针对常量池的优化。当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

2 StringBuffer/StringBuilder

StringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,拥有几乎一致对外提供的调用接口;其底层在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型的数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变以后,对象引用不会发生改变;两者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先评估大小,可提升性能。

唯一需要注意的是:StringBuffer是线程安全的,但是StringBuilder是线程不安全的。可参看Java标准类库的源代码,StringBuffer类中方法定义前面都会有synchronize关键字。为此,StringBuffer的性能要远低于StringBuilder。


3 应用场景

[A]在字符串内容不经常发生变化的业务场景优先使用String类。例如:常量声明、少量的字符串拼接操作等。如果有大量的字符串内容拼接,避免使用String与String之间的“+”操作,因为这样会产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。

[B]在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBuffer,例如XML解析、HTTP参数解析与封装。

[C]在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等。

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

代理模式(通过代理静默地解决一些业务无关的问题,比如远程、安全、事务、日志、资源关闭……让应用开发者可以只关心他的业务)
    静态代理:事先写好代理类,可以手工编写,也可以用工具生成。缺点是每个业务类都要对应一个代理类,非常不灵活。
    动态代理:运行时自动生成代理对象。缺点是生成代理代理对象和调用代理方法都要额外花费时间。
        JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。新版本也开始结合ASM机制。
        cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
Java 发射机制的常见应用:动态代理(AOP、RPC)、提供第三方开发者扩展能力(Servlet容器,JDBC连接)、第三方组件创建对象(DI)……

int和Integer有什么区别?

[1] 基本类型均具有取值范围,在大数*大数的时候,有可能会出现越界的情况。
[2] 基本类型转换时,使用声明的方式。例:long result= 1234567890 * 24 * 365;结果值一定不会是你所期望的那个值,因为1234567890 * 24已经超过了int的范围,如果修改为:long result= 1234567890L * 24 * 365;就正常了。
[3] 慎用基本类型处理货币存储。如采用double常会带来差距,常采用BigDecimal、整型(如果要精确表示分,可将值扩大100倍转化为整型)解决该问题。
[4] 优先使用基本类型。原则上,建议避免无意中的装箱、拆箱行为,尤其是在性能敏感的场合,
[5] 如果有线程安全的计算需要,建议考虑使用类型AtomicInteger、AtomicLong 这样的线程安全类。部分比较宽的基本数据类型,比如 float、double,甚至不能保证更新操作的原子性,可能出现程序读取到只更新了一半数据位的数值。

JAVA的内存结构分为3部分:
1,对象头 有两部分,markWord和Class对象指针,
markwork包括存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,
2,实例数据 
3,对齐填充

1. Mark Word:标记位 4字节,类似轻量级锁标记位,偏向锁标记位等。
2. Class对象指针:4字节,指向对象对应class对象的内存地址。
3. 对象实际数据:对象所有成员变量。
4. 对齐:对齐填充字节,按照8个字节填充。

Integer占用内存大小,4+4+4+4=16字节。

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

Vector、ArrayList、LinkedList均为线型的数据结构,但是从实现方式与应用场景中又存在差别。

1 底层实现方式
ArrayList内部用数组来实现;LinkedList内部采用双向链表实现;Vector内部用数组实现。

2 读写机制
ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。

LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。
Vector与ArrayList仅在插入元素时容量扩充机制不一致。对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;当插入元素数组大小不够时,如果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。

3 读写效率

ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。

LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但是检索速度较慢。

4 线程安全性

ArrayList、LinkedList为非线程安全;Vector是基于synchronized实现的线程安全的ArrayList。

需要注意的是:单线程应尽量使用ArrayList,Vector因为同步会有性能损耗;即使在多线程环境下,我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个线程安全的同步列表对象。

问题回答

利用PriorityBlockingQueue或Disruptor可实现基于任务优先级为调度策略的执行调度系统。

对比Hashtable、HashMap、TreeMap有什么不同?

三者均实现了Map接口,存储的内容是基于key-value的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值。

(1) 元素特性
HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然。

(2)顺序特性
HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式。

(3)初始化与增长方式
初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。
扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。

(4)线程安全性
HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。

(5)一段话HashMap
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

解决哈希冲突的常用方法有4种:

开放定址法
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key)  i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

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

1.7
put加锁
通过分段加锁segment,一个hashmap里有若干个segment,每个segment里有若干个桶,桶里存放K-V形式的链表,put数据时通过key哈希得到该元素要添加到的segment,然后对segment进行加锁,然后在哈希,计算得到给元素要添加到的桶,然后遍历桶中的链表,替换或新增节点到桶中

size
分段计算两次,两次结果相同则返回,否则对所以段加锁重新计算


1.8
put CAS 加锁
1.8中不依赖与segment加锁,segment数量与桶数量一致;
首先判断容器是否为空,为空则进行初始化利用volatile的sizeCtl作为互斥手段,如果发现竞争性的初始化,就暂停在那里,等待条件恢复,否则利用CAS设置排他标志(U.compareAndSwapInt(this, SIZECTL, sc, -1));否则重试
对key hash计算得到该key存放的桶位置,判断该桶是否为空,为空则利用CAS设置新节点
否则使用synchronize加锁,遍历桶中数据,替换或新增加点到桶中
最后判断是否需要转为红黑树,转换之前判断是否需要扩容

size
利用LongAdd累加计算

CAS:
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。
(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,
则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

--------------------
reentrantlock底层实现:

构造器分为公平锁(排队)、非公平锁(插队),默认为非公平锁      公平锁会消耗更多性能

-------------------
.lock() 获取锁:

.lock()获取锁时,是基于cas(compare and swap)  

    final void lock() {
        if (compareAndSetState(0, 1))     //有一个state状态值
        setExclusiveOwnerThread(Thread.currentThread());
        else
        acquire(1);
    }

compareAndSetState(0, 1) 会判断内存位置的值是否为0,是则把值替换为1,并且当前线程获取锁成功,拿到当前线程;这样后来获取锁的线程就会进入else;

acquire(1)过程为等待线程进入队列,在进入队列之前也会多次尝试再次获取锁且都是基于cas操作,成功则返回,失败则进入队列等待

---------------------
.unlock()   释放锁:

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases;
    // 如果不是当前线程占用锁,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 锁被重入次数为0,表示释放成功
        free = true;
        // 清空独占线程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}
这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。
若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,
并且则清空独占线程,最后更新state值,返回free。


-------------

公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1),这里不再赘述。  

-------------

在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,
获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。

-------------

Thread.interrupted();//中断线程


4、redis的数据类型,以及每种数据类型的使用场景

分析:是不是觉得这个问题很基础,其实我也这么觉得。然而根据面试经验发现,至少百分八十的人答不上这个问题。建议,在项目中用到后,再类比记忆,体会更深,不要硬记。基本上,一个合格的程序员,五种类型都会用到。
回答:一共五种


(一)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。

(二)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。

(四)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

(五)sorted set

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,参照另一篇《分布式之延时任务方案解析》,该文指出了sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。

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

我上面列出的回答是基于一种常见分类方式,即所谓的 BIO、NIO、NIO 2(AIO)

NIO 2是借助AsychronousChannelGroup,实现多Channel方案,有具备异步功能,解决NIO 1单线程多通路问题。
不过异步有两种方案:
1.New一个CompletionHandler对象,在重写方法中处理,Write和Read.
2.通过concurrent.Future对象,建模一个挂起操作,之后可以通过Get获取socketChannel处理Write和Read。

线程

当 Key 为 null 时,该条目就变成“废弃条目”,相关“value”的回收,往往依赖于几个关键点,即 set、remove、rehash。

下面是 set 的示例,我进行了精简和注释:

写一个最简单的打印 HelloWorld 的程序,说说看,运行这个应用,Java 至少会创建几个线程呢?

1、站在应用程序方面,只创建了一个线程。
2、站在jvm方面,肯定还有gc等其余线程。

使用了两种方式获取当前程序的线程数。
1、使用线程管理器MXBean
2、直接通过线程组的activeCount
第二种需要注意不断向上找父线程组,否则只能获取当前线程组,结果是1

JVM 启动 Hello World的线程分析
环境:
macOS + jdk8
检测获得
Thread[Reference Handler,10,system]
Thread[Finalizer,8,system]
Thread[main,5,main]
Thread[Signal Dispatcher,9,system]
Hello World!
其中:
Reference Handler:处理引用对象本身的垃圾回收
Finalizer:处理用户的Finalizer方法
Signal Dispatcher:外部jvm命令的转发器

在jdk6环境中
还有一个Attach Listener的线程
是负责接收外部命令的,如jmap、jstack

此外,如果使用的IDE是IDEA 直接运行会多一个Monitor Ctrl-break线程,这个是IDE的原因。debug模式下不会有这个线程。

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


  
  
  1. public static void copyFileByChannel(File source, File dest) throws
  2.        IOException
  3. {
  4.    try (FileChannel sourceChannel = new FileInputStream(source)
  5.             .getChannel() ;
  6.          FileChannel targetChannel = new FileOutputStream(dest).getChannel
  7.                  ();){
  8.        for (long count = sourceChannel.size(); count > 0; ) {
  9.            long transferred = sourceChannel.transferTo(
  10.                     sourceChannel.position(), count, targetChannel);            
  11.              sourceChannel.position(sourceChannel.position() + transferred);
  12.            count -= transferred;
  13.        }
  14.    }
  15.  }

你可以思考下,如果我们需要在 channel 读取的过程中,将不同片段写入到相应的 Buffer 里面(类似二进制消息分拆成消息头、消息体等),可以采用 NIO 的什么机制做到呢?

可以利用NIO分散-scatter机制来写入不同buffer。
Code:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header, body};
channel.read(bufferArray);
注意:该方法适用于请求头长度固定。

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

接口vs抽象类vs类

1 支持多重继承:接口支持;抽象类不支持;类不支持;
2 支持抽象函数:接口语义上支持;抽象类支持;类不支持;
3 允许函数实现:接口不允许;抽象类支持;类允许;
4 允许实例化:接口不允许;抽象类不允许;类允许;
5 允许部分函数实现:接口不允许;抽象类允许;类不允许。
6 定义的内容:接口中只能包括public函数以及public static final常量;抽象类与类均无任何限制。
7 使用时机:当想要支持多重继承,或是为了定义一种类型请使用接口;当打算提供带有部分实现的“模板”类,而将一些功能需要延迟实现请使用抽象类;当你打算提供完整的具体实现请使用类。

在实际项目开发过程,一方面是业务需求频繁,需要满足开闭原则,也就是小到一个模块,大到一个架构都需要有好的可扩展性;另外一方面软件往往是团队协同开发的过程;由于团队成员水平参差不齐,这方面的坑不少。可以通过前期做好设计评审、code review等手段去提升代码质量。

谈谈你知道的设计模式?

关于加@Transactional注解的方法之间调用,事务是否生效的问题

事务传播行为,所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

    TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

    TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。

    TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

    TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

    TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

    TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

    TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价TransactionDefinition.PROPAGATION_REQUIRED。

1. 不同类之间的方法调用,如类A的方法a()调用类B的方法b(),这种情况事务是正常起作用的。只要方法a()或b()配置了事务,运行中就会开启事务,产生代理。

若两个方法都配置了事务,两个事务具体以何种方式传播,取决于设置的事务传播特性。

2. 同一个类内方法调用:重点来了,同一个类内的方法调用就没那么简单了,假定类A的方法a()调用方法b()

  •     同一类内方法调用,无论被调用的b()方法是否配置了事务,此事务在被调用时都将不生效。

个人理解,当从类外调用方法a()时,从spring容器获取到的serviceImpl对象实际是包装好的proxy对象,因此调用a()方法的对象是动态代理对象。而在类内部a()调用b()的过程中,实质执行的代码是this.b(),此处this对象是实际的serviceImpl对象而不是本该生成的代理对象,因此直接调用了b()方法。

aop原理跟事务一样,往大里说是动态代理,往小里说是反射机制。我又测试了两个方法,分别加上aop增强通知,类内调用的效果跟事务是一样的。这里最好研究一下spring aop和事务的源码,应该能搞得更清楚。
 

今天我要问你的问题是,谈谈你对 Java 平台的理解? “Java 是解释执行”,这句话正确吗?

猜你喜欢

转载自blog.csdn.net/weixin_41722928/article/details/89090350