典型回答:
final能够用来修饰类、方法、变量,分别有不一样的意义, final修饰的class表明不能够继承扩展, final的变量是不能够修改的,而final的方法也是不能够重写的( override)。html
finally则是Java保证重点代码必定要被执行的一种机制。咱们能够使用try-finally或者try-catch-finally来进行相似关闭JDBC链接、保证unlock锁等动做。java
finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。 finalize机制如今已经不推荐使用,而且在JDK 9开始被标记
为deprecated。node
不一样的引用类型,主要体现的是对象不一样的可达性( reachable)状态和对垃圾收集的影响。web
所谓强引用( “Strong” Reference),就是咱们最多见的普通对象引用,只要还有强引用指向一个对象,就能代表对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对
象,若是没有其余的引用关系,只要超过了引用的做用域或者显式地将相应(强)引用赋值为null,就是能够被垃圾收集的了,固然具体回收时机仍是要看垃圾收集策略。面试
软引用( SoftReference),是一种相对强引用弱化一些的引用,可让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。 JVM会确保在抛
出OutOfMemoryError以前,清理软引用指向的对象。软引用一般用来实现内存敏感的缓存,若是还有空闲内存,就能够暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓
存的同时,不会耗尽内存。
andriod中的图片缓存是软引用的例子.算法
弱引用( WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就能够用来构建一种没有特定约束的关系,好比,维护一种非强制性
的映射关系,若是试图获取时对象还在,就使用它,不然重现实例化。它一样是不少缓存实现的选择。
ThreadLocal中entry的Key是弱引用的例子.docker
对于幻象引用,有时候也翻译成虚引用,你不能经过它访问对象。幻象引用仅仅是提供了一种确保对象被fnalize之后,作某些事情的机制,好比,一般用来作所谓的PostMortem清理机制,我在专栏上一讲中介绍的Java平台自身Cleaner机制等,也有人利用幻象引用监控对象的建立和销毁。shell
String是Java语言很是基础和重要的类,提供了构造和管理字符串的各类基本逻辑。它是典型的Immutable类,被声明成为fnal class,全部属性也都是fnal的。也因为它的不可
变性,相似拼接、裁剪字符串等动做,都会产生新的String对象。因为字符串操做的广泛性,因此相关操做的效率每每对应用性能有明显影响。数据库
StringBufer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,咱们能够用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。 StringBufer本
质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,因此除非有线程安全的须要,否则仍是推荐使用它的后继者,也就是StringBuilder。编程
StringBuilder是Java 1.5中新增的,在能力上和StringBufer没有本质区别,可是它去掉了线程安全的部分,有效减少了开销,是绝大部分状况下进行字符串拼接的首选。
String是Immutable类的典型实现,原生的保证了基础线程安全,由于你没法对它内部数据进行任何修改,这种便利甚至体如今拷贝构造函数中,因为不可
变, Immutable对象在拷贝时不须要额外复制数据。
为了实现修改字符序列的目的, StringBufer和StringBuilder底层都是利用可修改的( char, JDK 9之后是byte)数组,两者都继承了AbstractStringBuilder,里面包含了基本
操做,区别仅在于最终的方法是否加了synchronized。
典型回答:
反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省( introspect,官方用语)的能力。经过反射咱们能够直接操做类或者对象,好比获取某个对象的类定义,获取类
声明的属性和方法,调用方法或者构造对象,甚至能够运行时修改类定义。
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,不少场景都是利用相似机制作到的,好比用来包装RPC调用、面向切面的编程( AOP)。
实现动态代理的方式不少,好比JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其余的实现方式,好比利用传说中更高性能的字节码操做机制,类
似ASM、 cglib(基于ASM)、 Javassist等。
咱们知道Spring AOP支持两种模式的动态代理, JDK Proxy或者cglib,若是咱们选择cglib方式,你会发现对接口的依赖被克服了。
cglib动态代理采起的是建立目标类的子类的方式,由于是子类化,咱们能够达到近似使用被调用者自己的效果。
典型回答:
int是咱们常说的整形数字,是Java的8个原始数据类型( Primitive Types, boolean、 byte 、 short、 char、 int、 foat、 double、 long)之一。 Java语言虽然号称一切都是对象,
但原始数据类型是例外。
Integer是int对应的包装类,它有一个int类型的字段存储数据,而且提供了基本操做,好比数学运算、 int和字符串之间转换等。在Java 5中,引入了自动装箱和自动拆箱功能
( boxing/unboxing), Java能够根据上下文,自动进行转换,极大地简化了相关编程。
关于Integer的值缓存,这涉及Java 5中另外一个改进。构建Integer对象的传统方式是直接调用构造器,直接new一个对象。可是根据实践,咱们发现大部分数据操做都是集中在有
限的、较小的数值范围,于是,在Java 5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照Javadoc, 这个值默认缓存
是-128到127之间。
这种缓存机制并非只有Integer才有,一样存在于其余的一些包装类,好比:
注意事项:
[1] 基本类型均具备取值范围,在大数*大数的时候,有可能会出现越界的状况。
[2] 基本类型转换时,使用声明的方式。例: int result= 1234567890 * 24 * 365;结果值必定不会是你所指望的那个值,由于1234567890 * 24已经超过了int的范围,若是修改成: long result= 1234567890L * 24 * 365;就正常了。
[3] 慎用基本类型处理货币存储。如采用double常会带来差距,常采用BigDecimal、整型(若是要精确表示分,可将值扩大100倍转化为整型)解决该问题。
[4] 优先使用基本类型。原则上,建议避免无心中的装箱、拆箱行为,尤为是在性能敏感的场合,
[5] 若是有线程安全的计算须要,建议考虑使用类型AtomicInteger、 AtomicLong 这样的线程安全类。部分比较宽的基本数据类型,好比 foat、 double,甚至不能保证更新操做的原子性,
可能出现程序读取到只更新了一半数据位的数值。
[4].原则上, 建议避免无心中的装箱、拆箱行为,尤为是在性能敏感的场合,建立10万个Java对象和10万个整数的开销可不是一个数量级的,无论是内存使用仍是处理速度,光是对象头
的空间占用就已是数量级的差距了。
以咱们常常会使用到的计数器实现为例,下面是一个常见的线程安全计数器实现。
class Counter { private fnal AtomicLong counter = new AtomicLong(); public void increase() { counter.incrementAndGet(); } }
若是利用原始数据类型,能够将其修改成
class CompactCounter { private volatile long counter; private satic fnal AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter"); public void increase() { updater.incrementAndGet(this); } }
Java原始数据类型和引用类型局限性:
前面我谈了很是多的技术细节,最后再从Java平台发展的角度来看看,原始数据类型、对象的局限性和演进。
对于Java应用开发者,设计复杂而灵活的类型系统彷佛已经习觉得常了。可是坦白说,毕竟这种类型系统的设计是源于不少年前的技术决定,如今已经逐渐暴露出了一些反作用,例
如:
原始数据类型和Java泛型并不能配合使用
这是由于Java的泛型某种程度上能够算做伪泛型,它彻底是一种编译期的技巧, Java编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型能够转换
为Object。
没法高效地表达数据,也不便于表达复杂的数据结构,好比vector和tuple咱们知道Java的对象都是引用类型,若是是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则否则,数据存储的是引用,对象每每是分散地存储在堆的不一样位
置。这种设计虽然带来了极大灵活性,可是也致使了数据操做的低效,尤为是没法充分利用现代CPU缓存机制。
典型回答:
Vector是Java早期提供的线程安全的动态数组,若是不须要线程安全,并不建议选择,毕竟同步是有额外开销的。 Vector内部是使用对象数组来保存数据,能够根据须要自动的增长
容量,当数组已满时,会建立新的数组,并拷贝原有数组数据。
ArrayList是应用更加普遍的动态数组实现,它自己不是线程安全的,因此性能要好不少。与Vector近似, ArrayList也是能够根据须要调整容量,不过二者的调整逻辑有所区
别, Vector在扩容时会提升1倍,而ArrayList则是增长50%。
LinkedList顾名思义是Java提供的双向链表,因此它不须要像上面两种那样调整容量,它也不是线程安全的。
咱们能够看到Java的集合框架, Collection接口是全部集合的根,而后扩展开提供了三大类集合,分别是:
今天介绍的这些集合类,都不是线程安全的,对于java.util.concurrent里面的线程安全容器,我在专栏后面会去介绍。可是,并不表明这些集合彻底不能支持并发编程的场景,
在Collections工具类中,提供了一系列的synchronized方法,好比
static <T> List<T> synchronizedList(List<T> list)
咱们彻底能够利用相似方法来实现基本的线程安全集合:
List list = Collections.synchronizedList(new ArrayList());
它的实现,基本就是将每一个基本方法,好比get、 set、 add之类,都经过synchronizd添加基本的同步支持,很是简单粗暴,但也很是实用。注意这些方法建立的线程安全集合,都
符合迭代时fail-fast行为,当发生意外的并发修改时,尽早抛出ConcurrentModifcationException异常,以免不可预计的行为。
另一个常常会被考察到的问题,就是理解Java提供的默认排序算法,具体是什么排序方式以及设计思路等。
这个问题自己就是有点陷阱的意味,由于须要区分是Arrays.sort()仍是Collections.sort() (底层是调用Arrays.sort());什么数据类型;多大的数据集(过小的数据集,复杂排
序是不必的, Java会直接进行二分插入排序)等。
对于原始数据类型,目前使用的是所谓双轴快速排序( Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序,你能够阅读源码。
而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序( binarySort)结合的优化排序算法。 TimSort并非Java的首创,简单说它的思路是查找
数据集中已经排好序的分区(这里叫run),而后合并这些分区来达到排序的目的。
另外, Java 8引入了并行排序算法(直接使用parallelSort方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于fork-join框架,当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;可是,当数据集增加到数万或百万以上时,提升就很是大了,具体仍是取决于处理器和系统环境。
典型回答
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来决定,或者根据键的天然顺序来判断。
LinkedHashMap一般提供的是遍历顺序符合插入顺序,它的实现是经过为条目(键值对)维护一个双向链表。注意,经过特定构造函数,咱们能够建立反映访问顺序的实例,所
谓的put、 get、 compute等,都算做“访问”。
对于TreeMap,它的总体顺序是由键的顺序关系决定的,经过Comparator或Comparable(天然顺序)来决定。
HashMap:
而对于负载因子,我建议:
那么,为何HashMap要树化呢?
本质上这是个安全问题。 由于在元素放置过程当中,若是一个对象哈希冲突,都被放置到同一个桶里,则会造成一个链表,咱们知道链表查询是线性的,会严重影响存取的性能。而在现实世界,构造哈希冲突的数据并非很是复杂的事情,恶意代码就能够利用这些数据大量与服务器端交互,致使服务器端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过相似攻击事件。
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),链表就会被改造为树形结构。
典型回答:
Java提供了不一样层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),咱们能够调用Collections工具类提供的包装方法,来获取一个同步的包装容器(如Collections.synchronizedMap),可是它们都是利用很是粗粒度的同步方式,在高并发状况下,性能比较低下。
另外,更加广泛的选择是利用并发包提供的线程安全容器类,它提供了:
具体保证线程安全的方式,包括有从简单的synchronize方式,到基于更加精细化的,好比基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,
整体来讲,并发包内提供的容器通用场景,远优于早期的简单同步实现。
知识扩展
1.为何须要ConcurrentHashMap?
Hashtable自己比较低效,由于它的实现基本就是将put、 get、 size等各类方法加上“synchronized”。简单来讲,这就致使了全部并发操做都要竞争同一把锁,一个线程在进行同步操做时,其余线程只能等待,大大下降了并发操做的效率。
前面已经提过HashMap不是线程安全的,并发状况会致使相似CPU占用100%等一些问题,那么能不能利用Collections提供的同步包装器来解决问题呢?
看看下面的代码片断,咱们发现同步包装器只是利用输入Map构造了另外一个同步版本,全部操做虽然再也不声明成为synchronized方法,可是仍是利用了“this”做为互斥的mutex,没有真正意义上的改进!
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize // … public int size() { synchronized (mutex) {return m.size();} } // … }
因此, Hashtable或者同步包装版本,都只是适合在非高度并发的场景下。
2.ConcurrentHashMap分析
咱们再来看看ConcurrentHashMap是如何设计实现的,为何它能大大提升并发效率。
首先,我这里强调, ConcurrentHashMap的设计实现其实一直在演化,好比在Java 8中就发生了很是大的变化(Java 7其实也有很多更新),因此,我这里将比较分析结构、实现机制等方面,对比不一样版本的主要区别。
早期ConcurrentHashMap,其实现是基于:
ConcurrentHashMap 1.7中的get操做:get操做须要保证的是可见性,因此并无什么同步逻辑。
get:
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key.hashCode()); //利用位操做替换普通数学运算 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 以Segment为单位,进行定位 // 利用Unsafe直接进行volatile access if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { //省略 } return null; }
put:而对于put操做,首先是经过二次哈希避免哈希冲突,而后以Unsafe调用方式,直接获取相应的Segment,而后进行线程安全的put操做:
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); // 二次哈希,以保证数据的分散性,避免哈希冲突 int hash = hash(key.hashCode()); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }
因此,从上面的源码清晰的看出,在进行并发写操做时:
size:
分段计算两次,两次结果相同则返回,不然对因此段加锁从新计算
在Java 8和以后的版本中, ConcurrentHashMap发生了哪些变化呢?
1.8 中,数据存储内部实现,咱们能够发现Key是final的,由于在生命周期中,一个条目的Key发生变化是不可能的;与此同时val,则声明为volatile,以保证可见性。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; // … }
put:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; K fk; V fv; if (tab == null || (n = tab.length) == 0) tab = initTable(); //初始化 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 利用CAS去进行无锁线程安全操做,若是bin是空的 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else if (onlyIfAbsent // 不加锁,进行检查 && fh == hash && ((fk = f.key) == key || (fk != null && key.equals(fk))) && (fv = f.val) != null) return fv; else { V oldVal = null; synchronized (f) { // 细粒度的同步修改操做... } } // Bin超过阈值,进行树化 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
put CAS 加锁
1.8中不依赖与segment加锁, segment数量与桶数量一致;
首先判断容器是否为空,为空则进行初始化利用volatile的sizeCtl做为互斥手段,若是发现竞争性的初始化,就暂停在那里,等待条件恢复,不然利用CAS设置排他标志(U.compareAndSwapInt(this, SIZECTL, sc, -1)) ;不然重试
对key hash计算获得该key存放的桶位置(再也不是segement),判断该桶是否为空,为空则利用CAS设置新节点
不然使用synchronize加锁,遍历桶中数据,替换或新增长点到桶中
最后判断是否须要转为红黑树,转换以前判断是否须要扩容
size
利用LongAdder累加计算(性能还要高于直接使用AtomicLong)
典型回答
Java IO方式有不少种,基于不一样的IO抽象模型和交互方式,能够进行简单区分。
首先,传统的java.io包,它基于流模型实现,提供了咱们最熟知的一些IO功能,好比File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动做完成以前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。java.io包的好处是代码比较简单、直观,缺点则是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。
不少时候,人们也把java.net下面提供的部分网络API,好比Socket、 ServerSocket、 HttpURLConnection也归类到同步阻塞IO类库,由于网络通讯一样是IO行为。
第二,在Java 1.4中引入了NIO框架(java.nio包),提供了Channel、 Selector、 Bufer等新的抽象,能够构建多路复用的、同步非阻塞IO程序,同时提供了更接近操做系统底层的高性能数据操做方式。
第三,在Java 7中, NIO有了进一步的改进,也就是NIO 2,引入了异步非阻塞IO方式,也有不少人叫它AIO(Asynchronous IO)。异步IO操做基于事件和回调机制,能够简单理解为,应用操做直接返回,而不会阻塞在那里,当后台处理完成,操做系统会通知相应线程进行后续工做。
知识扩展
首先,须要澄清一些基本概念:
区分同步或异步(synchronous/asynchronous)。简单来讲,同步是一种可靠的有序运行机制,当咱们进行同步操做时,后续的任务是等待当前调用返回,才会进行下一步;
而异步则相反,其余任务不须要等待当前调用返回,一般依靠事件、回调等机制来实现任务间次序关系。
区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操做时,当前线程会处于阻塞状态,没法从事其余任务,只有当条件就绪才能继续,好比ServerSocket新链接创建完毕,或数据读取、写入操做完成;而非阻塞则是无论IO操做是否结束,直接返回,相应操做在后台继续处理。
1.Java NIO概览
首先,熟悉一下NIO的主要组成部分:
Buffer,高效的数据容器,除了布尔类型,全部原始数据类型都有相应的Buffer实现。
Channel,相似在Linux之类操做系统上看到的文件描述符,是NIO中被用来支持批量式IO操做的一种抽象。
File或者Socket,一般被认为是比较高层次的抽象,而Channel则是更加操做系统底层的一种抽象,这也使得NIO得以充分利用现代操做系统底层机制,得到特定场景的性能优化,例如, DMA(Direct Memory Access)等。不一样层次的抽象是相互关联的,咱们能够经过Socket获取Channel,反之亦然。
Selector,是NIO实现多路复用的基础,它提供了一种高效的机制,能够检测到注册在Selector上的多个Channel中,是否有Channel处于就绪状态,进而实现了单线程对多Channel的高效管理。Selector一样是基于底层操做系统机制,不一样模式、不一样版本都存在区别。
Chartset,提供Unicode字符串定义, NIO也提供了相应的编解码器等,例如,经过下面的方式进行字符串到ByteBufer的转换:
Charset.defaultCharset().encode("Hello world!"));
BIO NIO 代码略。
在Java 7引入的NIO 2中,又增添了一种额外的异步IO模式,利用事件和回调,处理Accept、 Read等操做。 AIO实现看起来是相似这样子:
AsynchronousServerSocketChannel serverSock =AsynchronousServerSocketChannel.open().bind(sockAddr); serverSock.accept(serverSock, new CompletionHandler<>() { //为异步操做指定CompletionHandler回调函数 @Override public void completed(AsynchronousSocketChannel sockChannel,AsynchronousServerSocketChannel serverSock) { serverSock.accept(serverSock, this); // 另一个 write(sock, CompletionHandler{}) sayHelloWorld(sockChannel, Charset.defaultCharset().encode("Hello World!")); } // 省略其余路径处理方法... });
小结:
典型回答
Java有多种比较典型的文件拷贝实现方式,好比:
利用java.io类库,直接为源文件构建一个FileInputStream读取,而后再为目标文件构建一个FileOutputStream,完成写入工做。
或者,利用java.nio类库提供的transferTo或transferFrom方法实现。
固然, Java标准类库自己已经提供了几种Files.copy的实现。
对于Copy的效率,这个其实与操做系统和配置等状况相关,整体上来讲, NIO transferTo/From的方式可能更快,由于它更能利用现代操做系统底层机制,避免没必要要拷贝和上下
文切换。
典型回答
接口和抽象类是Java面向对象设计的两个基础机制。
接口是对行为的抽象,它是抽象方法的集合,利用接口能够达到API定义和实现分离的目的。接口,不能实例化;不能包含任何很是量成员,任何feld都是隐含着public static
final的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。 Java标准类库中,定义了很是多的接口,好比java.util.List。
抽象类是不能实例化的类,用abstract关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和通常的Java类并无太大区别,能够有一个或者多个抽象方法,也可
以没有抽象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,而后经过继承的方式达到代码复用的目的。 Java标准库中,好比collection框架,不少通用
部分就被抽取成为抽象类,例如java.util.AbstractList。
设想,为接口添加任何抽象方法,相应的全部实现了这个接口的类,也必须实现新增方法,不然会出现编译错误。对于抽象类,若是咱们添加非抽象方法,其子类只会享受到能力扩展,而不用担忧编译出问题.
接口的职责也不只仅限于抽象方法的集合,其实有各类不一样的实践。有一类没有任何方法的接口,一般叫做Marker Interface,顾名思义,它的目的就是为了声明某些东西,好比我
们熟知的Cloneable、 Serializable等。这种用法,也存在于业界其余的Java产品代码中。
典型回答
大体按照模式的应用目标分类,设计模式能够分为建立型模式、结构型模式和行为型模式。
一块儿来简要看看主流开源框架,如Spring等如何在API设计中使用设计模式。你至少要有个大致的印象,如:
典型回答
synchronized是Java内建的同步机制,因此也有人称其为Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其余试图获取的线程只能等待或者阻
塞在那里。
在Java 5之前, synchronized是仅有的同步手段,在代码中, synchronized能够用来修饰方法,也能够使用在特定的代码块儿上,本质上synchronized方法等同于把方法所有语
句用synchronized块包起来。
ReentrantLock,一般翻译为再入锁,是Java 5提供的锁实现,它的语义和synchronized基本相同。再入锁经过代码直接调用lock()方法获取,代码书写也更加灵活。与此同
时, ReentrantLock提供了不少实用的方法,可以实现不少synchronized没法作到的细节控制,好比能够控制fairness,也就是公平性,或者利用定义条件等。可是,编码中也需
要注意,必需要明确调用unlock()方法释放,否则就会一直持有该锁。
synchronized和ReentrantLock的性能不能一律而论,早期版本synchronized在不少场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优
于ReentrantLock。
线程安全须要保证几个基本特性:
ReentrantLock。你可能好奇什么是再入?它是表示当一个线程试图获取一个它已经获取的锁时,这个获取动做就自动成功,这是对锁获取粒度的一个概念,也就是锁的持
有是以线程为单位而不是基于调用次数。 Java锁实现强调再入性是为了和thread的行为进行区分。
ReentrantLock相比synchronized,由于能够像普通对象同样使用,因此能够利用其提供的各类便利方法,进行精细的同步操做,甚至是实现synchronized难以表达的用例,如:
带超时的获取锁尝试。
能够判断是否有线程,或者某个特定线程,在排队等待获取锁。
能够响应中断请求。
…
这里我特别想强调条件变量( java.util.concurrent.Condition),若是说ReentrantLock是synchronized的替代选择, Condition则是将wait、 notify、 notifyAll等操做转化为相
应的对象,将复杂而晦涩的同步操做转变为直观可控的对象行为。
条件变量最为典型的应用场景就是标准类库中的ArrayBlockingQueue等。
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,而后试图进行降
级。
Java核心类库中还有其余一些特别的锁类型,具体请参考下面的图。
这些锁居然不都是实现了Lock接口, ReadWriteLock是一个单独的接口,它一般是表明了一对儿锁,分别对应只读和写操做,标准类库中提供了再入版本的读写
锁实现( ReentrantReadWriteLock),对应的语义和ReentrantLock比较类似。
StampedLock居然也是个单独的类型,从类图结构能够看出它是不支持再入性的语义的,也就是它不是以持有锁的线程为单位。
为何咱们须要读写锁( ReadWriteLock)等其余锁呢?
这是由于,虽然ReentrantLock和synchronized简单实用,可是行为上有必定局限性,通俗点说就是“太霸道”,要么不占,要么独占。实际应用场景中,有的时候不须要大量竞争
的写操做,而是以并发读取为主,如何进一步优化并发操做的粒度呢?
Java并发包提供的读写锁等扩展了锁的能力,它所基于的原理是多个读操做是不须要互斥的,由于读操做并不会更改数据,因此不存在互相干扰。而写操做则会致使并发一致性的问
题,因此写线程之间、读写线程之间,须要精心设计的互斥逻辑。
典型回答
Java的线程是不容许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,屡次调用start被认为是编程错误。
关于线程生命周期的不一样状态,在Java 5之后,线程状态被明肯定义在其公共内部枚举类型java.lang.Thread.State中,分别是:
public fnal native void wait(long timeout) throws InterruptedException;
知识扩展
1.首先,咱们来总体看一下线程是什么?
从操做系统的角度,能够简单认为,线程是系统调度的最小单元,一个进程能够包含多个线程,做为任务的真正运做者,有本身的栈( Stack)、寄存器( Register)、本地存储
( Thread Local)等,可是会和进程内其余线程共享文件描述符、虚拟地址空间等。
2.从线程生命周期的状态开始展开,那么在Java编程中,有哪些因素可能影响线程的状态呢?主要有:
典型回答
死锁是一种特定的程序状态,在实体之间,因为循环依赖致使彼此一直处于等待之中,没有任何个体能够继续前进。死锁不只仅是在线程之间会发生,存在资源独占的进程之间一样
也可能出现死锁。一般来讲,咱们大可能是聚焦在多线程场景中的死锁,指两个或多个线程之间,因为互相持有对方须要的锁,而永久处于阻塞的状态。
定位死锁最多见的方式就是利用jstack等工具获取线程栈,而后定位互相之间的依赖关系,进而找到死锁。若是是比较明显的死锁,每每jstack等就能直接定位,相似JConsole甚至
能够在图形界面进行有限的死锁检测。
如何在编程中尽可能预防死锁呢?
首先,咱们来总结一下前面例子中死锁的产生包含哪些基本元素。基本上死锁的发生是由于:
第一种方法
若是可能的话,尽可能避免使用多个锁,而且只有须要时才持有锁。不然,即便是很是精通并发编程的工程师,也不免会掉进坑里,嵌套的synchronized或者lock很是容易出问题。
第二种方法
若是必须使用多个锁,尽可能设计好锁的获取顺序,这个提及来简单,作起来可不容易,你能够参看著名的银行家算法.
第三种方法
使用带超时的方法,为程序带来更多可控性。
相似Object.wait(…)或者CountDownLatch.await(…),都支持所谓的timed_wait,咱们彻底能够就不假定该锁必定会得到,指定超时时间,并为没法获得锁时准备退出逻辑。
典型回答
咱们一般所说的并发包也就是java.util.concurrent及其子包,集中了Java并发的各类基础工具类,具体主要包括几个方面:
**知识扩展 **
Semaphore
1.工做原理
以一个停车场是运做为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时若是同时来了五辆车,看门人容许其中三辆不受阻碍的进入,而后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,若是又离开两辆,则又能够放入两辆,如此往复。这个停车系统中,每辆车就比如一个线程,看门人就比如一个信号量,看门人限制了能够活动的线程。假如里面依然是三个车位,可是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,可是得保证最多停两辆车。对于Semaphore类而言,就如同一个看门人,限制了可活动的线程数。
2.主要方法
https://www.cnblogs.com/klbc/p/9500947.html
下面,来看看CountDownLatch和CyclicBarrier,它们的行为有必定的类似度,常常会被考察两者有什么区别,我来简单总结一下。
CountDownLatch
模拟五个线程同时启动:
public static void main(String[] args) { //全部线程阻塞,而后统一开始 CountDownLatch begin = new CountDownLatch(1); //主线程阻塞,直到全部分线程执行完毕 CountDownLatch end = new CountDownLatch(5); for(int i = 0; i < 5; i++){ Thread thread = new Thread(new Runnable() { @Override public void run() { try { begin.await(); System.out.println(Thread.currentThread().getName() + " 起跑"); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " 到达终点"); end.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } try { System.out.println("1秒后统一开始"); Thread.sleep(1000); begin.countDown(); end.await(); System.out.println("中止比赛"); } catch (InterruptedException e) { e.printStackTrace(); } }
结果:
1秒后统一开始 Thread-1 起跑 Thread-4 起跑 Thread-3 起跑 Thread-0 起跑 Thread-2 起跑 Thread-3 到达终点 Thread-0 到达终点 Thread-4 到达终点 Thread-1 到达终点 Thread-2 到达终点 中止比赛
并发包里提供的线程安全Map、 List和Set:
若是咱们的应用侧重于Map放入或者获取的速度,而不在意顺序,大多推荐使用ConcurrentHashMap,反之则使
用ConcurrentSkipListMap;若是咱们须要对大量数据进行很是频繁地修改, ConcurrentSkipListMap也可能表现出优点。
SkipList结构:
关于两个CopyOnWrite容器,其实CopyOnWriteArraySet是经过包装了CopyOnWriteArrayList来实现的,因此在学习时,咱们能够专一于理解一种。
首先, CopyOnWrite究竟是什么意思呢?它的原理是,任何修改操做,如add、 set、 remove,都会拷贝原数组,修改后替换原来的数组,经过这种防护性的方式,实现另类的线程安全。
public boolean add(E e) { synchronized (lock) { Object[] elements = getArray(); int len = elements.length; // 拷贝 Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; // 替换 setArray(newElements); return true; } } final void setArray(Object[] a) { array = a; }
典型回答
有时候咱们把并发包下面的全部容器都习惯叫做并发容器,可是严格来说,相似ConcurrentLinkedQueue这种“Concurrent*”容器,才是真正表明并发。
关于问题中它们的区别:
不知道你有没有注意到, java.util.concurrent包提供的容器( Queue、 List、 Set)、 Map,从命名上能够大概区分为Concurrent、 CopyOnWrite和Blocking*等三类,一样是线
程安全容器,能够简单认为:
知识扩展
线程安全队列一览:
ArrayBlockingQueue是最典型的的有界队列,其内部以final的数组保存数据,数组的大小就决定了队列的边界,因此咱们在建立ArrayBlockingQueue时,都要指定容量,如
public ArrayBlockingQueue(int capacity, boolean fair)
LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过若是咱们没有在建立队列时就指定容量,那么其容量限制就自动被
设置为Integer.MAX_VALUE ,成为了无界队列。
SynchronousQueue,这是一个很是奇葩的队列实现,每一个删除操做都要等待插入操做,反之每一个插入操做也都要等待删除动做。那么这个队列的容量是多少呢?是1吗?其实不
是的,其内部容量是0。
PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来说,其大小总归是要受系统资源影响。
DelayedQueue和LinkedTransferQueue一样是无边界的队列。对于无边界的队列,有一个天然的结果,就是put操做永远也不会发生其余BlockingQueue的那种等待状况。
使用Blocking实现的生产者消费者代码:
package com.ryze.chapter3; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ConsumerProducer { public static final String EXIT_MSG = "Good bye!"; public static void main(String[] args) { // 使用较小的队列,以更好地在输出中展现其影响 BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); Producer producer = new Producer(queue); Consumer consumer = new Consumer(queue); new Thread(producer).start(); new Thread(consumer).start(); } static class Producer implements Runnable { private BlockingQueue<String> queue; public Producer(BlockingQueue<String> q) { this.queue = q; } @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(5L); String msg = "Message" + i; System.out.println("Produced new item: " + msg); queue.put(msg); } catch (InterruptedException e) { e.printStackTrace(); } } try { System.out.println("Time to say good bye!"); queue.put(EXIT_MSG); } catch (InterruptedException e) { e.printStackTrace(); } } } static class Consumer implements Runnable { private BlockingQueue<String> queue; public Consumer(BlockingQueue<String> q) { this.queue = q; } @Override public void run() { try { String msg; while (!EXIT_MSG.equalsIgnoreCase((msg = queue.take()))) { System.out.println("Consumed item: " + msg); Thread.sleep(10L); } System.out.println("Got exit message, bye!"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
####前面介绍了各类队列实现,在平常的应用开发中,如何进行选择呢?
以LinkedBlockingQueue、 ArrayBlockingQueue和SynchronousQueue为例,咱们一块儿来分析一下,根据需求能够从不少方面考量:
考虑应用场景中对队列边界的要求。 ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于咱们是否在建立时指定, SynchronousQueue则干脆不
能缓存任何元素。
从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,由于其不须要建立所谓节点,可是其初始分配阶段就须要一段连续的空间,因此初始内存
需求更大。
通用场景中, LinkedBlockingQueue的吞吐量通常优于ArrayBlockingQueue,由于它实现了更加细粒度的锁操做。
ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。
若是咱们须要实现的是两个线程之间接力性( handof)的场景,按照专栏上一讲的例子,你可能会选择CountDownLatch,可是SynchronousQueue也是完美符合这种场景
的,并且线程间协调和数据传输统一块儿来,代码更加规范。
可能使人意外的是,不少时候SynchronousQueue的性能表现,每每大大超过其余实现,尤为是在队列元素较小的场景。
典型回答
一般开发者都是利用Executors提供的通用线程池建立方法,去建立不一样配置的线程池,主要区别在于不一样的ExecutorService类型或者不一样的初始参数。
Executors目前提供了5种不一样的线程池建立配置:
newCachedThreadPool(),它是一种用来处理大量短期工做任务的线程池,具备几个鲜明特色:它会试图缓存线程并重用,当无缓存线程可用时,就会建立新的工做线程;如
果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue做为工做队列。
newFixedThreadPool(int nThreads),重用指定数目( nThreads)的线程,其背后使用的是无界的工做队列,任什么时候候最多有nThreads个工做线程是活动的。这意味着,如
果任务数量超过了活动队列数目,将在工做队列中等待空闲线程出现;若是有工做线程退出,将会有新的工做线程被建立,以补足指定的数目nThreads。
newSingleThreadExecutor(),它的特色在于工做线程数目被限制为1,操做一个无界的工做队列,因此它保证了全部任务的都是被顺序执行,最多会有一个任务处于活动状
态,而且不容许使用者改动线程池实例,所以能够避免其改变线程数目。
newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),建立的是个ScheduledExecutorService,能够进行定时或周期性的工做调度,
区别在于单一工做线程仍是多个工做线程。
ewWorkStealingPool(int parallelism),这是一个常常被人忽略的线程池, Java 8才加入这个建立方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处
理任务,不保证处理顺序。
Executor框架可不只仅是线程池,我以为至少下面几点值得深刻学习:
知识扩展
首先,咱们来看看Executor框架的基本组成,请参考下面的类图。
Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点能够体会其定义的惟一方法。
void execute(Runnable command);
ExecutorService则更加完善,不只提供service的管理功能,好比shutdown等方法,也提供了更加全面的提交任务机制,如返回Future而不是void的submit方法。
<T> Future<T> submit(Callable<T> task);
从源码角度,分析线程池的设计与实现,我将主要围绕最基础的ThreadPoolExecutor源码:
简单理解一下:
工做队列负责存储用户提交的各个任务,这个工做队列,能够是容量为0的SynchronousQueue(使用newCachedThreadPool),也能够是像固定大小线程池
( newFixedThreadPool)那样使用LinkedBlockingQueue。
private final BlockingQueue<Runnable> workQueue;
内部的“线程池”,这是指保持工做线程的集合,线程池须要在运行过程当中管理线程建立、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会建立新的工做线程;当
业务压力退去,线程池会在闲置一段时间(默认60秒)后结束线程。
private final HashSet<Worker> workers = new HashSet<>();
线程池的工做线程被抽象为静态内部类Worker,基于AQS实现。
从上面的分析,就能够看出线程池的几个基本组成部分,一块儿都体如今线程池的构造函数中,从字面咱们就能够大概猜想到其用意:
corePoolSize,所谓的核心线程数,能够大体理解为长期驻留的线程数目(除非设置了allowCoreThreadTimeOut)。对于不一样的线程池,这个值可能会有很大区别,比
如newFixedThreadPool会将其设置为nThreads,而对于newCachedThreadPool则是为0。
maximumPoolSize,顾名思义,就是线程不够时可以建立的最大线程数。一样进行对比,对于newFixedThreadPool,固然就是nThreads,由于其要求是固定大小,
而newCachedThreadPool则是Integer.MAX_VALUE 。
keepAliveTime和TimeUnit,这两个参数指定了额外的线程可以闲置多久,显然有些线程池不须要它。
workQueue,工做队列,必须是BlockingQueue。
经过配置不一样的参数,咱们就能够建立出行为截然不同的线程池,这就是线程池高度灵活性的基础
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
典型回答
AtomicIntger是对int类型的一个封装,提供原子性的访问和更新操做,其原子性操做的实现是基于CAS( compare-and-swap).
目前Java提供了两种公共API,能够实现这种CAS操做,好比使用java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基于反射机制建立,咱们须要保证类型和字段名称正确。
AQS内部数据和方法,能够简单拆分为:
private volatile int sate;
利用AQS实现一个同步结构,至少要实现两个基本类型的方法,分别是acquire操做,获取资源的独占权;还有就是release操做,释放对某个资源的独占
排除掉一些细节,总体地分析acquire方法逻辑,其直接实现是在AQS内部,调用了tryAcquire和acquireQueued,这是两个须要搞清楚的基本部分。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
以非公平的tryAcquire为例,其内部实现了如何配合状态与CAS获取锁,注意,对比公平版本的tryAcquire,它在锁无人占有时,并不检查是否有其余等待者,这里体现了非公平的
语义。
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();// 获取当前AQS内部状态量 if (c == 0) { // 0表示无人占有,则直接用CAS修改状态位, if (compareAndSetState(0, acquires)) {// 不检查排队状况,直接争抢 setExclusiveOwnerThread(current); //并设置当前线程独占锁 return true; } } else if (current == getExclusiveOwnerThread()) { //即便状态不是0,也可能当前线程是锁持有者,由于这是再入锁 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
再来分析acquireQueued,若是前面的tryAcquire失败,表明着锁争抢失败,进入排队竞争阶段。这里就是咱们所说的,利用FIFO队列,实现线程间对锁的竞争的部分,
算是是AQS的核心逻辑。
当前线程会被包装成为一个排他模式的节点( EXCLUSIVE),经过addWaiter方法添加到队列中。 acquireQueued的逻辑,简要来讲,就是若是当前节点的前面是头节点,则试图
获取锁,一切顺利则成为新的头节点;不然,有必要则等待,具体处理逻辑请参考我添加的注释。
final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; try { for (;;) {// 循环 final Node p = node.predecessor();// 获取前一个节点 if (p == head && tryAcquire(arg)) { // 若是前一个节点是头结点,表示当前节点合适去tryAcquire setHead(node); // acquire成功,则设置新的头节点 p.next = null; // 将前面节点对当前节点的引用清空 return interrupted; } if (shouldParkAfterFailedAcquire(p, node)) // 检查是否失败后须要park interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { cancelAcquire(node);// 出现异常,取消 if (interrupted) selfInterrupt(); throw t; } }
到这里线程试图获取锁的过程基本展示出来了, tryAcquire是按照特定场景须要开发者去实现的部分,而线程间竞争则是AQS经过Waiter队列与acquireQueued提供的,
在release方法中,一样会对队列进行对应操做.
典型回答
通常来讲,咱们把Java的类加载过程分为三个主要步骤:加载、连接、初始化,具体行为在Java虚拟机规范里有很是详细的定义。
首先是加载阶段( Loading),它是Java将字节码数据从不一样的数据源读取到JVM中,并映射为JVM承认的数据结构( Class对象),这里的数据源多是各类各样的形态,如jar文
件、 class文件,甚至是网络数据源等;若是输入数据不是ClassFile的结构,则会抛出ClassFormatError。
加载阶段是用户参与的阶段,咱们能够自定义类加载器,去实现本身的类加载过程。
第二阶段是连接( Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM运行的过程当中。这里可进一步细分为三个步骤:
验证( Verifcation),这是虚拟机安全的重要保障, JVM须要核验字节信息是符合Java虚拟机规范的,不然就被认为是VerifyError,这样就防止了恶意信息或者不合规的信息危
害JVM的运行,验证阶段有可能触发更多class的加载。
准备( Preparation),建立类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所须要的内存空间,
不会去执行更进一步的JVM指令。
解析( Resolution),在这一步会将常量池中的符号引用( symbolic reference)替换为直接引用。在Java虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解
析。
最后是初始化阶段( initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动做,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这
部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈双亲委派模型,简单说就是当类加载器( Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,不然尽可能将这个任务代理给当前加载器的父加载器去
作。使用委派模型的目的是避免重复加载Java类型。
一般类加载机制有三个基本特征:
双亲委派模型。但不是全部类加载都遵照这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,好比JDK内部的ServiceProvider/ServiceLoader机制,
用户能够在标准API框架上,提供本身的实现, JDK也须要提供些默认的参考实现。 例如, Java 中JNDI、 JDBC、文件系统、 Cipher等不少方面,都是利用的这种机制,这种情
况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。
可见性,子类加载器能够访问父加载器加载的类型,可是反过来是不容许的,否则,由于缺乏必要的隔离,咱们就没有办法利用类加载器去实现容器的逻辑。
单一性,因为父加载器的类型对于子加载器是可见的,因此父加载器中加载过的类型,就不会在子加载器中重复加载。可是注意,类加载器“邻居”间,同一类型仍然能够被加载多
次,由于互相并不可见。
典型回答
一般能够把JVM内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个JVM进程惟一的。
首先, 程序计数器( PC, Program Counter Register)。在JVM规范中,每一个线程都有它本身的程序计数器,而且任什么时候间一个线程都只有一个方法在执行,也就是所谓的当前方
法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,若是是在执行本地方法,则是未指定值( undefned)。
第二, Java虚拟机栈( Java Virtual Machine Stack),早期也叫Java栈。每一个线程在建立时都会建立一个虚拟机栈,其内部保存一个个的栈帧( Stack Frame),对应着一次次
的Java方法调用。
前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,一般叫做当前帧,方法所在的类叫做当前类。若是在该方法中调用了其余方法,对应
的新的栈帧会被建立出来,成为新的当前帧,一直到它返回结果或者执行结束。 JVM直接对Java栈的操做只有两个,就是对栈帧的压栈和出栈。
栈帧中存储着局部变量表、操做数( operand)栈、动态连接、方法正常退出或者异常退出的定义等。
第三, 堆( Heap),它是Java内存管理的核心区域,用来放置Java对象实例,几乎全部建立的Java对象实例都是被直接分配在堆上。堆被全部的线程共享,在虚拟机启动时,咱们
指定的“Xmx”之类参数就是用来指定最大堆空间等指标。
理所固然,堆也是垃圾收集器重点照顾的区域,因此堆内空间还会被不一样的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
第四, 方法区( Method Area)。这也是全部线程共享的一块内存区域,用于存储所谓的元( Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
因为早期的Hotspot JVM实现,不少人习惯于将方法区称为永久代( Permanent Generation)。 Oracle JDK 8中将永久代移除,同时增长了元数据区( Metaspace)。
第五, 运行时常量池( Run-Time Constant Pool),这是方法区的一部分。若是仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各类信息,还有一
项信息就是常量池。 Java的常量池能够存放各类常量信息,无论是编译期生成的各类字面量,仍是须要在运行时决定的符号引用,因此它比通常语言的符号表存储的信息更加宽泛。
第六, 本地方法栈( Native Method Stack)。它和Java虚拟机栈是很是类似的,支持对本地方法的调用,也是每一个线程都会建立一个。在Oracle Hotspot JVM中,本地方法栈
和Java虚拟机栈是在同一起区域,这彻底取决于技术实现的决定,并未在规范中强制。
全部的对象实例都是建立在堆上。
除了程序计数器,其余区域都有可能会由于可能的空间不足发生OutOfMemoryError,简单总结以下:
堆内存不足是最多见的OOM缘由之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,缘由可能千奇百怪,例如,可能存在内存泄漏问题;也颇有可
能就是堆的大小不合理,好比咱们要处理比较可观的数据量,可是没有显式指定JVM堆大小或者指定数值偏小;或者出现JVM处理引用不及时,致使堆积起来,内存没法释放等。
而对于Java虚拟机栈和本地方法栈,这里要稍微复杂一点。若是咱们写一段程序不断的进行递归调用,并且没有退出条件,就会致使不断地进行压栈。相似这种状况, JVM实际会
抛出StackOverFlowError;固然,若是JVM试图去扩展栈空间的的时候失败,则会抛出OutOfMemoryError。
对于老版本的Oracle JDK,由于永久代的大小是有限的,而且JVM对永久代垃圾回收(如,常量池回收、卸载再也不须要的类型)很是不积极,因此当咱们不断添加新类型的时
候,永久代出现OutOfMemoryError也很是多见,尤为是在运行时存在大量动态类型生成的场合;相似Intern字符串缓存占用太多空间,也会致使OOM问题。对应的异常信息,
会标记出来和永久代相关: “java.lang.OutOfMemoryError: PermGen space”。
随着元数据区的引入,方法区内存已经再也不那么窘迫,因此相应的OOM有所改观,出现OOM,异常信息则变成了: “java.lang.OutOfMemoryError: Metaspace”。
直接内存不足,也会致使OOM.
思考
我在试图分配一个100M bytes大数组的时候发生了OOME,可是GC日志显示,明明堆上还有远不止100M的空间,你以为可能问题的缘由是什么?想要弄清楚这个问题,还须要什么信息呢?
思路1:
若是仅从jvm的角度来看,要看下新生代和老年代的垃圾回收机制是什么。若是新生代是serial,会默认使用copying算法,利用两块eden和survivor来进行处理。可是默认当遇到超大对象
时,会直接将超大对象放置到老年代中,而不用走正常对象的存活次数记录。由于要放置的是一个byte数组,那么必然须要申请连续的空间,当空间不足时,会进行gc操做。这里又须要看老年
代的gc机制是哪种。若是是serial old,那么会采用mark compat,会进行整理,从而整理出连续空间,若是还不够,说明是老年代的空间不够,所谓的堆内存大于100m是新+老共同的结
果。若是采用的是cms(concurrent mark sweep),那么只会标记清理,并不会压缩,因此内存会碎片化,同时可能出现浮游垃圾。若是是cms的话,即便老年代的空间大于100m,也会出现
没有连续的空间供该对象使用。
思路2:
从不一样的垃圾收集器角度来看:
首先,数组的分配是须要连续的内存空间的(听说,有个别非主流JVM支持大数组用不连续的内存空间分配��)。因此:
1)对于使用年轻代和老年代来管理内存的垃圾收集器,堆大于 100M,表示的是新生代和老年代加起来总和大于100M,而新生代和老年代各自并无大于 100M 的连续内存空间。
进一步,又因为大数组通常直接进入老年代(会跳过对对象的年龄的判断),因此,是否能够认为老年代中没有连续大于 100M 的空间呢。
2)对于 G1 这种按 region 来管理内存的垃圾收集器,可能的状况是没有多个连续的 region,它们的内存总和大于 100M。
固然,无论是哪一种垃圾收集器以及收集算法,当内存空间不足时,都会触发 GC,只不过,可能 GC 以后,仍是没有连续大于 100M 的内存空间,因而 OOM了。
典型回答
了解JVM内存的方法有不少,具体能力范围也有区别,简单总结以下:
能够使用综合性的图形化工具,如JConsole、 VisualVM(注意,从Oracle JDK 9开始, VisualVM已经再也不包含在JDK安装包中)等。这些工具具体使用起来相对比较直观,直
接链接到Java进程,而后就能够在图形化界面里掌握内存使用状况。
以JConsole为例,其内存页面能够显示常见的堆内存和各类堆外部分使用状态。
也能够使用命令行工具进行运行时查询,如jstat和jmap等工具都提供了一些选项,能够查看堆、方法区等使用数据。
或者,也能够使用jmap等提供的命令,生成堆转储( Heap Dump)文件,而后利用jhat或Eclipse MAT等堆转储分析工具进行详细分析。
若是你使用的是Tomcat、 Weblogic等Java EE服务器,这些服务器一样提供了内存管理相关的功能。
另外,从某种程度上来讲, GC日志等输出,一样包含着丰富的信息。
首先,堆内部是什么结构?
对于堆内存,我在上一讲介绍了最多见的新生代和老年代的划分,其内部结构随着JVM的发展和新GC方式的引入,能够有不一样角度的理解,下图就是年代视角的堆结构示意图。
你能够看到,按照一般的GC年代方式划分, Java堆内分为:
1.新生代
新生代是大部分对象建立和销毁的区域,在一般的Java应用中,绝大部分对象生命周期都是很短暂的。其内部又分为Eden区域,做为对象初始分配的区域;两个Survivor,有时候
也叫from、 to区域,被用来放置从Minor GC中保留下来的对象。
JVM会随意选取一个Survivor区域做为“to”,而后会在GC过程当中进行区域间拷贝,也就是将Eden中存活下来的对象和from区域的对象,拷贝到这个“to”区域。这种设计主要是为
了防止内存的碎片化,并进一步清理无用对象。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分, Hotspot JVM还有一个概念叫作Thread Local Allocation Bufer( TLAB),据我所知全部OpenJDK衍生出来
的JVM都提供了TLAB的设计。这是JVM为每一个线程分配的一个私有缓存区域,不然,多线程同时分配内存时,为避免操做同一地址,可能须要使用加锁等机制,进而影响分配速
度,你能够参考下面的示意图。从图中能够看出, TLAB仍然在堆上,它是分配在Eden区域内的。其内部结构比较直观易懂, start、 end就是起始地址, top(指针)则表示已经
分配到哪里了。因此咱们分配新对象, JVM就会移动top,当top和end相遇时,即表示该缓存已满, JVM会试图再从Eden里分配一起。
2.老年代
放置长生命周期的对象,一般都是从Survivor区域拷贝过来的对象。固然,也有特殊状况,咱们知道普通的对象会被分配在TLAB上;若是对象较大, JVM会试图直接分配在Eden其
他位置上;若是对象太大,彻底没法在新生代找到足够长的连续空闲空间, JVM就会直接分配到老年代。
3.永久代
这部分就是早期Hotspot JVM的方法区实现方式了,储存Java类元数据、常量池、 Intern字符串缓存,在JDK 8以后就不存在永久代这块儿了。
那么,咱们如何利用JVM参数,直接影响堆和内部区域的大小呢?我来简单总结一下:
-Xmx value
-Xms value
-XX:NewRatio=value
默认状况下,这个数值是3,意味着老年代是新生代的3倍大;换句话说,新生代是堆大小的1/4。
固然,也能够不用比例的方式调整新生代的大小,直接指定下面的参数,设定具体的内存大小数值.
-XX:NewSize=value
Eden和Survivor的大小是按照比例设置的,若是SurvivorRatio是8,那么Survivor区域就是Eden的1/8大小,也就是新生代的1/10,由于YoungGen=Eden +2*Survivor
JVM参数格式是
-XX:SurvivorRatio=value
思考:
若是用程序的方式而不是工具,对Java内存使用进行监控,有哪些技术能够作到?
利用JMX MXbean公开出来的api:ManagementFactory;
典型回答:
实际上,垃圾收集器( GC, Garbage Collector)是和具体JVM实现紧密相关的,不一样厂商( IBM、 Oracle),不一样版本的JVM,提供的选择也不一样。接下来,我来谈谈最主流
的Oracle JDK。
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之间是复制算法,但总体上实际可看做是标记-整理( MarkCompact)算法,能够有效地避免内存碎片,尤为是当Java堆很是大的时候, G1的优点更加明显。
G1吞吐量和停顿表现都很是不错,而且仍然在不断地完善,与此同时CMS已经在JDK 9中被标记为废弃( deprecated),因此G1 GC值得你深刻掌握。
常见的垃圾收集算法,我认为整体上有个了解,理解相应的原理和优缺点,就已经足够了,其主要分为三类:
复制( Copying)算法,我前面讲到的新生代GC,基本都是基于复制算法,过程就如专栏上一讲所介绍的,将活着的对象复制到to区域,拷贝过程当中将对象顺序放置,就能够避
免内存碎片化。
这么作的代价是,既然要进行复制,既要提早预留内存空间,有必定的浪费;另外,对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC须要维护region之间对
象引用关系,这个开销也不小,无论是内存占用或者时间开销。
标记-清除( Mark-Sweep)算法,首先进行标记工做,标识出全部要回收的对象,而后进行清除。这么作除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,
这就致使其不适合特别大的堆;不然,一旦出现Full GC,暂停时间可能根本没法接受。
标记-整理( Mark-Compact),相似于标记-清除,但为避免内存碎片化,它会在清理过程当中将对象移动,以确保移动后的对象占用连续的内存空间。
在垃圾收集的过程,对应到Eden、 Survivor、 Tenured等区域会发生什么变化呢?
这实际上取决于具体的GC方式,先来熟悉一下一般的垃圾收集流程,我画了一系列示意图,但愿能有助于你理解清楚这个过程。
第一, Java应用不断建立对象,一般都是分配在Eden区域,当其空间占用达到必定阈值时,触发minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到JVM选择
的Survivor区域,而没有被引用的对象(黄色方块)则被回收。注意,我给存活对象标记了“数字1”,这是为了代表对象的存活时间。
第二, 通过一次Minor GC, Eden就会空闲下来,直到再次达到Minor GC触发条件,这时候,另一个Survivor区域则会成为to区域, Eden区域的存活对象和From区域对象,都
会被复制到to区域,而且存活的年龄计数会被加1。
第三, 相似第二步的过程会发生不少次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升( Promotion)过程,以下图所示,超过阈值的对象会被晋升到老年代。这个阈
值是能够经过参数指定:
-XX:MaxTenuringThreshold=<N>
后面就是老年代GC,具体取决于选择的GC选项,对应不一样的算法。下面是一个简单标记-整理算法过程示意图,老年代中的无用对象被清除后, GC会将对象进行整理,以防止内存
碎片化。
一般咱们把老年代GC叫做Major GC,将对整个堆进行的清理叫做Full GC,可是这个也没有那么绝对,由于不一样的老年代GC算法其实表现差别很大,例如CMS, “concurrent”就
体如今清理工做是与工做线程一块儿并发运行的.
总结:
典型回答
Happen-before关系,是Java内存模型中保证多线程操做可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精肯定义。
它的具体表现形式,包括但远不止是咱们直觉中的synchronized、 volatile、 lock操做顺序等方面,例如:
JMM内部的实现一般是依赖于所谓的内存屏障,经过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各类happen-before规则。与此同时,更多复杂度在于,须要尽可能
确保各类编译器、各类体系结构的处理器,都可以提供一致的行为。
以volatile为例,看看如何利用内存屏障实现JMM定义的可见性?
对于一个volatile变量:
内存屏障可以在相似变量读、写操做以后,保证其余线程对volatile变量的修改对当前线程可见,或者本地修改对其余线程提供可见性。换句话说,线程写入,写屏障会经过相似强迫
刷出处理器缓存的方式,让其余线程可以拿到最新数值。
典型回答
对于Java来讲, Docker毕竟是一个较新的环境,例如,其内存、 CPU等资源限制是经过CGroup( Control Group)实现的,早期的JDK版本( 8u131以前)并不能识别这些限
制,进而会致使一些基础问题:
若是未配置合适的JVM堆和元数据区、直接内存等参数, Java就有可能试图使用超过容器限制的内存,最终被容器OOM kill,或者自身发生OOM。
错误判断了可获取的CPU资源,例如, Docker限制了CPU的核数, JVM就可能设置不合适的GC并行线程数等
知识扩展
首先,咱们先来搞清楚Java在容器环境的局限性来源, Docker到底有什么特别?
虽然看起来Docker之类容器和虚拟机很是类似,例如,它也有本身的shell,能独立安装软件包,运行时与其余容器互不干扰。可是,若是深刻分析你会发现, Docker并非一种完
全的虚拟化技术,而更是一种轻量级的隔离技术。
对于Java平台来讲,这些未隐藏的底层信息带来了不少意外的困难,主要体如今几个方面:
第一,容器环境对于计算资源的管理方式是全新的, CGroup做为相对比较新的技术,历史版本的Java显然并不能天然地理解相应的资源限制。
第二, namespace对于容器内的应用细节增长了一些微妙的差别,好比jcmd、 jstack等工具会依赖于“/proc//”下面提供的部分信息,可是Docker的设计改变了这部分信息的原有
结构,咱们须要对原有工具进行修改以适应这种变化。
从JVM运行机制的角度,为何这些“沟通障碍”会致使OOM等问题呢?
你能够思考一下,这个问题实际是反映了JVM如何根据系统资源(内存、 CPU等)状况,在启动时设置默认参数
这就是所谓的Ergonomics机制,例如:
这些默认参数,是根据通用场景选择的初始值。可是因为容器环境的差别, Java的判断极可能是基于错误信息而作出的。
这就相似,我觉得我住的是整栋别墅,实际上却只有一个房间是给我住的。
更加严重的是, JVM的一些原有诊断或备用机制也会受到影响。为保证服务的可用性,一种常见的选择是依赖“-XX:OnOutOfMemoryError”功能,经过调用处理脚本的形式来作一
些补救措施,好比自动重启服务等。可是,这种机制是基于fork实现的,当Java进程已通过度提交内存时, fork新的进程每每已经不可能正常运行了。
根据前面的总结,彷佛问题很是棘手,那咱们在实践中, 如何解决这些问题呢?
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
若是你能够切换到JDK 10或者更新的版本,问题就更加简单了。 Java对容器( Docker)的支持已经比较完善,默认就会自适应各类资源限制和实现差别。前面提到的实验性参
数“UseCGroupMemoryLimitForHeap”已经被标记为废弃。
与此同时,新增了参数用以明确指定CPU核心的数目。
-XX:ActiveProcessorCount=N
可是,若是我暂时只能使用老版本的JDK怎么办?
这里有几个建议:
明确设置堆、元数据区等内存区域大小,保证Java进程的总大小可控。
例如,咱们可能在环境中,这样限制容器内存:
$ docker run -it --rm --name yourcontainer -p 8080:8080 -m 800M repo/your-java-container:openjdk
那么,就能够额外配置下面的环境变量,直接指定JVM堆大小。
-e JAVA_OPTIONS='-Xmx300m'
明确配置GC和JIT并行线程数目,以免两者占用过多计算资源。
-XX:ParallelGCThreads -XX:CICompilerCount
除了我前面介绍的OOM等问题,在不少场景中还发现Java在Docker环境中,彷佛会意外使用Swap。具体缘由待查,但颇有可能也是由于Ergonomics机制失效致使的,我建议配
置下面参数,明确告知JVM系统内存限额。
-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes`
也能够指定Docker运行参数,例如:
--memory-swappiness=0
下面是几种主要的注入式攻击途径,原则上提供动态执行能力的语言特性,都须要提防发生注入攻击的可能。
首先,就是最多见的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 “” = “”
第二,操做系统命令注入。 Java语言提供了相似Runtime.exec(…)的API,能够用来执行特定命令,假设咱们构建了一个应用,以输入文本做为参数,执行下面的命令:
ls –la input_fle_name
可是若是用户输入是 “input_fle_name;rm –rf /*”,这就有可能出现问题了 .
第三, XML注入攻击。 Java核心类库提供了全面的XML处理、转换等各类API,而XML自身是能够包含动态内容的,例如XPATH,若是使用不当,可能致使访问恶意内容。
还有相似LDAP等容许动态内容的协议,都是可能利用特定命令,构造注入式攻击的,包括XSS( Cross-site Scripting)攻击,虽然并不和Java直接相关,但也可能在JSP等动态页
面中发生。
知识扩展
首先,一块儿来看看哪些Java API和工具构成了Java安全基础。不少方面我在专栏前面的讲解中已经有所涉及,能够简单归为三个主要组成部分:
第一,运行时安全机制。能够简单认为,就是限制Java运行时的行为,不要作越权或者不靠谱的事情,具体来看:
在类加载过程当中,进行字节码验证,以防止不合规的代码影响JVM运行或者载入其余恶意代码。
类加载器自己也能够对代码之间进行隔离,例如,应用没法获取启动类加载器( Bootstrap Class-Loader)对象实例,不一样的类加载器也能够起到容器的做用,隔离模块之间不
必要的可见性等。目前, Java Applet、 RMI等特性已经或逐渐退出历史舞台,类加载等机制整体上反倒在不断简化。
利用SecurityManger机制和相关的组件,限制代码的运行时行为能力,其中,你能够定制policy文件和各类粒度的权限定义,限制代码的做用域和权限,例如对文件系统的操做
权限,或者监听某个网络端口的权限等.
另外,从原则上来讲, Java的GC等资源回收管理机制,均可以看做是运行时安全的一部分,若是相应机制失效,就会致使JVM出现OOM等错误,可看做是另类的拒绝服务。
第二, Java提供的安全框架API,这是构建安全通讯等应用的基础。例如:
第三, 就是JDK集成的各类安全工具,例如:
在应用实践中,若是对安全要求很是高,建议打开SecurityManager,
-Djava.security.manager
请注意其开销,一般只要开启SecurityManager,就会致使10% ~ 15%的性能降低,在JDK 9之后,这个开销有所改善.
典型回答
这个问题可能有点宽泛,咱们能够用特定类型的安全风险为例,如拒绝服务( DoS)攻击,分析Java开发者须要重点考虑的点。
DoS是一种常见的网络攻击,有人也称其为“洪水攻击”。最多见的表现是,利用大量机器发送请求,将目标网站的带宽或者其余资源耗尽,致使其没法响应正经常使用户的请求。
我认为,从Java语言的角度,更加须要重视的是程序级别的攻击,也就是利用Java、 JVM或应用程序的瑕疵,进行低成本的DoS攻击,这也是想要写出安全的Java代码所必须考虑
的。例如
Java提供了序列化等创新的特性,普遍使用在远程调用等方面,但也带来了复杂的安全问题。直到今天,序列化仍然是个安全问题频发的场景。
针对序列化,一般建议:
典型回答
首先,须要对这个问题进行更加清晰的定义:
第二,理清问题的症状,这更便于定位具体的缘由,有如下一些思路:
问题可能来自于Java服务自身,也可能仅仅是受系统里其余服务的影响。初始判断能够先确认是否出现了意外的程序错误,例如检查应用自己的错误日志。
对于分布式系统,不少公司都会实现更加系统的日志、性能等监控系统。一些Java诊断工具也能够用于这个诊断,例如经过JFR( Java Flight Recorder),监控应用是否大量出
现了某种类型的异常。
若是有,那么异常可能就是个突破点。
若是没有,能够先检查系统级别的资源等状况,监控CPU、内存等资源是否被其余进程大量占用,而且这种占用是否不符合系统正常运行情况。
监控Java服务自身,例如GC日志里面是否观察到Full GC等恶劣状况出现,或者是否Minor GC在变长等;利用jstat等工具,获取内存使用的统计信息也是个经常使用手段;利
用jstack等工具检查是否出现死锁等。
若是还不能肯定具体问题,对应用进行Profling也是个办法,但由于它会对系统产生侵入性,若是不是很是必要,大多数状况下并不建议在生产系统进行。
定位了程序错误或者JVM配置的问题后,就能够采起相应的补救措施,而后验证是否解决,不然还须要重复上面部分过程。
根据系统架构不一样,分布式系统和大型单体应用也存在着思路的区别,例如,分布式系统的性能瓶颈可能更加集中。传统意义上的性能调优大可能是针对单体应用的调优,专栏的侧重
点也是如此, Charlie Hunt曾将其方法论总结为两类:
自上而下分析中,各个阶段的常见工具和思路。须要注意的是,具体的工具在不一样的操做系统上可能区别很是大。
系统性能分析中, CPU、内存和IO是主要关注项。
怎么找到最耗费CPU的Java线程,简要介绍步骤:
top –H
而后转换成为16进制。
printf "%x" your_pid
最后利用jstack获取的线程栈,对比相应的ID便可。
典型回答
所谓隔离级别( Isolation Level),就是在数据库事务中,为保证并发数据读写的正确性而提出的定义,它并非MySQL专有的概念,而是源于ANSI/ISO制定的SQL-92标准。
每种关系型数据库都提供了各自特点的隔离级别实现,虽然在一般的定义中是以锁为实现单元,但实际的实现千差万别。以最多见的MySQL InnoDB引擎为例,它是基于
MVCC( Multi-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高, MySQL事务隔离级别分为四个不一样层次:
典型回答
单独从性能角度, Netty在基础的NIO等类库之上进行了不少改进,例如:
从设计思路和目的上, Netty与Java自身的NIO框架相比有哪些不一样呢?
从API能力范围来看, Netty彻底是Java NIO框架的一个大大的超集.
典型回答
首先,咱们须要明确一般的分布式ID定义,基本的要求包括:
目前业界的方案不少,典型方案包括:
Redis、 Zookeeper、 MangoDB等中间件,也都有各类惟一ID解决方案。其中一些设计也能够算做是Snowfake方案的变种。例如, MongoDB的ObjectId提供了一个12
byte( 96位)的ID定义,其中32位用于记录以秒为单位的时间,机器ID则为24位, 16位用做进程ID, 24位随机起始的计数序列。
国内的一些大厂开源了其自身的部分分布式ID实现, InfoQ就曾经介绍过微信的seqsvr,它采起了相对复杂的两层架构,并根据社交应用的数据特色进行了针对性设计,具体请参考相关代码实现。另外, 百度、美团等也都有开源或者分享了不一样的分布式ID实现,均可以进行参考。
再补充一些当前分布式领域的面试热点,例如: