1. ThreadLocal 的设计理念与作用
Java中的 ThreadLocal 类允许我们创建只能被同一个线程读写的变量。
如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。
private ThreadLocal myThreadLocal = new ThreadLocal();
myThreadLocal.set("A thread local value”);
String threadLocalValue = (String) myThreadLocal.get();
2. ThreadPool 用法与优势
利用线程池能够带来三个好处:
第一,降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
第二,提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三,提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
Java 通过 Executors提供四种线程池:
newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;(较适合处理执行时间比较小的任务)
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行;
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
3. Concurrent 包里的其他东西:ArrayBlockingQueue、CountDownLatch 等等
- volatile
当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。
监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- volatile写和volatile读的内存语义做个总结:
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
- AbstractQueuedSynchronizer (AQS)
AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这是接下来实现大部分同步需求的基础。提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作:
java.util.concurrent.locks.AbstractQueuedSynchronizer.getState()
java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int, int)
子类推荐被定义为自定义同步装置的内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干acquire之类的方法来供使用。该同步器即可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。
同步器是实现锁的关键,利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。可以这样理解:锁的API是面向使用者的,它定义了与锁交互的公共行为,而每个锁需要完成特定的操作也是透过这些行为来完成的(比如:可以允许两个线程进行加锁,排除两个以上的线程),但是实现是依托给同步器来完成;同步器面向的是线程访问和资源控制,它定义了线程对资源是否能够获取以及线程的排队等操作。锁和同步器很好的隔离了二者所需要关注的领域,严格意义上讲,同步器可以适用于除了锁以外的其他同步设施上(包括锁)。
同步器的开始提到了其实现依赖于一个FIFO队列,那么队列中的元素Node就是保存着线程引用和线程状态的容器,每个线程对同步器的访问,都可以看做是队列中的一个节点。
- ConcurrentHashMap
ConcurrentHashMap是线程安全的HashMap的实现,默认构造同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel属性,三属性默认值分别为16、0.75及16。其内部使用锁分段技术,维持这锁Segment的数组,在Segment数组中又存放着Entity[]数组,内部hash算法将数据较均匀分布在不同锁中。
put操作:并没有在此方法上加上synchronized,首先对key.hashcode进行hash操作,得到key的hash值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。
ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key)首先对key.hashCode进行hash操作,基于其值找到对应的Segment对象,调用其get方法完成当前操作。而Segment的get操作首先通过hash值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry产生不一致性,那么ConcurrentHashMap是如何保证的?
对象数组大小的改变只有在put操作时有可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如HashEntry对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了HashEntry对象后,怎么能保证它及其next属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap采用了一个简单的方式,即HashEntry对象中的hash、key、next属性都是final的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于next属性构建的链表是不会发生变化的。
ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于put和remove等改变集合对象的操作,基于volatile及HashEntry链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的Map而言,而它采用的这些方法也可谓是对于Java内存模型、并发机制深刻掌握的体现。
- ReentrantLock
在并发包的开始部分介绍了volatile特性及AQS同步器,而这两部分正是ReentrantLock实现的基础。通过上面AQS的介绍及原理分析,可知道是以volatile维持的int类型的state值,来判断线程是执行还是在syn队列中等待。
ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
对于公平和非公平的定义是通过对同步器AbstractQueuedSynchronizer的扩展加以实现的,也就是tryAcquire的实现上做了语义的控制。
公平和非公平性的更多原理分析见于http://ifeve.com/reentrantlock-and-fairness/
- Condition
Condition是并发包中提供的一个接口,典型的实现有ReentrantLock,ReentrantLock提供了一个newCondition的方法,以便用户在同一个锁的情况下可以根据不同的情况执行等待或唤醒动作。典型的用法可参考ArrayBlockingQueue的实现,下面来看ReentrantLock中newCondition的实现。
ReentrantLock.newCondition()创建一个AbstractQueuedSynchronizer的内部类ConditionObject的对象实例。
ReentrantLock.newCondition().await()将当前线程加入此condition的等待队列中,并将线程置为等待状态。
ReentrantLock.newCondition().signal()从此condition的等待队列中获取一个等待节点,并将节点上的线程唤醒,如果要唤醒全部等待节点的线程,则调用signalAll方法。
- CopyOnWriteArrayList
CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList,其具体实现方法如下。
CopyOnWriteArrayList()和ArrayList不同,此步的做法为创建一个大小为0的数组。
add(E)方法并没有加上synchronized关键字,它通过使用ReentrantLock来保证线程安全。此处和ArrayList的不同是每次都会创建一个新的Object数组,此数组的大小为当前数组大小加1,将之前数组中的内容复制到新的数组中,并将新增加的对象放入数组末尾,最后做引用切换将新创建的数组对象赋值给全局的数组对象。
remove(E)和add方法一样,此方法也通过ReentrantLock来保证其线程安全,但它和ArrayList删除元素采用的方式并不一样。
首先创建一个比当前数组小1的数组,遍历新数组,如找到equals或均为null的元素,则将之后的元素全部赋值给新的数组对象,并做引用切换,返回true;如未找到,则将当前的元素赋值给新的数组对象,最后特殊处理数组中的最后一个元素,如最后一个元素等于要删除的元素,即将当前数组对象赋值为新创建的数组对象,完成删除操作,如最后一个元素也不等于要删除的元素,那么返回false。
此方法和ArrayList除了锁不同外,最大的不同在于其复制过程并没有调用System的arrayCopy来完成,理论上来说会导致性能有一定下降。
get(int)此方法非常简单,直接获取当前数组对应位置的元素,这种方法是没有加锁保护的,因此可能会出现读到脏数据的现象。但相对而言,性能会非常高,对于写少读多且脏数据影响不大的场景而言是不错的选择。
iterator()调用iterator方法后创建一个新的COWIterator对象实例,并保存了一个当前数组的快照,在调用next遍历时则仅对此快照数组进行遍历,因此遍历此list时不会抛出ConcurrentModificatiedException。
与ArrayList的性能对比,在读多写少的并发场景中,较之ArrayList是更好的选择,单线程以及多线程下增加元素及删除元素的性能不比ArrayList好
- CopyOnWriteArraySet
CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法。保证了无重复元素,但在add时每次都要进行数组的遍历,因此性能会略低于上个。
ArrayBlockingQueue
ThreadPoolExecutor
与每次需要时都创建线程相比,线程池可以降低创建线程的开销,在线程执行结束后进行的是回收操作,提高对线程的复用。Java中主要使用的线程池是ThreadPoolExecutor,此外还有定时的线程池ScheduledThreadPoolExecutor。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
- Future和FutureTask
Future是一个接口,FutureTask是一个具体实现类。这里先通过两个场景看看其处理方式及优点。
- 并发容器
在JDK中,有一些线程不安全的容器,也有一些线程安全的容器。并发容器是线程安全容器的一种,但是并发容器强调的是容器的并发性,也就是说不仅追求线程安全,还要考虑并发性,提升在容器并发环境下的性能。
加锁互斥的方式确实能够方便地完成线程安全,不过代价是降低了并发性,或者说是串行了。而并发容器的思路是尽量不用锁,比较有代表性的是以CopyOnWrite和Concurrent开头的几个容器。CopyOnWrite容器的思路是在更改容器的时候,把容器写一份进行修改,保证正在读的线程不受影响,这种方式用在读多写少的场景中会非常好,因为实质上是在写的时候重建了一次容器。而以Concurrent开头的容器的具体实现方式则不完全相同,总体来说是尽量保证读不加锁,并且修改时不影响读,所以达到比使用读写锁更高的并发性能。比如上面所说的ConcurrentHashMap,其他的并发容器的具体实现,可直接分析JDK中的源码。
- 参考网址
4. wait() 和 sleep() 的区别
- 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
- 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源,sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。
Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
- 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
synchronized(x){
x.notify()
//或者wait()
}
5. foreach 与正常 for 循环效率对比
在固定长度或长度不需要计算的时候for循环效率高于foreach。
在不确定长度,或计算长度有性能损耗的时候,用foreach比较方便。
foreach 使用 Iterator.next 方法寻找下一个元素,for 使用下标寻找下一个元素。
public void testForeach();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field integers:Ljava/util/List;
4: invokeinterface #3, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_1
10: aload_1
11: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
16: ifeq 32
19: aload_1
20: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
25: checkcast #6 // class java/lang/Integer
28: astore_2
29: goto 10
32: return
LineNumberTable:
line 11: 0
line 13: 29
line 14: 32
LocalVariableTable:
Start Length Slot Name Signature
0 2 i Ljava/lang/Integer;
33 0 this Ltest/TestForeach;
}
6. Java IO 与 NIO
- NIO 是面向缓冲,IO 是面向流
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
- NIO 是非阻塞,IO 是阻塞
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- NIO 的选择器(Selectors)
Selectors(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
7. 反射的作用于原理
指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法.这种动态获取信息,以及动态调用对象方法的功能叫java语言的反射机制.
- 反射的作用
生成动态代理,面向切片编程(在调用方法的前后各加栈帧).
- 反射的原理
Java 虚拟机的方法区:
Java 虚拟机有一个运行时数据区,这个数据区又被分为方法区,堆区和栈区,我们这里需要了解的主要是方法区。方法区的主要作用是存储被装载的类的类型信息,当java虚拟机装载某个类型的时候,需要类装载器定位相应的class文件,然后将其读入到java虚拟机中,紧接着虚拟机提取class中的类型信息,将这些信息存储到方法区中。
8. 泛型常用特点,List String 能否转为 List Object
不能。
由于 List String 和 List Object 不是继承关系,是两个不同的类型,不能想换转换。