2020java面试题,找不到工作来找我(二)

14 篇文章 0 订阅

阻塞线程。
put 操作-生产者
与带超时时间的 poll 类似不同在于 put 时候如果当前队列满了它会一直等待其他线程调用 notFull.signal 才会被唤醒。
take 操作-消费者
与带超时时间的 poll 类似不同在于 take 时候如果当前队列空了它会一直等待其他线程调用 notEmpty.signal()才会被唤醒。
size 操作-消费者
当前队列元素个数,如代码直接使用原子变量 count 获取。

public int size() { return count.get();
}
peek 操作
获取但是不移除当前队列的头元素,没有则返回 null。

public E peek() {
//队列空,则返回 null if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock; takeLock.lock();
try {
Node first = head.next; if (first == null)
return null; else
return first.item;
} finally {
takeLock.unlock();
}
}

remove 操作

157

删除队列里面的一个元素,有则删除返回 true,没有则返回 false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。

last = trail;
//如果当前队列满,删除后,也不忘记最快的唤醒等待的线程if (count.getAndDecrement() == capacity)
notFull.signal();
}

开源框架的使用
tomcat 中任务队列 TaskQueue。
类结构图:

可知TaskQueue 继承了LinkedBlockingQueue 并且泛化类型固定了为Runnalbe.重写了offer,poll,take 方法。
tomcat 中有个线程池 ThreadPoolExecutor,在 NIOEndPoint 中当 acceptor 线程接受到请求后,会把任务放入队列,然后 poller 线程从队列里面获取任务,然后就把任务放入线程池执行。这个 ThreadPoolExecutor 中的的一个参数就是 TaskQueue。
先看看 ThreadPoolExecutor 的参数如果是普通 LinkedBlockingQueue 是怎么样的执行逻辑: 当调用线程池方法 execute() 方法添加一个任务时:

如果当前运行的线程数量小于 corePoolSize,则创建新线程运行该任务
如果当前运行的线程数量大于或等于 corePoolSize,则将这个任务放入阻塞队列。
如果当前队列满了,并且当前运行的线程数量小于 maximumPoolSize,则创建新线程运行该任务;
如果当前队列满了,并且当前运行的线程数量大于或等于 maximumPoolSize,那么线程池将会抛出RejectedExecutionException 异常。
如果线程执行完了当前任务,那么会去队列里面获取一个任务来执行,如果任务执行完了,并且当前线程数大于
corePoolSize,那么会根据线程空闲时间 keepAliveTime 回收一些线程保持线程池 corePoolSize 个线程。首先看下线程池中 exectue 添加任务时候的逻辑:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();

//当前工作线程个数小于 core 个数则开新线程执行(1)
int c = ctl.get();
if (workerCountOf© < corePoolSize) { if (addWorker(command, true))
return;
c = ctl.get();
}
//放入队列(2)
if (isRunning© && workQueue.offer(command)) { int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) reject(command);
else if (workerCountOf(recheck) == 0) addWorker(null, false);
}

//如果队列满了则开新线程,但是个数要不超过最大值,超过则返回 false
//然后执行 reject handler(3)
else if (!addWorker(command, false)) reject(command);
}
可知当当前工作线程个数为 corePoolSize 后,如果在来任务会把任务添加到队列,队列满了或者入队失败了则开

启新线程。
然后看看 TaskQueue 中重写的 offer 方法的逻辑:

public boolean offer(Runnable o) {
// 如果 parent 为 null 则直接调用父类方法
if (parent==null) return super.offer(o);
//如果当前线程池中线程个数达到最大,则无条件调用父类方法
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//如果当前提交的任务小于当前线程池线程数,说明线程用不完,没必要重新开线程
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//如果当前线程池线程个数>core 个数但是小于最大个数,则开新线程代替放入队列
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//到了这里,无条件调用父类return super.offer(o);
}
可知 parent.getPoolSize()<parent.getMaximumPoolSize()普通队列会把当前任务放入队列,TaskQueue 则是返回 false,因为这会开启新线程执行任务,当然前提是当前线程个数没有达到最大值。
LinkedBlockingQueue 安全分析总结
仔细思考下阻塞队列是如何实现并发安全的维护队列链表的,先分析下简单的情况就是当队列里面有多个元素时候,由于同时只有一个线程(通过独占锁 putLock 实现)入队元素并且是操作 last 节点(,而同时只有一个出队线程(通过独占锁 takeLock 实现)操作 head 节点,所以不存在并发安全问题。

考虑当队列为空的时候队列状态为:

这时候假如一个线程调用了 take 方法, 由于队列为空, 所以 count.get()0 所以当前线程会调用
notEmpty.await() 把 自 己 挂 起 , 并 且 放 入 notEmpty 的 条 件 队 列 , 并 且 释 放 当 前 条 件 变 量 关 联 的 通 过
takeLock.lockInterruptibly() 获取的独占锁。由于释放了锁, 所以这时候其他线程调用 take 时候就会通过
takeLock.lockInterruptibly()获取独占锁,然后同样阻塞到 notEmpty.await(),同样会被放入 notEmpty 的条件队列,也就说在队列为空的情况下可能会有多个线程因为调用 take 被放入了 notEmpty 的条件队列。
这时候如果有一个线程调用了 put 方法,那么就会调用 enqueue 操作,该操作会在 last 节点后面添加新元素并且设置 last 为新节点。然后 count.getAndIncrement()先获取当前队列元个数为 0 保存到 c,然后自增 count 为 1, 由于 c
0 所以调用 signalNotEmpty 激活 notEmpty 的条件队列里面的阻塞时间最长的线程,这时候 take 中调用
notEmpty.await()的线程会被激活 await 内部会重新去获取独占锁获取成功则返回,否者被放入 AQS 的阻塞队列,如果获取成功,那么 count.get() >0 因为可能多个线程 put 了,所以调用 dequeue 从队列获取元素(这时候一定可以获取到),然后调用 c = count.getAndDecrement() 把当前计数返回后并减去 1,如果 c>1 说明当前队列还有其他元素,那么就调用 notEmpty.signal()去激活 notEmpty 的条件队列里面的其他阻塞线程。
考虑当队列满的时候:

当队列满的时候调用 put 方法时候,会由于 notFull.await()当前线程被阻塞放入 notFull 管理的条件队列里面, 同理可能会有多个调用 put 方法的线程都放到了 notFull 的条件队列里面。
这时候如果有一个线程调用了 take 方法,调用 dequeue()出队一个元素,c = count.getAndDecrement();count 值减一;c==capacity;现在队列有一个空的位置,所以调用 signalNotFull()激活 notFull 条件队列里面等待最久的一个线程。
LinkedBlockingQueue 简单示例
并发库中的 BlockingQueue 是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法 put()和
take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从 head 取一个对象,如果没有对象,就等待直到有可取的对象。
下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中, 另外四个写线程用于取出文件对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该 Demo 也使用到了线程池和原子整型 (AtomicInteger),AtomicInteger 可以在并发情况下达到原子化更新,避免使用了 synchronized,而且性能非常高。由 于阻塞队列的 put 和 take 操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫
“哨兵”,当发现这个哨兵后,写线程就退出。当然线程池也要显式退出了。

public static void main(String[] args) {
// 能容纳 100 个文件
final BlockingQueue queue = new LinkedBlockingQueue(100);
// 线程池
final ExecutorService exec = Executors.newFixedThreadPool(5);
final File root = new File(“F:\JavaLib”);
// 完成标志
final File exitFile = new File("");
// 读个数
final AtomicInteger rc = new AtomicInteger();
// 写个数
final AtomicInteger wc = new AtomicInteger();
// 读线程
Runnable read = new Runnable() {
public void run() {
scanFile(root);
scanFile(exitFile);
}

public void scanFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory()
|| pathname.getPath().endsWith(".java");
}
});
for (File one : files)
scanFile(one);
} else {
try {
int index = rc.incrementAndGet();
System.out.println("Read0: " + index + " "

  • file.getPath());
    queue.put(file);
    } catch (InterruptedException e) {
    }
    }
    }
    };
    exec.submit(read);
    // 四个写线程

for (int index = 0; index < 4; index++) {
// write thread
final int NO = index;
Runnable write = new Runnable() {
String threadName = “Write” + NO;
public void run() {
while (true) {
try {
Thread.sleep(randomTime());
int index = wc.incrementAndGet();
File file = queue.take();
// 队列已经无对象
if (file == exitFile) {
// 再次添加"标志",以让其他线程正常退出
queue.put(exitFile);
break;
}
System.out.println(threadName + ": " + index + " "

  • file.getPath());
    } catch (InterruptedException e) {
    }
    }
    }
    };
    exec.submit(write);
    }
    exec.shutdown();
    }
    }

PriorityBlockingQueue 无界阻塞优先级队列
PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现,研究过数组方式存放最小堆节点的都知道,直接遍历队列元素是无序的。
PriorityBlockingQueue 类图结构

如图 PriorityBlockingQueue 内部有个数组 queue 用来存放队列元素, size 用来存放队列元素个数,
allocationSpinLockOffset 是用来在扩容队列时候做 cas 的,目的是保证只有一个线程可以进行扩容。
由于这是一个优先级队列所以有个比较器 comparator 用来比较元素大小。lock 独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty 条件变量用来实现 take 方法阻塞模式。这里没有 notFull 条件变量是因为这里的 put 操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。
最后 PriorityQueue q 用来搞序列化的。
如下构造函数,默认队列容量为 11,默认比较器为 null;

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null);
}
166

public PriorityBlockingQueue(int initialCapacity) { this(initialCapacity, null);
}

public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException(); this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.comparator = comparator;
this.queue = new Object[initialCapacity];
}

PriorityBlockingQueue 方法
Offer 操作
在队列插入一个元素,由于是无界队列,所以一直为成功返回 true;

public boolean offer(E e) {

if (e == null)
throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock();
int n, cap;
Object[] array;

//如果当前元素个数>=队列容量,则扩容(1)
while ((n = size) >= (cap = (array = queue).length)) tryGrow(array, cap);

try {
Comparator<? super E> cmp = comparator;

//默认比较器为 null if (cmp == null)(2)
siftUpComparable(n, e, array); else
//自定义比较器(3)
167

siftUpUsingComparator(n, e, array, cmp);

//队列元素增加 1,并且激活 notEmpty 的条件队列里面的一个阻塞线程
size = n + 1;(9)
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}

主流程比较简单,下面看看两个主要函数

private void tryGrow(Object[] array, int oldCap) { lock.unlock(); //must release and then re-acquire main lock Object[] newArray = null;

//cas 成功则扩容(4)
if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
//oldGap<64 则扩容新增 oldcap+2,否者扩容 50%,并且最大为 MAX_ARRAY_SIZE int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small (oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE) throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array) newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}

//第一个线程 cas 成功后,第二个线程会进入这个地方,然后第二个线程让出 cpu,尽量让第一个线程执行下面点获取锁,但是这得不到肯定的保证。(5)
if (newArray == null) // back off if another thread is allocating Thread.yield();
lock.lock();(6)
168

if (newArray != null && queue == array) { queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}

tryGrow 目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用 cas 控制只有一个线程可以扩容成功。我的理解是为了性能,因为扩容时候是需要花时间的,如果这些操作时候还占用锁那么其他线程在这个时候是不能进行出队操作的,也不能进行入队操作,这大大降低了并发性。
所以在扩容前释放锁,这允许其他出队线程可以进行出队操作,但是由于释放了锁,所以也允许在扩容时候进行入队操作,这就会导致多个线程进行扩容会出现问题,所以这里使用了一个 spinlock 用 cas 控制只有一个线程可以进行扩容,失败的线程调用 Thread.yield()让出 cpu,目的意在让扩容线程扩容后优先调用 lock.lock 重新获取锁,但是这得不到一定的保证,有可能调用 Thread.yield()的线程先获取了锁。
那 copy 元素数据到新数组为啥放到获取锁后面那?原因应该是因为可见性问题,因为 queue 并没有被 volatile 修饰。另外有可能在扩容时候进行了出队操作,如果直接拷贝可能看到的数组元素不是最新的。而通过调用 Lock 后,获取的数组则是最新的,并且在释放锁前 数组内容不会变化。
具体建堆算法:

private static void siftUpComparable(int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x;

//队列元素个数>0 则判断插入位置,否者直接入队(7) while (k > 0) {
int parent = (k - 1) >>> 1; Object e = array[parent];
if (key.compareTo((T) e) >= 0) break;
array[k] = e; k = parent;
}
array[k] = key;(8)
}
下面用图说话模拟下过程:

假设队列容量为 2
第一次 offer(2)时候

执行(1)为 false 所以执行(2),由于 k=n=size=0;所以执行(8)元素入队,然执行(9)size+1;
现在队列状态:

第二次 offer(4)时候
执行(1)为 false,所以执行(2)由于 k=1,所以进入 while 循环,parent=0;e=2;key=4;key>e 所以 break;
然后把 4 存到数据下标为 1 的地方,这时候队列状态为:

第三次 offer(4)时候
执行(1)为 true,所以调用 tryGrow,由于 2<64 所以 newCap=2 + (2+2)=6;然后创建新数组并拷贝,然后调用 siftUpComparable;k=2>0 进入循环 parent=0;e=2;key=6;key>e 所以 break;然后把 6 放入下标为 2 的地方,现在队列状态:

第四次 offer(1)时候
执行(1)为 false,所以执行(2)由于 k=3,所以进入 while 循环,parent=0;e=2;key=1; key<e;所以把 2
复制到数组下标为 3 的地方,然后 k=0 退出循环;然后把 2 存放到下标为 0 地方,现在状态:

Poll 操作
在队列头部获取并移除一个元素,如果队列为空,则返回 null

public E poll() {
final ReentrantLock lock = this.lock; lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
主要看 dequeue

private E dequeue() {

//队列为空,则返回 null int n = size - 1;
if (n < 0)
return null; else {

//获取队头元素(1)

Object[] array = queue; E result = (E) array[0];

//获取对尾元素,并值 null(2) E x = (E) array[n];
array[n] = null;

Comparator<? super E> cmp = comparator;
if (cmp == null)//cmp=null 则调用这个,把对尾元素位置插入到 0 位置,并且调整堆为最小堆(3) siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp); size = n;(4)
return result;
}
}

private static void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x; int half = n >>> 1; // loop while a non-leaf while (k < half) {
int child = (k << 1) + 1; // assume left child is least Object c = array[child];(5)
int right = child + 1;(6)
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)(7) c = array[child = right];
if (key.compareTo((T) c) <= 0)(8) break;
array[k] = c; k = child;
}
array[k] = key;(9)
}
}

下面用图说话模拟下过程:
第一次调用 poll()
首先执行(1) result=1;然后执行(2)x=2;这时候队列状态

http:// t© 2018 黑马程序员

然后执行(3)后状态为:

执行(4)后的结果:

下面重点说说 siftDownComparable 这个屌屌的建立最小堆的算法:
首先说下思想,其中 k 一开始为 0,x 为数组里面最后一个元素,由于第 0 个元素为树根,被出队时候要被搞掉, 所以建堆要从它的左右孩子节点找一个最小的值来当树根,子树根被搞掉后,会找子树的左右孩子最小的元素来代替, 直到树节点为止,还不明白,没关系,看图说话:
假如当前队列元素:

那么对于树为:

这时候如果调用了 poll();那么 result=2;x=11;现在树为:

然后看 leftChildVal = 4;rightChildVal = 6; 4<6;所以 c=4;也就是获取根节点的左右孩子值小的那一个; 然后看
11>4 也就是 key>c;然后把 c 放入树根,现在树为:

然后看根的左边孩子 4 为根的子树我们要为这个字树找一个根节点。
看 leftChildVal = 8;rightChildVal = 10; 8<10;所以 c=8;也就是获取根节点的左右孩子值小的那一个; 然后看
11>8 也就是 key>c;然后把 c 放入树根,现在树为:

这时候 k=3;half=3 所以推出循环,执行(9)后结果为:

这时候队列为:

Put 操作
内部调用的 offer,由于是无界队列,所以不需要阻塞

public void put(E e) {
offer(e); // never need to block
}
Take 操作
获取队列头元素,如果队列为空则阻塞。

public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly();
E result; try {

//如果队列为空,则阻塞,把当前线程放入 notEmpty 的条件队列
while ( (result = dequeue()) == null) notEmpty.await();
} finally {
lock.unlock();
}
176

return result;
}

这里是阻塞实现,阻塞后直到入队操作调用 notEmpty.signal 才会返回。
Size 操作
获取队列元个数,由于加了独占锁所以返回结果是精确的

public int size() {
final ReentrantLock lock = this.lock; lock.lock();
try {
return size;
} finally {
lock.unlock();
}
}
PriorityBlockingQueue 小结
PriorityBlockingQueue 类似于 ArrayBlockingQueue 内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个 notEmpty 条件变量而没有 notFull 这是因为前者是无界队列,当 put 时候永远不会处于 await 所以也不需要被唤醒。
PriorityBlockingQueue 始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。
值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过 cas 保证同时只有一个线程可以扩容成功。
PriorityBlockingQueue 示例
PriorityBlockingQueue 类是 JDK 提供的优先级队列 本身是线程安全的 内部使用显示锁 保证线程安全。
PriorityBlockingQueue 存储的对象必须是实现 Comparable 接口的 因为 PriorityBlockingQueue 队列会根据内部存储的每一个元素的 compareTo 方法比较每个元素的大小。这样在 take 出来的时候会根据优先级 将优先级最

小的最先取出 。

177

下面是示例代码

public static PriorityBlockingQueue queue = new PriorityBlockingQueue();

public static void main(String[] args) { queue.add(new User(1,“wu”));
queue.add(new User(5,“wu5”)); queue.add(new User(23,“wu23”)); queue.add(new User(55,“wu55”)); queue.add(new User(9,“wu9”));
queue.add(new User(3,“wu3”)); for (User user : queue) {
try {
System.out.println(queue.take().name);
} catch (InterruptedException e) { e.printStackTrace();
}
}
}

//静态内部类
static class User implements Comparable{

public User(int age,String name) { this.age = age;
this.name = name;
}

int age;
String name;

@Override
public int compareTo(User o) { return this.age > o.age ? -1 : 1;
}
}

SychronousQueue 同步队列
SynchronousQueue 是一个比较特别的队列,由于在线程池方面有所应用,为了更好的理解线程池的实现原理, 此队列源码中充斥着大量的 CAS 语句,理解起来是有些难度的,为了方便日后回顾,本篇文章会以简洁的图形化方式
178

展示该队列底层的实现原理。

SychronousQueue 简单实用
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞; 有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
如下面的示意图所示:

SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即
put 的时候),如果当前没有人想要消费产品(即当前没有线程执行 take),此生产线程必须阻塞,等待一个消费线程调用 take 操作,take 操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先 take 后 put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:

package com.concurrent;
import java.util.concurrent.SynchronousQueue; public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue queue = new SynchronousQueue();

Thread putThread = new Thread(new Runnable() { @Override
public void run() { System.out.println(“put thread start”); try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println(“put thread end”);
}
});

Thread takeThread = new Thread(new Runnable() { @Override
public void run() { System.out.println(“take thread start”); try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println(“take thread end”);
}
});

putThread.start(); Thread.sleep(1000); takeThread.start();
}
}
一种输出结果如下:
put thread start take thread start
take from putThread: 1 put thread end
take thread end

从结果可以看出,put 线程执行 queue.put(1) 后就被阻塞了,只有 take 线程进行了消费,put 线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。
SychronousQueue 实现原理

不 像 ArrayBlockingQueue 、 LinkedBlockingDeque 之 类 的 阻 塞 队 列 依 赖 AQS 实 现 并 发 操 作 ,
SynchronousQueue 直接使用 CAS 实现线程的安全访问。
队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。
公平模式下的模型:
公平模式下,底层实现使用的是 TransferQueue 这个内部队列,它有一个 head 和 tail 指针,用于指向当前正在等待匹配的线程节点。
初始化时,TransferQueue 的状态如下:

接着我们进行一些操作:
1、线程 put1 执行 put(1)操作,由于当前没有配对的消费线程,所以 put1 线程入队列,自旋一小会后睡眠等待,这时队列状态如下:

2、接着,线程 put2 执行了 put(2)操作,跟前面一样,put2 线程入队列,自旋一小会后睡眠等待,这时队列

状态如下:

3、这时候,来了一个线程 take1,执行了 take 操作,由于 tail 指向 put2 线程,put2 线程跟 take1 线程配对了(一 put 一 take),这时 take1 线程不需要入队,但是请注意了,这时候,要唤醒的线程并不是 put2,而是 put1。
为何? 大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显是put1 应该优先被唤醒。至于读者可能会有一个疑问,明明是 take1 线程跟 put2 线程匹配上了,结果是 put1 线程被唤醒消费,怎么确保 take1 线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配队头出队。
执行后 put1 线程被唤醒,take1 线程的 take()方法返回了 1(put1 线程的数据),这样就实现了线程间的一对一通信,这时候内部状态如下:

4、最后,再来一个线程 take2,执行 take 操作,这时候只有 put2 线程在等候,而且两个线程匹配上了,线程 put2 被唤醒, take2 线程 take 操作返回了 2(线程 put2 的数据),这时候队列又回到了起点,如下所示:

以上便是公平模式下,SynchronousQueue 的实现模型。总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。
非公平模式下的模型:
我们还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。非公平模式底层的实现使用的是
TransferStack,一个栈,实现中用 head 指针指向栈顶,接着我们看看它的实现模型:
1、线程 put1 执行 put(1)操作,由于当前没有配对的消费线程,所以 put1 线程入栈,自旋一小会后睡眠等待,这时栈状态如下:

head

2、接着,线程 put2 再次执行了 put(2)操作,跟前面一样,put2 线程入栈,自旋一小会后睡眠等待,这时栈状态如下:

head

3、这时候,来了一个线程 take1,执行了 take 操作,这时候发现栈顶为 put2 线程,匹配成功,但是实现会先把 take1 线程入栈,然后 take1 线程循环执行匹配 put2 线程逻辑,一旦发现没有并发冲突,就会把栈顶指针直接指向 put1 线程

步骤一:

head

循环中匹配put2线程

步骤二:

head


4、最后,再来一个线程 take2,执行 take 操作,这跟步骤 3 的逻辑基本是一致的,take2 线程入栈, 然后在循环中匹配 put1 线程,最终全部匹配完毕,栈变为空,恢复初始状态,如下图所示:
步骤一:

head

循环中匹配put1线程

步骤二:

head

可以从上面流程看出,虽然 put1 线程先入栈了,但是却是后匹配,这就是非公平的由来。

SychronousQueue 总结
SynchronousQueue 由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用 AQS,而是直接使用 CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。
DeplayQueue 延时无界阻塞队列
在谈到 DelayQueue 的使用和原理的时候,我们首先介绍一下 DelayQueue,DelayQueue 是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。
DelayQueue 阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检 查看数据是否过期等,首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要 12:00 点执行的任务可能
12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用 DelayQueue。

下面将会对 DelayQueue 做一个介绍,然后举个例子。并且提供一个 Delayed 接口的实现和 Sample 代码。
DelayQueue 是一个 BlockingQueue,其特化的参数是 Delayed。(不了解 BlockingQueue 的同学,先去了解
BlockingQueue 再看本文)
Delayed 扩展了 Comparable 接口,比较的基准为延时的时间值,Delayed 接口的实现类 getDelay 的返回值应为固定值(final)。DelayQueue 内部是使用 PriorityQueue 实现的。
DelayQueue = BlockingQueue +PriorityQueue + Delayed

DelayQueue 定义和原理
DelayQueue 的关键元素 BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue 是一个使用优先队列(PriorityQueue)实现的 BlockingQueue,优先队列的比较基准值是时间。
他们的基本定义如下

public interface Comparable { public int compareTo(T o);
}
public interface Delayed extends Comparable { long getDelay(TimeUnit unit);
}
public class DelayQueue implements BlockingQueue { private final PriorityQueue q = new PriorityQueue();
}
DelayQueue 内部的实现使用了一个优先队列。当调用 DelayQueue 的 offer 方法时,把 Delayed 对象加入到优先队列 q 中。如下:

187

lock.unlock();
}
}

DelayQueue 的 take 方法,把优先队列 q 的 first 拿出来(peek),如果没有达到延时阀值,则进行 await
处理。如下:

public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly();
try {
for (;😉 {
E first = q.peek(); if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll(); assert x != null; if (q.size() != 0)
available.signalAll(); // wake up other takers return x;

}
}
}
} finally {
lock.unlock();
}
}
DelayQueue 实例应用
Ps:为了具有调用行为,存放到 DelayDeque 的元素必须继承 Delayed 接口。Delayed 接口使对象成为延迟对象,它使存放在 DelayQueue 类中的对象具有了激活日期。该接口强制执行下列两个方法。
一下将使用 Delay 做一个缓存的实现。其中共包括三个类

Pair

188

DelayItem
Cache Pair 类:
public class Pair<K, V> {
public K first;

public V second;

public Pair() {}

public Pair(K first, V second) { this.first = first; this.second = second;
}
}
一下是对 Delay 接口的实现:

import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class DelayItem implements Delayed {
/** Base of nanosecond timings, to avoid wrapping */ private static final long NANO_ORIGIN = System.nanoTime();

/**

  • Returns nanosecond time offset by origin
    */
    final static long now() {
    return System.nanoTime() - NANO_ORIGIN;
    }

/**
*Sequence number to break scheduling ties, and in turn to guarantee FIFO order among tied
*entries.
*/
private static final AtomicLong sequencer = new AtomicLong(0);

/** Sequence number to break ties FIFO */ private final long sequenceNumber;
189

/** The time the task is enabled to execute in nanoTime units */ private final long time;

private final T item;

public DelayItem(T submit, long timeout) { this.time = now() + timeout;
this.item = submit;
this.sequenceNumber = sequencer.getAndIncrement();
}

public T getItem() { return this.item;
}

public long getDelay(TimeUnit unit) {
long d = unit.convert(time - now(), TimeUnit.NANOSECONDS); return d;
}

public int compareTo(Delayed other) {
if (other == this) // compare zero ONLY if same object return 0;
if (other instanceof DelayItem) { DelayItem x = (DelayItem) other; long diff = time - x.time;
if (diff < 0) return -1;
else if (diff > 0) return 1;
else if (sequenceNumber < x.sequenceNumber) return -1;
else
return 1;
}
long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
}

以下是 Cache 的实现,包括了 put 和 get 方法
import java.util.concurrent.ConcurrentHashMap;
190

import java.util.concurrent.ConcurrentMap; import java.util.concurrent.DelayQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level;
import java.util.logging.Logger;

public class Cache<K, V> {
private static final Logger LOG = Logger.getLogger(Cache.class.getName());

private ConcurrentMap<K, V> cacheObjMap = new ConcurrentHashMap<K, V>();

private DelayQueue<DelayItem<Pair<K, V>>> q = new DelayQueue<DelayItem<Pair<K, V>>>();

private Thread daemonThread;

public Cache() {

Runnable daemonTask = new Runnable() { public void run() {
daemonCheck();
}
};

daemonThread = new Thread(daemonTask); daemonThread.setDaemon(true); daemonThread.setName(“Cache Daemon”); daemonThread.start();
}

private void daemonCheck() {

if (LOG.isLoggable(Level.INFO)) LOG.info(“cache service started.”);

for (;😉 { try {
DelayItem<Pair<K, V>> delayItem = q.take(); if (delayItem != null) {
// 超时对象处理
Pair<K, V> pair = delayItem.getItem(); cacheObjMap.remove(pair.first, pair.second); // compare and remove
}
} catch (InterruptedException e) {
191

if (LOG.isLoggable(Level.SEVERE)) LOG.log(Level.SEVERE, e.getMessage(), e);
break;
}
}

if (LOG.isLoggable(Level.INFO)) LOG.info(“cache service stopped.”);
}

// 添加缓存对象
public void put(K key, V value, long time, TimeUnit unit) { V oldValue = cacheObjMap.put(key, value);
if (oldValue != null) q.remove(key);

long nanoTime = TimeUnit.NANOSECONDS.convert(time, unit);
q.put(new DelayItem<Pair<K, V>>(new Pair<K, V>(key, value), nanoTime));
}

public V get(K key) {
return cacheObjMap.get(key);
}

测试 main 方法:

// 测试入口函数
public static void main(String[] args) throws Exception { Cache<Integer, String> cache = new Cache<Integer, String>(); cache.put(1, “aaaa”, 3, TimeUnit.SECONDS);

Thread.sleep(1000 * 2);
{
String str = cache.get(1); System.out.println(str);
}

Thread.sleep(1000 * 2);
{
String str = cache.get(1); System.out.println(str);
}
}

输出结果为:
aaaa
null

我们看到上面的结果,如果超过延时的时间,那么缓存中数据就会自动丢失,获得就为 null。

C.并发(Collection)队列-非阻塞队列

非阻塞队列
首先我们要简单的理解下什么是非阻塞队列:
与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。在底层,非阻塞队列使用的是 CAS(compare and swap)来实现线程执行的非阻塞。
非阻塞队列简单操作
与阻塞队列相同,非阻塞队列中的常用方法,也是出队和入队。入队方法:
add():底层调用 offer();
offer():Queue 接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回 true; 出队方法:
poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回 null;
peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回 null;

非阻塞算法 CAS
首先我们需要了解悲观锁和乐观锁

悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进

一个人,才能时刻盯紧他”。

乐观锁:假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解, 就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。
首先保证正确性,有必要的话,再去追求性能。

CAS
乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个 CAS 指令,实现“Compare And Swap”的语义(这里的 swap 是“换入”,也就是 set),构成了基本的乐观锁。
CAS 包含 3 个操作数:
需要读写的内存位置 V
进行比较的值 A
拟写入的新值 B
当且仅当位置 V 的值等于 A 时,CAS 才会通过原子方式用新值 B 来更新位置 V 的值;否则不会执行任何操作。无论位置 V 的值是否等于 A,都将返回 V 原有的值。
一个有意思的事实是,“使用 CAS 控制并发”与“使用乐观锁”并不等价。CAS 只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。下文将分别用 CAS 实现悲观锁和乐观锁?
ConcurrentLinkedQueue 非阻塞无界链表队列
ConcurrentLinkedQueue 是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来说队列的
194

长度可以无限扩大。
与其他队列相同,ConcurrentLinkedQueue 也采用的是先进先出(FIFO)入队规则,对元素进行排序。当我们向队列中添加元素时,新插入的元素会插入到队列的尾部;而当我们获取一个元素时,它会从队列的头部中取出。
因为 ConcurrentLinkedQueue 是链表结构,所以当入队时,插入的元素依次向后延伸,形成链表;而出队时,则从链表的第一个元素开始获取,依次递增;
不知道,我这样形容能否让你对链表的入队、出队产生一个大概的思路!

ConcurrentLinkedQuere 简单示例
值得注意的是,在使用 ConcurrentLinkedQueue 时,如果涉及到队列是否为空的判断,切记不可使用 size()==0 的做法,因为在 size()方法中,是通过遍历整个链表来实现的,在队列元素很多的时候,size()方法十分消耗性能和时间,只是单纯的判断队列为空使用 isEmpty()即可!!!

System.out.println(“出队元素为”+ele);
}
}
}

public static void main(String[] agrs) {
ExecutorService executorService = Executors.newFixedThreadPool(4); for(int x=0;x<threadCount;x++){
executorService.submit(new Offer()); executorService.submit(new Poll());
}
executorService.shutdown();
}
}

一种输出:
入队元素为 313732926
出队元素为 313732926
入队元素为 812655435
出队元素为 812655435
入队元素为 1893079357
出队元素为 1893079357
入队元素为 1137820958
出队元素为 1137820958
入队元素为 1965962048
出队元素为 1965962048
出队元素为 685567162
入队元素为 685567162
出队元素为 1441081163
入队元素为 1441081163
出队元素为 1627184732
入队元素为 1627184732

ConcurrentLinkedQuere 类图

如图 ConcurrentLinkedQueue 中有两个 volatile 类型的 Node 节点分别用来存在列表的首尾节点,其中 head 节点存放链表第一个 item 为 null 的节点,tail 则并不是总指向最后一个节点。Node 节点内部则维护一个变量 item 用来存放节点的值,next 用来存放下一个节点,从而链接为一个单向无界列表。
public ConcurrentLinkedQueue() {
head = tail = new Node(null);
}
如上代码初始化时候会构建一个 item 为 NULL 的空节点作为链表的首尾节点。

ConcurrentLinkedQuere 方法
Offer 操作
offer 操作是在链表末尾添加一个元素,下面看看实现原理。

public boolean offer(E e) {
//e 为 null 则抛出空指针异常

checkNotNull(e);

//构造 Node 节点构造函数内部调用 unsafe.putObject,后面统一讲final Node newNode = new Node(e);

//从尾节点插入
for (Node t = tail, p = t;😉 {

Node q = p.next;

//如果 q=null 说明 p 是尾节点则插入
if (q == null) {

//cas 插入(1)
if (p.casNext(null, newNode)) {
//cas 成功说明新增节点已经被放入链表,然后设置当前尾节点(包含 head,1,3,5.。。个节点为尾节点) if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK. return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)//(2)
//多线程操作时候,由于 poll 时候会把老的 head 变为自引用,然后 head 的 next 变为新 head,所以这里需要
//重新找新的 head,因为新的 head 后面的节点才是激活的节点
p = (t != (t = tail)) ? t : head; else
// 寻找尾节点(3)
p = (p != t && t != (t = tail)) ? t : q;
}
}

从构造函数知道一开始有个 item 为 null 的哨兵节点,并且 head 和 tail 都是指向这个节点,然后当一个线程调用 offer 时候首先

如图首先查找尾节点,qnull,p 就是尾节点,所以执行 p.casNext 通过 cas 设置 p 的 next 为新增节点, 这时候 pt 所以不重新设置尾节点为当前新节点。由于多线程可以调用 offer 方法,所以可能两个线程同时执行到了(1)进行 cas,那么只有一个会成功(假如线程 1 成功了),成功后的链表为:
失败的线程会循环一次这时候指针为:

这时候会执行(3)所以 p=q,然后在循环后指针位置为:

所以没有其他线程干扰的情况下会执行(1)执行 cas 把新增节点插入到尾部,没有干扰的情况下线程 2 cas 会成功,然后去更新尾节点 tail,由于 p!=t 所以更新。这时候链表和指针为:

假如线程 2cas 时候线程 3 也在执行,那么线程 3 会失败,循环一次后,线程 3 的节点状态为:

这时候 p!=t ;并且 t 的原始值为 told,t 的新值为 tnew ,所以 told!=tnew,所以 p=tnew=tail;

然后在循环一下后节点状态:

qnull 所以执行(1)。
现在就差 p
q 这个分支还没有走,这个要在执行 poll 操作后才会出现这个情况。poll 后会存在下面的状态

这个时候添加元素时候指针分布为:

所以会执行(2)分支 结果 p=head
然后循环,循环后指针分布:

所以执行(1),然后 p!=t 所以设置 tail 节点。现在分布图:

自引用的节点会被垃圾回收掉。

add 操作
add 操作是在链表末尾添加一个元素,下面看看实现原理。其实内部调用的还是 offer
public boolean add(E e) {
return offer(e);
}
poll 操作
poll 操作是在链表头部获取并且移除一个元素,下面看看实现原理。

public E poll() { restartFromHead:

// 死 循 环 for (;😉 {

//死循环
for (Node h = head, p = h, q;😉 {

//保存当前节点值
E item = p.item;

//当前节点有值则 cas 变为 null(1)
if (item != null && p.casItem(item, null)) {
//cas 成功标志当前节点以及从链表中移除
if (p != h) // 类似 tail 间隔 2 设置一次头节点(2)
updateHead(h, ((q = p.next) != null) ? q : p); return item;
}
//当前队列为空则返回 null(3)
else if ((q = p.next) == null) { updateHead(h, p);
return null;
}
//自引用了,则重新找新的队列头节点(4) else if (p == q)
continue restartFromHead; else//(5)
p = q;
}

}
}
final void updateHead(Node h, Node p) { if (h != p && casHead(h, p))
h.lazySetNext(h);
}

当队列为空时候:

可知执行(3)这时候有两种情况,第一没有其他线程添加元素时候(3)结果为 true 然后因为 h!=p 为 false 所以直接返回 null。第二在执行 q=p.next 前,其他线程已经添加了一个元素到队列,这时候(3)返回 false,然后执行(5)p=q,然后循环后节点分布:

这时候执行(1)分支,进行 cas 把当前节点值值为 null,同时只有一个线程会成功,cas 成功 标示该节点从队列中移除了,然后 p!=h,调用 updateHead 方法,参数为 h,p;h!=p 所以把 p 变为当前链表 head 节点,然后 h 节点的 next 指向自己。现在状态为:

cas 失败 后 会再次循环,这时候分布图为:

这时候执行(3)返回 null.
现在还有个分支(4)没有执行过,那么什么时候会执行那?

这时候执行(1)分支,进行 cas 把当前节点值值为 null,同时只有一个线程 A 会成功,cas 成功 标示该节点从队列中移除了,然后 p!=h,调用 updateHead 方法,假如执行 updateHead 前另外一个线程 B 开始 poll 这时候它 p 指向为原来的 head 节点,然后当前线程 A 执行 updateHead 这时候 B 线程链表状态为:

所以会执行(4)重新跳到外层循环,获取当前 head,现在状态为:

peek 操作

peek 操作是获取链表头部一个元素(只读取不移除),下面看看实现原理。
代码与 poll 类似,只是少了 castItem.并且 peek 操作会改变 head 指向,offer 后 head 指向哨兵节点,第一次 peek 后 head 会指向第一个真的节点元素。
public E peek() {
restartFromHead: for (;😉 {
for (Node h = head, p = h, q;😉 { E item = p.item;
if (item != null || (q = p.next) == null) { updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead; else
p = q;
}
}
}
size 操作
获取当前队列元素个数,在并发环境下不是很有用,因为使用 CAS 没有加锁所以从调用 size 函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。

207

boolean hasItem = (p.item != null); if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead; else
p = q;
}
}
}

//获取当前节点的 next 元素,如果是自引入节点则返回真正头节点
final Node succ(Node p) { Node next = p.next;
return (p == next) ? head : next;
}

remove 操作
如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回 true,否者返回 false

public boolean remove(Object o) {

//查找元素为空,直接返回 false if (o == null) return false;
Node pred = null;
for (Node p = first(); p != null; p = succ§) { E item = p.item;

//相等则使用 cas 值 null,同时一个线程成功,失败的线程循环查找队列中其他元素是否有匹配的。if (item != null &&
o.equals(item) && p.casItem(item, null)) {

//获取 next 元素
Node next = succ§;

//如果有前驱节点,并且 next 不为空则链接前驱节点到 next, if (pred != null && next != null)
pred.casNext(p, next); return true;
}
208

pred = p;
}
return false;
}

contains 操作
判断队列里面是否含有指定对象,由于是遍历整个队列,所以类似 size 不是那么精确,有可能调用该方法时候元素还在队列里面,但是遍历过程中才把该元素删除了,那么就会返回 false.
public boolean contains(Object o) {
if (o == null) return false;
for (Node p = first(); p != null; p = succ§) { E item = p.item;
if (item != null && o.equals(item)) return true;
}
return false;
}
ConcurrentLinkedQuere 的 offer 方法有意思的问题
offer 中有个 判断 t != (t = tail)假如 t=node1;tail=node2;并且 node1!=node2 那么这个判断是 true 还是 false 那,答案是 true,这个判断是看当前 t 是不是和 tail 相等,相等则返回 true 否者为 false,但是无论结果是啥执行后 t 的值都是 tail。
下面从字节码来分析下为啥。
一个例子

public static void main(String[] args) { int t = 2;
int tail = 3;
System.out.println(t != (t = tail));
}
结果为:true
字节码文件

C:\Users\Simple\Desktop\TeacherCode\Crm_Test\build\classes\com\itheima\crm\util>javap -c Test001
警告: 二进制文件 Test001 包含 com.itheima.crm.util.Test001 Compiled from “Test001.java”
public class com.itheima.crm.util.Test001 {

public com.itheima.crm.util.Test001(); Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object.""😦)V
4: return

public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_1
8: iload_2
9: dup
10: istore_1
11: if_icmpeq 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #22 // Method java/io/PrintStream.println:(Z)V
22: return
}

我们从上面标黄的字节码文件中分析一开始栈为空:

第 0 行指令作用是把值 2 入栈栈顶元素为 2

第 1 行指令作用是将栈顶 int 类型值保存到局部变量 t 中


第 2 行指令作用是把值 3 入栈栈顶元素为 3

第 3 行指令作用是将栈顶 int 类型值保存到局部变量 tail 中。

第 4 调用打印命令
第 7 行指令作用是把变量 t 中的值入栈

第 8 行指令作用是把变量 tail 中的值入栈

现在栈里面的元素为 3、2,并且 3 位于栈顶
第 9 行指令作用是当前栈顶元素入栈,所以现在栈内容 3,3,2

第 10 行指令作用是把栈顶元素存放到 t,现在栈内容 3,2

第 11 行指令作用是判断栈顶两个元素值,相等则跳转 18。由于现在栈顶严肃为 3,2 不相等所以返回
true.
第 14 行指令作用是把 1 入栈
然后回头分析下!=是双目运算符,应该是首先把左边的操作数入栈,然后在去计算了右侧操作数。

ConcurrentLinkedQuere 总结
ConcurrentLinkedQueue 使用 CAS 非阻塞算法实现使用 CAS 解决了当前节点与 next 节点之间的安全链接和对当前节点值的赋值。由于使用 CAS 没有使用锁,所以获取 size 的时候有可能进行 offer,poll 或者 remove 操作,导致获取的元素个数不精确,所以在并发情况下 size 函数不是很有用。另外第一次 peek 或者 first 时候会把 head 指向第一个真正的队列元素。
下面总结下如何实现线程安全的,可知入队出队函数都是操作 volatile 变量:head,tail。所以要保证队列线程安全只需要保证对这两个 Node 操作的可见性和原子性,由于 volatile 本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。
对于 offer 操作是在 tail 后面添加元素,也就是调用 tail.casNext 方法,而这个方法是使用的 CAS 操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取 tail,然后执行 casNext 方法。对于 poll 也是这样的。

ConcurrentHashMap 非阻塞 Hash 集合
ConcurrentHashMap 是 Java 并发包中提供的一个线程安全且高效的 HashMap 实现,ConcurrentHashMap 在并发编程的场景中使用频率非常之高,本文就来分析下 ConcurrentHashMap 的实现原理,并对其实现原理进行分析。
ConcurrentLinkedQuere 类图

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁
ReentrantLock ,在 ConcurrentHashMap 里 扮 演 锁 的 角 色 , HashEntry 则 用 于 存 储 键 值 对 数 据 。 一 个
ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护者一个
HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

ConcurrentLinkedQuere 实现原理
众所周知,哈希表是中非常高效,复杂度为 O(1)的数据结构,在 Java 开发中,我们最常见到最频繁使用的就是
HashMap 和 HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
HashMap :先说 HashMap,HashMap 是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度 google 或查看源码分析),导致 get 操作时,cpu 空转,所以,在并发环境中使用 HashMap 是非常危险的。
HashTable : HashTable 和 HashMap 的实现原理几乎一样,差别无非是 1.HashTable 不允许 key 和 value 为
null;2.HashTable 是线程安全的。但是 HashTable 线程安全的策略实现代价却太大了,简单粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。如下图

HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在 多 线 程 访 问 时 不 同 段 的 数 据 时 , 就 不 会 存 在 锁 竞 争 了 , 这 样 便 可 以 有 效 地 提 高 并 发 效 率 。 这 就 是
ConcurrentHashMap 所采用的"分段锁"思想

ConcurrentLinkedQuere 源码解析
ConcurrentHashMap 采用了非常精妙的"分段锁"策略,ConcurrentHashMap 的主干是个 Segment 数组。
final Segment<K,V>[] segments;
Segment 继承了 ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在 ConcurrentHashMap,
216

一个Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的 ConcurrentLeve 为 16 来讲,理论上就允许 16 个线程并发执行,有木有很酷)
所以,对于同一个Segment 的操作才需考虑线程同步,不同的Segment 则无需考虑。Segment 类似于HashMap, 一个 Segment 维护着一个 HashEntry 数组
transient volatile HashEntry<K,V>[] table;
HashEntry 是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap 维护一个Segment 数组, 一个 Segment 维护一个 HashEntry 数组。
static final class HashEntry<K,V> {
final int hash; final K key; volatile V value;
volatile HashEntry<K,V> next;
//其他省略
}
我们说 Segment 类似哈希表,那么一些属性就跟我们之前提到的 HashMap 差不离,比如负载因子
loadFactor,比如阈值 threshold 等等,看下 Segment 的构造方法

Segment(float lf, int threshold, HashEntry<K,V>[] tab) { this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即 HashEntry 数组
}
我们来看下 ConcurrentHashMap 的构造方法

public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException();
//MAX_SEGMENTS 为 1<<16=65536,也就是最大并发数为 65536
if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS;
//2 的 sshif 次方等于 ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
int sshift = 0;
//ssize 为 segments 数组长度,根据 concurrentLevel 计算得出
217

int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift; ssize <<= 1;
}
//segmentShift 和 segmentMask 这两个变量在定位 segment 时会用到,后面会详细讲
this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
//计算 cap 的大小,即 Segment 中 HashEntry 的数组长度,cap 也一定为 2 的 n 次方.
int c = initialCapacity / ssize; if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c) cap <<= 1;
//创建 segments 数组并初始化第一个 Segment,其余的 Segment 延迟初始化
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}

初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity 为 16,loadFactor 为 0.75(负载因子,扩容时需要参考),concurrentLevel 为 16。
从上面的代码可以看出来,Segment 数组的大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于concurrentLevel, ssize 一定是大于或等于 concurrentLevel 的最小的 2 的次幂。比如: 默认情况下concurrentLevel 是 16,则 ssize 为 16;若 concurrentLevel 为 14,ssize 为 16;若 concurrentLevel 为17,则 ssize 为 32。为什么 Segment 的数组大小一定是 2 的次幂?其实主要是便于通过按位与的散列算法来定位 Segment 的 index。
其实,put 方法对 segment 也会有所体现

public V put(K key, V value) {
Segment<K,V> s;
218

//concurrentHashMap 不允许 key/value 为空
if (value == null)
throw new NullPointerException();
//hash 函数对 key 的 hashCode 重新散列,避免差劲的不合理的 hashcode,保证散列均匀int hash = hash(key);
//返回的 hash 值无符号右移 segmentShift 位与段掩码进行位运算,定位 segment 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);
}

从源码看出,put 的主要逻辑也就两步:
1.定位 segment 并确保定位的 Segment 已初始化
2.调用 Segment 的 put 方法。
Ps:关于 segmentShift 和 segmentMask
segmentShift 和 segmentMask 这两个全局变量的主要作用是用来定位 Segment,int j =(hash >>> segmentShift) & segmentMask 。
segmentMask:段掩码,假如 segments 数组长度为 16,则段掩码为 16-1=15;segments 长度为
32,段掩码为 32-1=31。这样得到的所有 bit 位都为 1,可以更好地保证散列的均匀性
segmentShift:2 的 sshift 次方等于 ssize,segmentShift=32-sshift。若 segments 长度为 16, segmentShift=32-4=28;若 segments 长度为 32,segmentShift=32-5=27。而计算得出的 hash 值最大为32 位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位 Segment。
ConcurrentLinkedQuere 方法
Get 操作

public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
219

int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//先定位 Segment,再定位 HashEntry
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) { K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value;
}
}
return null;
}

get 方法无需加锁,由于其中涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以不会读取到过期数据。
来看下 concurrentHashMap 代理到 Segment 上的 put 方法,Segment 中的 put 方法是要加锁的。只不过是锁粒度细了而已。
Put 操作

final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//tryLock 不成功时会遍历定位到的 HashEnry 位置的链表
(遍历主要是为了使 CPU 缓存链表),若找不到,则创建 HashEntry。tryLock 一定次数后(MAX_SCAN_RETRIES 变量决定),则
lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
V oldValue; try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;//定位 HashEntry,可以看到,这个 hash 值在定位 Segment
时和在 Segment 中定位 HashEntry 都会用到,只不过定位 Segment 时只用到高几位。
HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;😉 {
if (e != null) { K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) { oldValue = e.value;
if (!onlyIfAbsent) {
220

e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null) node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1;
//若 c 超出阈值 threshold,需要扩容并 rehash。扩容后的容量是当前容量的 2 倍。这样可以最大程
度避免之前散列好的 entry 重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并 rehash 的这个过程是比较消耗资源的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node);
else
setEntryAt(tab, index, node);
++modCount; count = c; oldValue = null; break;
}
}
} finally {
unlock();
}
return oldValue;
}

ConcurrentLinkedQuere 总结
ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比
HashTable 的全表锁在性能上的提升非常之大。本文对 ConcurrentHashMap 的实现原理进行了详细分析,并解读了部分源码,希望能帮助到有需要的童鞋。
ConcurrentSkipListMap 非阻塞 Hash 跳表集合
大家都是知道 TreeMap,它是使用树形结构的方式进行存储数据的线程不安全的 Map 集合(有序的哈希表),

并且可以对 Map 中的 Key 进行排序,Key 中存储的数据需要实现 Comparator 接口或使用 CompareAble 接口的子类来实现排序。
ConcurrentSkipListMap 也是和 TreeMap,它们都是有序的哈希表。但是,它们是有区别的:
第一,它们的线程安全机制不同,TreeMap 是非线程安全的,而 ConcurrentSkipListMap 是线程安全的。第二,ConcurrentSkipListMap 是通过跳表实现的,而 TreeMap 是通过红黑树实现的。
那现在我们需要知道说明是跳表。

什么是 SkipList
Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照 Key 值升序的。Skip list 让已排序的数据分布在多层链表中,以 0-1 随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
从概率上保持数据结构的平衡比显示的保持数据结构平衡要简单的多。对于大多数应用,用 Skip list 要比用树算法相对简单。由于 Skip list 比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是 skip
list 的常数项会相对小很多。Skip list 在空间上也比较节省。一个节点平均只需要 1.333 个指针(甚至更少)。下图为 Skip list 结构图(以 7,14,21,32,37,71,85 序列为例)

SkipList 性质
(1)由很多层结构组成,level 是通过一定的概率随机产生的。

(2)每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
(3)最底层(Level 1)的链表包含所有元素。
(4)如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5)每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

什么是 ConcurrentSkipListMap
ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表。内部是 SkipList(跳表)结构实现, 在理论上能够在 O(log(n))时间内完成查找、插入、删除操作。 注意,调用 ConcurrentSkipListMap 的 size 时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个 O(log(n))的操作。
ConcurrentSkipListMap 数据结构
ConcurrentSkipListMap 的数据结构,如下图所示:

说明:可以看到 ConcurrentSkipListMap 的数据结构使用的是跳表,每一个 HeadIndex、Index 结点都会包含一个对 Node 的引用,同一垂直方向上的 Index、HeadIndex 结点都包含了最底层的 Node 结点的引用。并且层级越高,该层级的结点(HeadIndex 和 Index)数越少。Node 结点之间使用单链表结构。
ConcurrentSkipListMap 源码分析
ConcurrentSkipListMap 主要用到了 Node 和 Index 两种节点的存储方式,通过 volatile 关键字实现了并发的操

static final class Node<K,V> { final K key;
volatile Object value;//value 值
volatile Node<K,V> next;//next 引用
……
}
static class Index<K,V> {
final Node<K,V> node;
final Index<K,V> down;//downy 引用
volatile Index<K,V> right;//右边引用
……
}
ConcurrentSkipListMap 查找操作
通过 SkipList 的方式进行查找操作:(下图以“查找 91”进行说明:)

红色虚线,表示查找的路径,蓝色向右箭头表示 right 引用;黑色向下箭头表示 down 引用; 下面就是源码中的实现(get 方法是通过 doGet 方法来时实现的)

224

private V doGet(Object okey) {
Comparable<? super K> key = comparable(okey); Node<K,V> bound = null;
Index<K,V> q = head;//把头结点作为当前节点的前驱节点
Index<K,V> r = q.right;//前驱节点的右节点作为当前节点
Node<K,V> n;
K k;
int c;
for (;😉 {//遍历Index<K,V> d;
// 依次遍历 right 节点
if (r != null && (n = r.node) != bound && (k = n.key) != null) {
if ((c = key.compareTo(k)) > 0) {//由于 key 都是升序排列的,所有当前关键字大于所要查找的 key 时继续向右遍历
q = r;
r = r.right; continue;
} else if (c == 0) {
//如果找到了相等的 key 节点,则返回该 Node 的 value 如果 value 为空可能是其他并发 delete
导致的,于是通过另一种
//遍历 findNode 的方式再查找
Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else
bound = n;
}
//如果一个链表中 right 没能找到 key 对应的 value,则调整到其 down 的引用处继续查找
if ((d = q.down) != null) { q = d;
r = d.right;
} else
break;
}
// 如果通过上面的遍历方式,还没能找到 key 对应的 value,再通过 Node.next 的方式进行查找
for (n = q.node.next; n != null; n = n.next) { if ((k = n.key) != null) {
if ((c = key.compareTo(k)) == 0) { Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else if (c < 0) break;
}
}
225

return null;
}

ConcurrentSkipListMap 删除操作
通过 SkipList 的方式进行删除操作:(下图以“删除 23”进行说明:)

红色虚线,表示查找的路径,蓝色向右箭头表示 right 引用;黑色向下箭头表示 down 引用; 下面就是源码中的实现(remove 方法是通过 doRemove 方法来时实现的)

break;
}
if (v == n || b.value == null) // 当其前驱被 delet 的时候直接跳出,重新获取 b 和 n break;
int c = key.compareTo(n.key); if (c < 0)
return null;
if (c > 0) {//当 key 较大时就继续遍历
b = n; n = f;
continue;
}
if (value != null && !value.equals(v)) return null;
if (!n.casValue(v, null)) break;
if (!n.appendMarker(f) || !b.casNext(n, f))//casNext 方法就是通过比较和设置 b(前驱)
的 next 节点的方式来实现删除操作
findNode(key); // 通过尝试 findNode 的方式继续 find
else {
findPredecessor(key); // Clean index
if (head.right == null) //如果 head 的 right 引用为空,则表示不存在该 level tryReduceLevel();
}
return (V)v;
}
}
}

ConcurrentSkipListMap 插入操作
通过 SkipList 的方式进行插入操作:(下图以“添加 55”的两种情况,进行说明:)

在 level=2(该 level 存在)的情况下添加 55 的图示:只需在 level<=2 的合适位置插入 55 即可

在 level=4(该 level 不存在,图示 level4 是新建的)的情况下添加 55 的情况:首先新建 level4,然后在 level<=4
的合适位置插入 55。
下面就是源码中的实现(put 方法是通过 doPut 方法来时实现的)

//put 操作,通过 doPut 实现
public V put(K key, V value) { if (value == null)
throw new NullPointerException(); return doPut(key, value, false);
}
private V doPut(K kkey, V value, boolean onlyIfAbsent) { Comparable<? super K> key = comparable(kkey); for (;😉 {
Node<K,V> b = findPredecessor(key);//前驱
228

Node<K,V> n = b.next;
//定位的过程就是和 get 操作相似
for (;😉 {
if (n != null) { Node<K,V> f = n.next;
if (n != b.next) // 前后值不一致的情况下,跳转到第一层循环重新获得 b 和 n
break;;
Object v = n.value;
if (v == null) { // n 被 delete 的情况下
n.helpDelete(b, f); break;
}
if (v == n || b.value == null) // b 被 delete 的情况,重新获取 b 和 n break;
int c = key.compareTo(n.key); if (c > 0) {
b = n; n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) return (V)v;
else
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
Node<K,V> z = new Node<K,V>(kkey, value, n); if (!b.casNext(n, z))
break; // restart if lost race to append to b
int level = randomLevel();//得到一个随机的 level 作为该 key-value 插入的最高 level if (level > 0)
insertIndex(z, level);//进行插入操作return null;
}
}
}

/**

  • 获得一个随机的 level 值
    */
    private int randomLevel() {
    229

int x = randomSeed; x ^= x << 13;
x ^= x >>> 17;
randomSeed = x ^= x << 5;
if ((x & 0x8001) != 0) // test highest and lowest bits return 0;
int level = 1;
while (((x >>>= 1) & 1) != 0) ++level; return level;
}
//执行插入操作:如上图所示,有两种可能的情况:
//1.当 level 存在时,对 level<=n 都执行 insert 操作
//2.当 level 不存在(大于目前的最大 level)时,首先添加新的 level,然后在执行操作 1
private void insertIndex(Node<K,V> z, int level) { HeadIndex<K,V> h = head;
int max = h.level;
if (level <= max) {//情况 1 Index<K,V> idx = null;
for (int i = 1; i <= level; ++i)//首先得到一个包含 1~level 个级别的 down 关系的链表, 最后的 inx 为最高 level
idx = new Index<K,V>(z, idx, null);
addIndex(idx, h, level);//把最高 level 的 idx 传给 addIndex 方法
} else { // 情况 2 增加一个新的级别
level = max + 1;
Index<K,V>[] idxs = (Index<K,V>[])new Index[level+1]; Index<K,V> idx = null;
for (int i = 1; i <= level; ++i)//该步骤和情况 1 类似
idxs[i] = idx = new Index<K,V>(z, idx, null); HeadIndex<K,V> oldh;
int k;
for (;😉 {
oldh = head;
int oldLevel = oldh.level;
if (level <= oldLevel) { // lost race to add level k = level;
break;
}
HeadIndex<K,V> newh = oldh; Node<K,V> oldbase = oldh.node;
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);//创建新的if (casHead(oldh, newh)) {
k = oldLevel;
230

break;
}
}
addIndex(idxs[k], oldh, k);
}
}
/**
*在 1~indexlevel 层中插入数据
*/
private void addIndex(Index<K,V> idx, HeadIndex<K,V> h, int indexLevel) {
// insertionLevel 代表要插入的 level,该值会在 indexLevel~1 间遍历一遍
int insertionLevel = indexLevel;
Comparable<? super K> key = comparable(idx.node.key); if (key == null) throw new NullPointerException();
// 和 get 操作类似,不同的就是查找的同时在各个 level 上加入了对应的 key
for (;😉 {
int j = h.level; Index<K,V> q = h; Index<K,V> r = q.right; Index<K,V> t = idx;
for (;😉 {
if (r != null) { Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck int c = key.compareTo(n.key);
if (n.value == null) { if (!q.unlink®)
break;
r = q.right; continue;
}
if (c > 0) {
q = r;
r = r.right; continue;
}
}
if (j == insertionLevel) {//在该层 level 中执行插入操作
// Don’t insert index if node already deleted if (t.indexesDeletedNode()) {
findNode(key); // cleans up return;
}
231

if (!q.link(r, t))//执行 link 操作,其实就是 inset 的实现部分
break; // restart
if (–insertionLevel == 0) {
// need final deletion check before return if (t.indexesDeletedNode())
findNode(key); return;
}
}
if (–j >= insertionLevel && j < indexLevel)//key 移动到下一层 level t = t.down;
q = q.down; r = q.right;
}
}
}

ConcurrentLinkedQuere 示例
下面我们看下面示例输出的结果

import java.util.;import java.util.concurrent.;
/*
*ConcurrentSkipListMap 是“线程安全”的哈希表,而 TreeMap 是非线程安全的。
*
*下面是“多个线程同时操作并且遍历 map”的示例
*(01) 当 map 是 ConcurrentSkipListMap 对象时,程序能正常运行。
*(02) 当 map 是 TreeMap 对象时,程序会产生 ConcurrentModificationException 异常。
*
*/public class ConcurrentSkipListMapDemo1 {

// TODO: map 是 TreeMap 对象时,程序会出错。
//private static Map<String, String> map = new TreeMap<String, String>();
private static Map<String, String> map = new ConcurrentSkipListMap<String, String>(); public static void main(String[] args) {

// 同时启动两个线程对 map 进行操作!
new MyThread(“a”).start();
new MyThread(“b”).start();
}

private static void printAll() { String key, value;
Iterator iter = map.entrySet().iterator();

while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next(); key = (String)entry.getKey();
value = (String)entry.getValue(); System.out.print("("+key+", “+value+”), ");
}
System.out.println();
}

private static class MyThread extends Thread { MyThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0; while (i++ < 6) {
// “线程名” + “序号”
String val = Thread.currentThread().getName()+i; map.put(val, “0”);
// 通过“Iterator”遍历 map。
printAll();
}
}
}
}

某一次的运行结果:
(a1, 0), (a1, 0), (b1, 0), (b1, 0),
(a1, 0), (b1, 0), (b2, 0),
(a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0),
(b3, 0), (a1, 0),
(a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0),
(b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4,
0), (b2, 0), (b5, 0),
(b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3,
0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0),
(b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0),
(a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),

结果说明:
示例程序中,启动两个线程(线程 a 和线程 b)分别对 ConcurrentSkipListMap 进行操作。以线程 a 而言,它会先获取“线程名”+“序号”,然后将该字符串作为 key,将“0”作为 value,插入到 ConcurrentSkipListMap 中;接着,遍历并输出 ConcurrentSkipListMap 中的全部元素。 线程 b 的操作和线程 a 一样,只不过线程 b 的名字和线程
a 的名字不同。
当 map 是 ConcurrentSkipListMap 对象时,程序能正常运行。如果将 map 改为 TreeMap 时,程序会产生
ConcurrentModificationException 异常。

2)java.util.concurrent.atomic 包

AtomicBoolean 原子性布尔
AtomicBoolean 是 java.util.concurrent.atomic 包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
AtomicBoolean,在这个 Boolean 值的变化的时候不允许在之间插入,保持操作的原子性。下面将解释重点方法并举例:
boolean compareAndSet(expectedValue, updateValue),这个方法主要两个作用:
1.比较 AtomicBoolean 和 expect 的值,如果一致,执行方法内的语句。其实就是一个 if 语句
2.把 AtomicBoolean 的值设成 update,比较最要的是这两件事是一气呵成的,这连个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行。为多线程的控制提供了解决的方案

下面我们从代码上解释:

234

首先我们看下在不使用 AtomicBoolean 情况下,代码的运行情况:

package zmx.atomic.test;

import java.util.concurrent.TimeUnit;

public class BarWorker implements Runnable {
//静态变量
private static boolean exists = false;

private String name;

public BarWorker(String name) { this.name = name;
}

@Override
public void run() { if (!exists) {
try { TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
// do nothing
}
exists = true; System.out.println(name + " enter"); try {
System.out.println(name + " working"); TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave"); exists = false;
} else {
System.out.println(name + " give up");
}

}

public static void main(String[] args) {
235

BarWorker bar1 = new BarWorker(“bar1”); BarWorker bar2 = new BarWorker(“bar2”); new Thread(bar1).start();
new Thread(bar2).start();
}
}

运行结果:
bar1 enter bar2 enter bar1 working bar2 working bar1 leave
bar2 leave

从上面的运行结果我们可看到,两个线程运行时,都对静态变量 exists 同时做操作,并没有保证 exists 静态变量的原子性,也就是一个线程在对静态变量 exists 进行操作到时候,其他线程必须等待或不作为。等待一个线程操作完后,才能对其进行操作。
下面我们将静态变量使用 AtomicBoolean 来进行操作

package zmx.atomic.test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class BarWorker2 implements Runnable {
//静态变量使用 AtomicBoolean 进行操作
private static AtomicBoolean exists = new AtomicBoolean(false);

private String name;

public BarWorker2(String name) { this.name = name;
}

@Override
public void run() {
if (exists.compareAndSet(false, true)) {

System.out.println(name + " enter");
236

try {
System.out.println(name + " working"); TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave"); exists.set(false);
} else {
System.out.println(name + " give up");
}

}
public static void main(String[] args) { BarWorker2 bar1 = new BarWorker2(“bar1”); BarWorker2 bar2 = new BarWorker2(“bar2”); new Thread(bar1).start();
new Thread(bar2).start();
}
}

运行结果:
bar1 enter bar1 working bar2 give up
bar1 leave

可以从上面的运行结果看出仅仅一个线程进行工作,因为 exists.compareAndSet(false, true)提供了原子性操作, 比较和赋值操作组成了一个原子操作, 中间不会提供可乘之机。使得一个线程操作,其他线程等待或不作为。
下面我们简单介绍下 AtomicBoolean 的 API

创建一个 AtomicBoolean
你可以这样创建一个 AtomicBoolean:
AtomicBoolean atomicBoolean = new AtomicBoolean();
以上示例新建了一个默认值为 false 的 AtomicBoolean。如果你想要为 AtomicBoolean 实例设置一个显式的初始值,那么你可以将初始值传给 AtomicBoolean 的构造子:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);

237

获得 AtomicBoolean 的值
你可以通过使用 get() 方法来获取一个 AtomicBoolean 的值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();
设置 AtomicBoolean 的值
你可以通过使用 set() 方法来设置一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);
以上代码执行后 AtomicBoolean 的值为 false。

交换 AtomicBoolean 的值
你 可 以 通 过 getAndSet() 方 法 来 交 换 一 个 AtomicBoolean 实 例 的 值 。 getAndSet() 方 法 将 返 回
AtomicBoolean 当前的值,并将为 AtomicBoolean 设置一个新值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);
以 上 代 码 执 行 后 oldValue 变 量 的 值 为 true , atomicBoolean 实 例 将 持 有 false 值 。 代 码 成 功将
AtomicBoolean 当前值 ture 交换为 false。

比较并设置 AtomicBoolean 的值
compareAndSet() 方法允许你对 AtomicBoolean 的当前值与一个期望值进行比较,如果当前值等于期望值的话,将会对 AtomicBoolean 设定一个新值。compareAndSet() 方法是原子性的,因此在同一时间之内有单个线程执行它。因此 compareAndSet() 方法可被用于一些类似于锁的同步的简单实现。以下是一个
compareAndSet() 示例:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);

boolean expectedValue = true; boolean newValue = false;

boolean wasNewValueSet = atomicBoolean.compareAndSet(
expectedValue, newValue);

本示例对 AtomicBoolean 的当前值与 true 值进行比较,如果相等,将 AtomicBoolean 的值更新为 false

AtomicInteger 原子性整型
AtomicInteger,一个提供原子操作的 Integer 的类。在 Java 语言中,++i 和 i++操作并不是线程安全的, 在使用的时候,不可避免的会用到 synchronized 关键字。而 AtomicInteger 则通过一种线程安全的加减操作接口。
我们先来看看 AtomicInteger 给我们提供了什么方法:

public final int get() //获取当前的值

public final int getAndSet(int newValue)//获取当前的值,并设置新的值public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减

public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

下面通过两个简单的例子来看一下 AtomicInteger 的优势在哪: 普通线程同步:

使用 AtomicInteger:

class Test2 {
private AtomicInteger count = new AtomicInteger();

public void increment() {
count.incrementAndGet();
}
//使用 AtomicInteger 之后,不需要加锁,也可以实现线程安全。public int getCount() {
return count.get();
}
}
从上面的例子中我们可以看出:使用 AtomicInteger 是非常的安全的.而且因为 AtomicInteger 由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。AtomicInteger 是使用非阻塞算法来实现并发控制的。AtomicInteger 的关键域只有一下 3 个:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset;
static {
try {
valueOffset=
unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField(“value”));
} catch (Exception ex) {
throw new Error(ex);
}
}
private volatile int value;
这里, unsafe 是 java 提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是 AtomicInteger 中的一个工具。valueOffset 是用来记录 value 本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到 value 的位置,方便比较。
注意:value 是用来存储整数的时间变量,这里被声明为 volatile,就是为了保证在更新操作时,当前线程可以拿到 value 最新的值(并发环境下,value 可能已经被其他线程更新了)。
优点:最大的好处就是可以避免多线程的优先级倒置和死锁情况的发生,提升在高并发处理下的性能。
240

下面我们简单介绍下 AtomicInteger 的 API

创建一个 AtomicInteger
创建一个 AtomicInteger 示例如下:

AtomicInteger atomicInteger = new AtomicInteger();
本示例将创建一个初始值为 0 的 AtomicInteger。如果你想要创建一个给定初始值的 AtomicInteger,你可以这样:
AtomicInteger atomicInteger = new AtomicInteger(123);

本示例将 123 作为参数传给 AtomicInteger 的构造子,它将设置 AtomicInteger 实例的初始值为 123。

获得 AtomicInteger 的值
你可以使用 get() 方法获取 AtomicInteger 实例的值。示例如下:

AtomicInteger atomicInteger = new AtomicInteger(123);
int theValue = atomicInteger.get();

设置 AtomicInteger 的值
你可以通过 set() 方法对 AtomicInteger 的值进行重新设置。以下是 AtomicInteger.set() 示例:

AtomicInteger atomicInteger = new AtomicInteger(123);
atomicInteger.set(234);
以上示例创建了一个初始值为 123 的 AtomicInteger,而在第二行将其值更新为 234。

比较并设置 AtomicInteger 的值
AtomicInteger 类也通过了一个原子性的 compareAndSet() 方法。这一方法将 AtomicInteger 实例的当前值与期望值进行比较,如果二者相等,为 AtomicInteger 实例设置一个新值。AtomicInteger.compareAndSet() 代码示例:

本示例首先新建一个初始值为 123 的 AtomicInteger 实例。然后将 AtomicInteger 与期望值 123 进行比较, 如果相等,将 AtomicInteger 的值更新为 234。
增加 AtomicInteger 的值
AtomicInteger 类包含有一些方法,通过它们你可以增加 AtomicInteger 的值,并获取其值。这些方法如下:
public final int addAndGet(int addValue)//在原来的数值上增加新的值,并返回新值

public final int getAndIncrement()//获取当前的值,并自增public final int incrementAndget() //自减,并获得自减后的值
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
第一个 addAndGet() 方法给 AtomicInteger 增加了一个值,然后返回增加后的值。getAndAdd() 方法为
AtomicInteger 增加了一个值,但返回的是增加以前的 AtomicInteger 的值。具体使用哪一个取决于你的应用场景。以下是这两种方法的示例:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10)); System.out.println(atomicInteger.addAndGet(10));
本示例将打印出 0 和 20。例子中,第二行拿到的是加 10 之前的 AtomicInteger 的值。加 10 之前的值是 0。第三行将 AtomicInteger 的值再加 10,并返回加操作之后的值。该值现在是为 20。你当然也可以使用这俩方法为
AtomicInteger 添加负值。结果实际是一个减法操作。getAndIncrement() 和 incrementAndGet() 方法类似于
getAndAdd() 和 addAndGet(),但每次只将 AtomicInteger 的值加 1。

减小 AtomicInteger 的值
AtomicInteger 类还提供了一些减小 AtomicInteger 的值的原子性方法。这些方法是:

public final int decrementAndGet()
public final int getAndDecrement()
decrementAndGet() 将 AtomicInteger 的值减一,并返回减一后的值。getAndDecrement() 也将
AtomicInteger 的值减一,但它返回的是减一之前的值。

AtomicIntegerArray 原子性整型数组
java.util.concurrent.atomic.AtomicIntegerArray 类提供了可以以原子方式读取和写入的底层 int 数组的操作, 还包含高级原子操作。 AtomicIntegerArray 支持对底层 int 数组变量的原子操作。 它具有获取和设置方法,如在变量上的读取和写入。 也就是说,一个集合与同一变量上的任何后续 get 相关联。 原子 compareAndSet 方法也具有这些内存一致性功能。
AtomicIntegerArray 本质上是对 int[]类型的封装。使用 Unsafe 类通过 CAS 的方式控制 int[]在多线程下的安全性。它提供了以下几个核心 API:

下面给出一个简单的示例,展示 AtomicIntegerArray 使用:

public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10); public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++) arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException { Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
243

for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();} System.out.println(arr);
}
}

输出结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

上述代码第 2 行,申明了一个内含 10 个元素的数组。第 3 行定义的线程对数组内 10 个元素进行累加操作,每个
元素各加 1000 次。第 11 行,开启 10 个这样的线程。因此,可以预测,如果线程安全,数组内 10 个元素的值必然都是 10000。反之,如果线程不安全,则部分或者全部数值会小于 10000。
AtomicLong、AtomicLongArray 原子性整型数组
AtomicLong、AtomicLongArray 的 API 跟 AtomicInteger、AtomicIntegerArray 在使用方法都是差不多的。区别在于用前者是使用原子方式更新的 long 值和 long 数组,后者是使用原子方式更新的 Integer 值和 Integer 数组。两者的相同处在于它们此类确实扩展了 Number,允许那些处理基于数字类的工具和实用工具进行统一访问。在实际开发中,它们分别用于不同的场景。这个就具体情况具体分析了,下面将举例说明 AtomicLong 的使用场景(使用
AtomicLong 生成自增长 ID),其他就不在过多介绍。

import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

public class AtomicLongTest {

/**

  • @param args
    */
    public static void main(String[] args) {
    final AtomicLong orderIdGenerator = new AtomicLong(0); final List orders = Collections
    .synchronizedList(new ArrayList());
    244

for (int i = 0; i < 10; i++) {
Thread orderCreationThread = new Thread(new Runnable() { public void run() {
for (int i = 0; i < 10; i++) {
long orderId = orderIdGenerator.incrementAndGet();
Item order = new Item(Thread.currentThread().getName(), orderId);
orders.add(order);
}
}
});
orderCreationThread.setName(“Order Creation Thread " + i); orderCreationThread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
Set orderIds = new HashSet(); for (Item order : orders) {
orderIds.add(order.getID());
System.out.println(“Order name:” + order.getItemName()
+”----"+“Order id:” + order.getID());
}
}
}

class Item {
String itemName; long id;

Item(String n, long id) { this.itemName = n; this.id = id;
}

public String getItemName() { return itemName;
}

public long getID() {
245

return id;
}
}

输出:
Order name:Order Creation Thread 0----Order id:1 Order name:Order Creation Thread 1----Order id:2 Order name:Order Creation Thread 0----Order id:4 Order name:Order Creation Thread 1----Order id:5 Order name:Order Creation Thread 3----Order id:3 Order name:Order Creation Thread 0----Order id:7 Order name:Order Creation Thread 1----Order id:6

Order name:Order Creation Thread 2----Order id:100

从运行结果我们看到,不管是哪个线程。它们获得的 ID 是不会重复的,保证的 ID 生成的原子性,避免了线程安全上的问题。

  1. java.util.concurrent.lock 包

待续…

(三)多线程面试题

  1. 多线程的创建方式(2017-11-23-wzz)

(1)、继承 Thread 类:但 Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并复写 run()方法,就可以启动新线程并执行自己定义的 run()方法。例如:继承 Thread 类实现多线程,并在合适的地方启动线程

246

5.}	

6.MyThread myThread1 = new MyThread();
7.MyThread myThread2 = new MyThread(); 8.myThread1.start();
9.myThread2.start();

(2)、实现 Runnable 接口的方式实现多线程,并且实例化 Thread,传入自己的 Thread 实例,调用 run( )方法

1.public class MyThread implements Runnable {
2. public void run() {
3. System.out.println(“MyThread.run()”); 4. }
5.}
6.MyThread myThread = new MyThread(); 7.Thread thread = new Thread(myThread);
8.thread.start();
(3)、使用 ExecutorService、Callable、Future 实现有返回结果的多线程:ExecutorService、Callable、Future 这 个 对 象 实 际 上 都 是 属 于 Executor 框 架 中 的 功 能 类 。 想 要 详 细 了 解 Executor 框 架 的 可 以 访 问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在 JDK1.5 中引入
的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。可返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口
ExecutorService 就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在
JDK1.5 下验证过没问题可以直接使用。代码如下:

1.import java.util.concurrent.; 2.import java.util.Date; 3.import java.util.List; 4.import java.util.ArrayList; 5.
6./**
7.
有返回值的线程
8.*/
247

9.@SuppressWarnings(“unchecked”) 10.public class Test {
11.public static void main(String[] args) throws ExecutionException, 12. InterruptedException {
13. System.out.println("----程序开始运行----"); 14. Date date1 = new Date();
15.
16. int taskSize = 5; 17. // 创建一个线程池
18. ExecutorService pool = Executors.newFixedThreadPool(taskSize); 19. // 创建多个有返回值的任务
20. List list = new ArrayList(); 21. for (int i = 0; i < taskSize; i++) {
22. Callable c = new MyCallable(i + " “); 23. // 执行任务并获取 Future 对象
24. Future f = pool.submit©;
25. // System.out.println(”>>>" + f.get().toString()); 26. list.add(f);
27. }
28. // 关闭线程池
29. pool.shutdown();
30.
31. // 获取所有并发任务的运行结果
32. for (Future f : list) {
33. // 从 Future 对象上获取任务的返回值,并输出到控制台34. System.out.println(">>>" + f.get().toString()); 35. }
36.
37. Date date2 = new Date();
38. System.out.println("----程序结束运行----,程序运行时间【" 39. + (date2.getTime() - date1.getTime()) + “毫秒】”); 40.}
41.}
42.
43.class MyCallable implements Callable { 44.private String taskNum;
45.
46.MyCallable(String taskNum) { 47. this.taskNum = taskNum; 48.}
49.
50.public Object call() throws Exception {
51. System.out.println(">>>" + taskNum + “任务启动”);

52. Date dateTmp1 = new Date(); 53. Thread.sleep(1000);
54. Date dateTmp2 = new Date();
55. long time = dateTmp2.getTime() - dateTmp1.getTime(); 56. System.out.println(">>>" + taskNum + “任务终止”);
57. return taskNum + “任务返回运行结果,当前任务时间【” + time + “毫秒】”; 58.}
59.}

2.在 java 中 wait 和 sleep 方法的不同?

最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 通常被用于线程间交互,sleep 通常被用于暂停执行。
3.synchronized 和 volatile 关键字的作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
1.volatile 仅能使用在变量级别;
synchronized 则可以使用在变量、方法、和类级别的
2.volatile 仅能实现变量的修改可见性,并不能保证原子性;
synchronized 则可以保证变量的修改可见性和原子性
3.volatile 不会造成线程的阻塞;

synchronized 可能会造成线程的阻塞。
4.volatile 标记的变量不会被编译器优化;
synchronized 标记的变量可以被编译器优化

4.分析线程并发访问代码解释原因

1.public class Counter {
2.private volatile int count = 0;
3.public void inc(){
4.try {
5.Thread.sleep(3);
6.} catch (InterruptedException e) {
7.e.printStackTrace(); 8. }
9. count++;
10. }
11.@Override
12.public String toString() {
13.return “[count=” + count + “]”;
14. }
15. }
16. //---------------------------------华丽的分割线-----------------------------
17.public class VolatileTest {
18.public static void main(String[] args) {
19.final Counter counter = new Counter(); 20. for(int i=0;i<1000;i++){
21.new Thread(new Runnable() {
22.@Override
23.public void run() {
24.counter.inc();
25. }
26. }).start();
27. }
28. System.out.println(counter);
29. }
30. }

上面的代码执行完后输出的结果确定为 1000 吗?

答案是不一定,或者不等于 1000。这是为什么吗?
在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值 load 到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
也就是说上面主函数中开启了 1000 个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了
自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。因此结果就不可能等于 1000
了,一般都会小于 1000。
上面的解释用一张图表示如下:
(图片来自网络,非本人所绘)

5.什么是线程池,如何使用?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。

1.ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
2.ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
3.ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
4.ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的 execute 方法即可。

  1. 常用的线程池有哪些?(2017-11-23-wzz)

newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

7.请叙述一下您对线程池的理解?(2015-11-25)

(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略) 合理利用线程池能够带来三个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
8. 线程池的启动策略?(2015-11-25)

官方对线程池的执行过程描述如下:

  1. /*
    • Proceed in 3 steps:

29.* 1. If fewer than corePoolSize threads are running, try to
30.* start a new thread with the given command as its first
31.* task. The call to addWorker atomically checks runState and
32.* workerCount, and so prevents false alarms that would add
33.* threads when it shouldn’t, by returning false.
34. *
35.* 2. If a task can be successfully queued, then we still need
36.* to double-check whether we should have added a thread
37.* (because existing ones died since last checking) or that
38.* the pool shut down since entry into this method. So we
39.* recheck state and if necessary roll back the enqueuing if
40.* stopped, or start a new thread if there are none.
41. *
42.* 3. If we cannot queue task, then we try to add a new
43.* thread. If it fails, we know we are shut down or saturated
44.* and so reject the task.
45. */
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这

个任务;

d.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

  1. 如何控制某个方法允许并发访问线程的个数?(2015-11-30)

  2. package com.yange;

  3. import java.util.concurrent.Semaphore; 4. /**

    • @author wzy 2015-11-30
  4. */
    254

  5. public class SemaphoreTest { 10. /*
    11.* permits the initial number of permits available. This value may be negative,
    12.in which case releases must occur before any acquires will be granted.
    13.fair true if this semaphore will guarantee first-in first-out granting of
    14.permits under contention, else false 15. */
    16.static Semaphore semaphore = new Semaphore(5,true);
    17.public static void main(String[] args) { 18. for(int i=0;i<100;i++){

  6. new Thread(new Runnable() { 20.
    21.@Override
    22.public void run() {
    23.test();

  7. }

  8. }).start();

  9. }

  10. } 29.
    30.public static void test(){
    31.try {
    32.//申请一个请求
    33.semaphore.acquire();
    34.} catch (InterruptedException e1) {
    35.e1.printStackTrace();

  11. }
    37.System.out.println(Thread.currentThread().getName()+“进来了”);
    38.try {
    39.Thread.sleep(1000);
    40.} catch (InterruptedException e) {
    41.e.printStackTrace();

  12. }
    43.System.out.println(Thread.currentThread().getName()+“走了”);
    44.//释放一个请求
    45.semaphore.release();

  13. }

  14. }

可以使用 Semaphore 控制,第 16 行的构造函数创建了一个 Semaphore 对象,并且初始化了 5 个信号。这样的
效果是控件 test 方法最多只能有 5 个线程并发访问,对于 5 个线程时就排队等待,走一个来一下。第 33 行,请求一
255

个信号(消费一个信号),如果信号被用完了则等待,第 45 行释放一个信号,释放的信号新的线程就可以使用了。

  1. 三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现(上海 3 期学员提
    供)

根据问题的描述,我将问题用以下代码演示,ThreadA、ThreadB、ThreadC,ThreadA 用于初始化数据 num, 只有当 num 初始化完成之后再让 ThreadB 和 ThreadC 获取到初始化后的变量 num。
分析过程如下:
考虑到多线程的不确定性,因此我们不能确保 ThreadA 就一定先于 ThreadB 和 ThreadC 前执行,就算 ThreadA 先执行了,我们也无法保证 ThreadA 什么时候才能将变量 num 给初始化完成。因此我们必须让 ThreadB 和 ThreadC 去等待 ThreadA 完成任何后发出的消息。
现在需要解决两个难题,一是让 ThreadB 和 ThreadC 等待 ThreadA 先执行完,二是 ThreadA 执行完之后给
ThreadB 和 ThreadC 发送消息。
解决上面的难题我能想到的两种方案,一是使用纯 Java API 的 Semaphore 类来控制线程的等待和释放,二是使用 Android 提供的 Handler 消息机制。

  1. package com.example;
  2. /**
    • 三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现(上海 3 期学员提供)
  3. */
    6.public class ThreadCommunication {
    7.private static int num;//定义一个变量作为数据8.
  4. public static void main(String[] args) { 10.
  5. Thread threadA = new Thread(new Runnable() { 12.
    13.@Override
    14.public void run() {
    15.try {
    256

16.//模拟耗时操作之后初始化变量 num
17.Thread.sleep(1000);
18.num = 1;
19.
20.} catch (InterruptedException e) {
21.e.printStackTrace();
22. }
23. }
24. });
25. Thread threadB = new Thread(new Runnable() { 26.
27.@Override
28.public void run() {
29.System.out.println(Thread.currentThread().getName()+“获取到 num 的值为:”+num);
30. }
31. });
32. Thread threadC = new Thread(new Runnable() { 33.
34.@Override
35.public void run() {
36.System.out.println(Thread.currentThread().getName()+“获取到 num 的值为:”+num);
37. }
38. });
39.//同时开启 3 个线程
40.threadA.start();
41.threadB.start();
42.threadC.start();
43.
44. }
45. } 46.

解决方案一:

1.public class ThreadCommunication {
2.private static int num; 3. /**
4.* 定义一个信号量,该类内部维持了多个线程锁,可以阻塞多个线程,释放多个线程,
5.线程的阻塞和释放是通过 permit 概念来实现的
6.* 线程通过 semaphore.acquire()方法获取 permit,如果当前 semaphore 有 permit 则分配给该线程,
7.如果没有则阻塞该线程直到 semaphore
8.* 调用 release()方法释放 permit。
9.* 构造函数中参数:permit(允许) 个数,
10. */
257

11.private static Semaphore semaphore = new Semaphore(0);
12.public static void main(String[] args) { 13.
14. Thread threadA = new Thread(new Runnable() { 15.
16.@Override
17.public void run() {
18.try {
19.//模拟耗时操作之后初始化变量 num
20.Thread.sleep(1000);
21.num = 1;
22.//初始化完参数后释放两个 permit
23.semaphore.release(2); 24.
25.} catch (InterruptedException e) {
26.e.printStackTrace();
27. }
28. }
29. });
30. Thread threadB = new Thread(new Runnable() { 31.
32.@Override
33.public void run() {
34.try {
35.//获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个
36.semaphore.acquire();
37.} catch (InterruptedException e) {
38.e.printStackTrace();
39. }
40. System.out.println(Thread.currentThread().getName()+“获取到 num 的值为:”+num);
41. }
42. });
43. Thread threadC = new Thread(new Runnable() { 44.
45.@Override
46.public void run() {
47.try {
48.//获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个
49.semaphore.acquire();
50.} catch (InterruptedException e) {
51.e.printStackTrace();
52. }
53. System.out.println(Thread.currentThread().getName()+“获取到 num 的值为:”+num);
258

  1. }

  2. });
    56.//同时开启 3 个线程
    57.threadA.start();
    58.threadB.start();
    59.threadC.start();

  3. }

  4. }

  5. 同一个类中的 2 个方法都加了同步锁,多个线程能同时访问同一个类中的这两
    个方法吗?(2017-2-24)

这个问题需要考虑到Lock 与synchronized 两种实现锁的不同情形。因为这种情况下使用Lock 和synchronized 会有截然不同的结果。Lock 可以让等待锁的线程响应中断,Lock 获取锁,之后需要释放锁。如下代码,多个线程不可访问同一个类中的 2 个加了 Lock 锁的方法。
63.package com;
64.import java.util.concurrent.locks.Lock;
65.import java.util.concurrent.locks.ReentrantLock;
66.public class qq { 67.
68.private int count = 0;
69.private Lock lock = new ReentrantLock();//设置 lock 锁
70. //方法 1
71.public Runnable run1 = new Runnable(){
72.public void run() {
73.lock.lock(); //加锁
74.while(count < 1000) {
75.try {
76.//打印是否执行该方法
77.System.out.println(Thread.currentThread().getName() + " run1: "+count++);
78.} catch (Exception e) {
79.e.printStackTrace();
80. }
81. }
82. }
83. lock.unlock(); 84. }};
259

  1. //方法 2
    86.public Runnable run2 = new Runnable(){
    87.public void run() {
    88.lock.lock();
    89.while(count < 1000) {
    90.try {
    91.System.out.println(Thread.currentThread().getName() +
  2. " run2: "+count++);
    93.} catch (Exception e) {
    94.e.printStackTrace();
  3. }
  4. }
  5. lock.unlock(); 98.	}};
    

102.public static void main(String[] args) throws InterruptedException {
103.qq t = new qq(); //创建一个对象
104.new Thread(t.run1).start();//获取该对象的方法 1
105.
106.new Thread(t.run2).start();//获取该对象的方法 2
107. }
108. }
结果是:
Thread-0 run1: 0
Thread-0 run1: 1
Thread-0 run1: 2
Thread-0 run1: 3
Thread-0 run1: 4
Thread-0 run1: 5
Thread-0 run1: 6

而 synchronized 却不行,使用 synchronized 时,当我们访问同一个类对象的时候,是同一把锁,所以可以访问该对象的其他 synchronized 方法。代码如下:

7. public Runnable run1 = new Runnable(){ 8. public void run() {
9. synchronized(this) { //设置关键字 synchronized,以当前类为锁10. while(count < 1000) {
11. try {
12. //打印是否执行该方法
13. System.out.println(Thread.currentThread().getName() + " run1: "+count++); 14. } catch (Exception e) {
15. e.printStackTrace(); 16. }
17. }
18. }
19. }};
20. public Runnable run2 = new Runnable(){ 21. public void run() {
22. synchronized(this) { 23. while(count < 1000) { 24. try {
25. System.out.println(Thread.currentThread().getName() 26. + " run2: "+count++);
27. } catch (Exception e) {
28. e.printStackTrace(); 29. }
30. }
31. }
32. }};
33. public static void main(String[] args) throws InterruptedException { 34. qq t = new qq(); //创建一个对象
35. new Thread(t.run1).start(); //获取该对象的方法 1 36. new Thread(t.run2).start(); //获取该对象的方法 2 37. }
38.}
结果为:
Thread-1 run2: 0
Thread-1 run2: 1
Thread-1 run2: 2
Thread-0 run1: 0
Thread-0 run1: 4 Thread-0 run1: 5 Thread-0 run1: 6

12.什么情况下导致线程死锁,遇到线程死锁该怎么解决?(2017-2-24)

11.1死锁的定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
11.2死锁产生的必要条件:
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有, 此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1)占有(i=0, 1, …, n-1), Pn 等待的资源被 P0 占有,如图 2-15 所示。

11.3产生死锁的一个例子

1.package itheima.com;
2./**
3.* 一个简单的死锁类
4.* 当 DeadLock 类的对象 flag1 时(td1),先锁定 o1,睡眠 500 毫秒
5.* 而 td1 在睡眠的时候另一个 flag
0 的对象(td2)线程启动,先锁定 o2,睡眠 500 毫秒
262

6.* td1 睡眠结束后需要锁定 o2 才能继续执行,而此时 o2 已被 td2 锁定;
7.* td2 睡眠结束后需要锁定 o1 才能继续执行,而此时 o1 已被 td1 锁定;
8.* td1、td2 相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
9.*/
10.public class DeadLock implements Runnable { 11. public int flag = 1;
12. //静态对象是类的所有对象共享的
13. private static Object o1 = new Object(), o2 = new Object(); 14. public void run() {
15. System.out.println(“flag=” + flag); 16. if (flag == 1) {
17. synchronized (o1) { 18. try {
19. Thread.sleep(500);
20. } catch (Exception e) {
21. e.printStackTrace(); 22. }
23. synchronized (o2) {
24. System.out.println(“1”); 25. }
26. }
27. }
28. if (flag == 0) {
29. synchronized (o2) { 30. try {
31. Thread.sleep(500);
32. } catch (Exception e) {
33. e.printStackTrace(); 34. }
35. synchronized (o1) {
36. System.out.println(“0”); 37. }
38. }
39. }
40. }
41. public static void main(String[] args) { 42. DeadLock td1 = new DeadLock();
43. DeadLock td2 = new DeadLock(); 44. td1.flag = 1;
45. td2.flag = 0;
46. //td1,td2 都处于可执行状态,但 JVM 线程调度先执行哪个线程是不确定的。
47. //td2 的 run()可能在 td1 的 run()之前运行
48. new Thread(td1).start();
263

49. new Thread(td2).start(); 50. }
51.}

11.4如何避免死锁
在有些情况下死锁是可以避免的。两种用于避免死锁的技术:
1)加锁顺序(线程按照一定的顺序加锁)

1.package itheima.com; 2.public class DeadLock { 3. public int flag = 1;
4. //静态对象是类的所有对象共享的
5. private static Object o1 = new Object(), o2 = new Object(); 6. public void money(int flag) {
7. this.flag=flag;
8. if( flag ==1){
9. synchronized (o1) { 10. try {
11. Thread.sleep(500);
12. } catch (Exception e) {
13. e.printStackTrace(); 14. }
15. synchronized (o2) {
16. System.out.println(“当前的线程是”+
17. Thread.currentThread().getName()+" “+“flag 的值”+“1”); 18. }
19. }
20. }
21. if(flag ==0){
22. synchronized (o2) { 23. try {
24. Thread.sleep(500);
25. } catch (Exception e) {
26. e.printStackTrace(); 27. }
28. synchronized (o1) {
29. System.out.println(“当前的线程是”+
30. Thread.currentThread().getName()+” "+“flag 的值”+“0”); 31. }
32. }
33. }
34. }
264

35.
36. public static void main(String[] args) { 37. final DeadLock td1 = new DeadLock(); 38. final DeadLock td2 = new DeadLock(); 39. td1.flag = 1;
40. td2.flag = 0;
41. //td1,td2 都处于可执行状态,但 JVM 线程调度先执行哪个线程是不确定的。
42. //td2 的 run()可能在 td1 的 run()之前运行
43. final Thread t1=new Thread(new Runnable(){ 44. public void run() {
45. td1.flag = 1;
46. td1.money(1); 47. }
48. });
49. t1.start();
50. Thread t2= new Thread(new Runnable(){ 51. public void run() {
52. // TODO Auto-generated method stub
53. try {
54. //让 t2 等待 t1 执行完
55. t1.join();//核心代码,让 t1 执行完后 t2 才会执行
56. } catch (InterruptedException e) { 57. // TODO Auto-generated catch block
58. e.printStackTrace(); 59. }
60. td2.flag = 0;
61. td1.money(0); 62. }
63. });
64. t2.start();
65. }
66.}

结果:

当前的线程是 Thread-0 flag 的值 1
当前的线程是 Thread-1 flag 的值 0
2)加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

1. package itheima.com;
2.import java.util.concurrent.TimeUnit; 3.import java.util.concurrent.locks.Lock;
4.import java.util.concurrent.locks.ReentrantLock;
5.public class DeadLock {
265

6. public int flag = 1;
7. //静态对象是类的所有对象共享的
8. private static Object o1 = new Object(), o2 = new Object(); 9. public void money(int flag) throws InterruptedException { 10. this.flag=flag;
11. if( flag ==1){
12. synchronized (o1) {
13. Thread.sleep(500); 14. synchronized (o2) {
15. System.out.println(“当前的线程是”+
16. Thread.currentThread().getName()+" “+“flag 的值”+“1”); 17. }
18. }
19. }
20. if(flag ==0){
21. synchronized (o2) {
22. Thread.sleep(500); 23. synchronized (o1) {
24. System.out.println(“当前的线程是”+
25. Thread.currentThread().getName()+” "+“flag 的值”+“0”); 26. }
27. }
28. }
29. }
30.
31. public static void main(String[] args) { 32. final Lock lock = new ReentrantLock(); 33. final DeadLock td1 = new DeadLock(); 34. final DeadLock td2 = new DeadLock(); 35. td1.flag = 1;
36. td2.flag = 0;
37. //td1,td2 都处于可执行状态,但 JVM 线程调度先执行哪个线程是不确定的。
38. //td2 的 run()可能在 td1 的 run()之前运行39.
40. final Thread t1=new Thread(new Runnable(){ 41. public void run() {
42. // TODO Auto-generated method stub
43. String tName = Thread.currentThread().getName(); 44.
45. td1.flag = 1;
46. try {
47. //获取不到锁,就等 5 秒,如果 5 秒后还是获取不到就返回 false
48. if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
266

49. System.out.println(tName + “获取到锁!”); 50. } else {
51. System.out.println(tName + “获取不到锁!”);
52. return;
53. }
54. } catch (Exception e) {
55. e.printStackTrace(); 56. }
57.
58. try {
59. td1.money(1);
60. } catch (Exception e) {
61. System.out.println(tName + “出错了!!!”); 62. } finally {
63. System.out.println(“当前的线程是”+Thread.currentThread().getName()+"释放锁!!
");
64. lock.unlock(); 65. }
66. }
67. });
68. t1.start();
69. Thread t2= new Thread(new Runnable(){ 70. public void run() {
71. String tName = Thread.currentThread().getName(); 72. // TODO Auto-generated method stub
73. td1.flag = 1;
74. try {
75. //获取不到锁,就等 5 秒,如果 5 秒后还是获取不到就返回 false
76. if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) { 77. System.out.println(tName + “获取到锁!”);
78. } else {
79. System.out.println(tName + “获取不到锁!”);
80. return;
81. }
82. } catch (Exception e) {
83. e.printStackTrace(); 84. }
85. try {
86. td2.money(0);
87. } catch (Exception e) {
88. System.out.println(tName + “出错了!!!”); 89. } finally {
90. System.out.println(“当前的线程是”+Thread.currentThread().getName()+"释放锁!!
267

");
91. lock.unlock(); 92. }
93. }
94. });
95. t2.start();
96. }
97.}

打印结果:

Thread-0 获取到锁!
当前的线程是 Thread-0 flag 的值 1 当前的线程是 Thread-0 释放锁!! Thread-1 获取到锁!
当前的线程是 Thread-1 flag 的值 0
当前的线程是 Thread-1 释放锁!!

13.Java 中多线程间的通信怎么实现?(2017-2-24)

线程通信的方式:
1.共享变量
线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值。线程 A 在一个同步块里设置 boolean 型成员变量 hasDataToProcess 为 true,线程 B 也在同步块里读取 hasDataToProcess 这个成员变量。这个简单的例子使用了一个持有信号的对象,并提供了 set 和 get 方法:

268

12. }
13. public static void main(String[] args){ 14. //同一个对象
15. final MySignal my=new MySignal(); 16. //线程 1 设置 hasDataToProcess 值为 true
17. final Thread t1=new Thread(new Runnable(){ 18. public void run() {
19. my.setHasDataToProcess(true); 20. }
21. });
22. t1.start();
23. //线程 2 取这个值 hasDataToProcess
24. Thread t2=new Thread(new Runnable(){ 25. public void run() {
26. try {
27. //等待线程 1 完成然后取值
28. t1.join();
29. } catch (InterruptedException e) { 30. e.printStackTrace();
31. }
32. my.getHasDataToProcess();
33. System.out.println(“t1 改变以后的值:” + my.isHasDataToProcess()); 34. }
35. });
36. t2.start();
37.}
38.}
结果:
t1 改变以后的值:true

2.wait/notify 机制
以资源为例,生产者生产一个资源,通知消费者就消费掉一个资源,生产者继续生产资源,消费者消费资源,以此循环。代码如下:

269

9. while(flag) { 10. try{
11. //线程等待。消费者消费资源
12. wait();
13. }catch(Exception e){} 14. }
15. this.name=name+"—"+count++;
16. System.out.println(Thread.currentThread().getName()+"…生产者…"+this.name); 17. flag=true;
18. //唤醒等待中的消费者
19. this.notifyAll();
20. }
21. public synchronized void out(){ 22. //消费资源
23. while(!flag) {
24. //线程等待,生产者生产资源
25. try{wait();}catch(Exception e){} 26. }
27. System.out.println(Thread.currentThread().getName()+"…消费者…"+this.name); 28. flag=false;
29. //唤醒生产者,生产资源
30. this.notifyAll();
31. }
32.}
33. //生产者
34. class Producer implements Runnable{ 35. private Resource res;
36. Producer(Resource res){ 37. this.res=res;
38. }
39. //生产者生产资源 40. public void run(){ 41. while(true){
42. res.set(“商品”); 43. }
44. }
45. }
46. //消费者消费资源
47. class Consumer implements Runnable{ 48. private Resource res;
49. Consumer(Resource res){ 50. this.res=res;
51. }
270

52. public void run(){ 53. while(true){
54. res.out(); 55. }
56. }
57. }
58.public class ProducerConsumerDemo{
59. public static void main(String[] args){ 60. Resource r=new Resource();
61. Producer pro=new Producer®; 62. Consumer con=new Consumer®; 63. Thread t1=new Thread(pro); 64. Thread t2=new Thread(con); 65. t1.start();
66. t2.start();
67. }
68.}

  1. 线程和进程的区别(2017-11-23-wzz)

进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。

  1. 请说出同步线程及线程调度相关的方法?(2017-11-23-wzz)

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,

271

而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意:java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock() 方法),增强了多线程编程的灵活性及对线程的协调
16. 启动一个线程是调用 run()方法还是 start()方法?(2017-11-23-wzz)

启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。
run()方法是线程启动后要进行回调(callback)的方法。

十、Java 内部类
1.静态嵌套类 (Static Nested Class) 和内部类(Inner Class)的不同?(2017- 11-16-wl)

静态嵌套类:Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。
内部类:需要在外部类实例化后才能实例化,其语法看起来挺诡异的。
2.下面的代码哪些地方会产生编译错误?(2017-11-16-wl)

1.class Outer {
2.
3.class Inner {} 4.
5.public static void foo() { new Inner(); } 6.
7.public void bar() { new Inner(); }

272

8.
9.public static void main(String[] args) { 10.new Inner();
11.}
12.}

注意:Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo 和 main 方法都是静态方法,静态方法中没有 this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做
1.new Outer().new Inner();

第三章 JavaSE 高级
一、Java 中的反射
1.说说你对 Java 中反射的理解

Java 中 的 反 射 首 先 是 能 够 获 取 到 Java 中 要 反 射 类 的 字 节 码 , 获 取 字 节 码 有 三 种 方 法 , 1.Class.forName(className) 2.类名.class 3.this.getClass()。然后将字节码中的方法,变量,构造函数等映射成相应的 Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
二、Java 中的动态代理
1.写一个 ArrayList 的动态代理类(笔试题)

  1. final List list = new ArrayList(); 2.
    3.List proxyInstance =
    4.(List)Proxy.newProxyInstance(list.getClass().getClassLoader(),
    5.list.getClass().getInterfaces(),
    6.new InvocationHandler() {

8.@Override
9.public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
10.return method.invoke(list, args);
11. }
12. });
13.proxyInstance.add(“你好”);
14.System.out.println(list);

2.动静态代理的区别,什么场景使用?(2015-11-25)

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
动态代理是实现 JDK 里的 InvocationHandler 接口的 invoke 方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过 Proxy 里的 newProxyInstance 得到代理对象。
还有一种动态代理 CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。
AOP 编程就是基于动态代理实现的,比如著名的 Spring 框架、Hibernate 框架等等都是动态代理的使用例子。

三、Java 中的设计模式&回收机制

  1. 你所知道的设计模式有哪些

Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

274

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
2. 单例设计模式

最好理解的一种设计模式,分为懒汉式和饿汉式。
饿汉式:

1.public class Singleton {
2.// 直接创建对象
3.public static Singleton instance = new Singleton(); 4.
5.// 私有化构造函数
6.private Singleton() { 7. }
8.
9.// 返回对象实例
10.public static Singleton getInstance() {
11.return instance;
12. }
13. }

懒汉式:

1.public class Singleton {
2.// 声明变量
3.private static volatile Singleton singleton = null; 4.
5.// 私有构造函数
6.private Singleton() { 7. }
8.
9.// 提供对外方法
10.public static Singleton getInstance() {
11.if (singleton == null) {
12.synchronized (Singleton.class) {
13.if (singleton == null) {
14.singleton = new Singleton();
275

  1. }

  2. }

  3. }

  4. return singleton;

  5. }

  6. }

  7. 工厂设计模式

工厂模式分为工厂方法模式和抽象工厂模式。工厂方法模式
工厂方法模式分为三种:普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能
正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。普通工厂模式
1.public interface Sender {
2.public void Send();
3. }
4. public class MailSender implements Sender { 5.
6.@Override
7.public void Send() {
8.System.out.println(“this is mail sender!”); 9. }
10. }
11. public class SmsSender implements Sender { 12.
13.@Override
14.public void Send() {
15.System.out.println(“this is sms sender!”);
276

  1. }
  2. }
    18.public class SendFactory {
    19.public Sender produce(String type) {
    20.if (“mail”.equals(type)) {
    21.return new MailSender();
    22.} else if (“sms”.equals(type)) {
    23.return new SmsSender();
    24.} else {
    25.System.out.println(“请输入正确的类型!”);
    26.return null;
  3. }
  4. }
  5. }

多个工厂方法模式

该模式是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
1.public class SendFactory {
2.public Sender produceMail(){
3.return new MailSender(); 4. }
5.
6.public Sender produceSms(){
7.return new SmsSender(); 8. }
9. }
10.
11.public class FactoryTest {
12.public static void main(String[] args) {
13.SendFactory factory = new SendFactory();
14.Sender sender = factory.produceMail();
15.sender.send();
16. }
17. }
静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

1.public class SendFactory {
2.public static Sender produceMail(){
277

  1.  return new MailSender(); 4.	}
    

6.public static Sender produceSms(){
7.return new SmsSender(); 8. }
9. }
10.
11.
12.public class FactoryTest {
13.public static void main(String[] args) {
14.Sender sender = SendFactory.produceMail();
15.sender.send();
16. }
17. }

抽象工厂模式
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修

改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

278

19.@Override
20.public void send() {
21.System.out.println(“this is sms sender!”);
22. }
23. }
24. -------------------------------------------------------------------------------------
25. public class SendSmsFactory implements Provider { 26.
27.@Override
28.public Sender produce() {
29.return new SmsSender();
30. }
31. }

  1. public class SendMailFactory implements Provider { 2.
    3.@Override
    4.public Sender produce() {
    5.return new MailSender(); 6. }
  2. }

9.public class Test {
10.public static void main(String[] args) {
11.Provider provider = new SendMailFactory();
12.Sender sender = provider.produce();
13.sender.send();
14. }
15. }

  1. 建造者模式(Builder)

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象, 所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的 Test 结合起来得到的。
1.public class Builder {
2.private List list = new ArrayList(); 3.
279

4.public void produceMailSender(int count) {
5.for (int i = 0; i < count; i++) {
6.list.add(new MailSender()); 7. }
8. }
9.
10.public void produceSmsSender(int count) {
11.for (int i = 0; i < count; i++) {
12.list.add(new SmsSender());
13. }
14. }
15. }

1.public class Builder {
2.private List list = new ArrayList(); 3.
4.public void produceMailSender(int count) {
5.for (int i = 0; i < count; i++) {
6.list.add(new MailSender()); 7. }
8. }
9.
10.public void produceSmsSender(int count) {
11.for (int i = 0; i < count; i++) {
12.list.add(new SmsSender());
13. }
14. }
15. }

1.public class TestBuilder {
2.public static void main(String[] args) {
3.Builder builder = new Builder();
4.builder.produceMailSender(10); 5. }
6. }

  1. 适配器设计模式

适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容

280

性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
类的适配器模式

1.public class Source {
2.public void method1() {
3.System.out.println(“this is original method!”); 4. }
5. }
6. -------------------------------------------------------------
7.public interface Targetable {
8./* 与原类中的方法相同 /
9.public void method1();
10./
新类的方法 */
11.public void method2();
12. }
13.public class Adapter extends Source implements Targetable {
14.@Override
15.public void method2() {
16.System.out.println(“this is the targetable method!”);
17. }
18. }
19.public class AdapterTest {
20.public static void main(String[] args) {
21.Targetable target = new Adapter();
22.target.method1();
23.target.method2();
24. }
25. }

对象的适配器模式

基本思路和类的适配器模式相同,只是将 Adapter 类作修改,这次不继承 Source 类,而是持有 Source 类的实例,以达到解决兼容性的问题。

5.super();
6.this.source = source; 7. }
8.
9.@Override
10.public void method2() {
11.System.out.println(“this is the targetable method!”);
12. } 13.
14.@Override
15.public void method1() {
16.source.method1();
17. }
18. }
19. --------------------------------------------------------------
20. public class AdapterTest { 21.
22.public static void main(String[] args) {
23.Source source = new Source();
24.Targetable target = new Wrapper(source);
25.target.method1();
26.target.method2();
27. }
28. }

接口的适配器模式

接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。
6.装饰模式(Decorator)

顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个

接口,装饰对象持有被装饰对象的实例。

1.public interface Sourceable {
2.public void method(); 3. }
4. ----------------------------------------------------
5.public class Source implements Sourceable {
6.@Override
7.public void method() {
8.System.out.println(“the original method!”); 9. }
10. }
11. ----------------------------------------------------
12.public class Decorator implements Sourceable {
13.private Sourceable source;
14.public Decorator(Sourceable source) {
15.super();
16.this.source = source;
17. } 18.
19.@Override
20.public void method() {
21.System.out.println(“before decorator!”);
22.source.method();
23.System.out.println(“after decorator!”);
24. }
25. }
26. ----------------------------------------------------
27.public class DecoratorTest {
28.public static void main(String[] args) {
29.Sourceable source = new Source();
30.Sourceable obj = new Decorator(source);
31.obj.method();
32. }
33. }

7.策略模式(strategy)

策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法

283

的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无, 属于辅助类),提供辅助函数。策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
1.public interface ICalculator {
2.public int calculate(String exp); 3. }
4. ---------------------------------------------------------
5. public class Minus extends AbstractCalculator implements ICalculator { 6.
7.@Override
8.public int calculate(String exp) {
9.int arrayInt[] = split(exp, “-”);
10.return arrayInt[0] - arrayInt[1];
11. }
12. }
13. ---------------------------------------------------------
14. public class Plus extends AbstractCalculator implements ICalculator { 15.
16.@Override
17.public int calculate(String exp) {
18.int arrayInt[] = split(exp, “\+”);
19.return arrayInt[0] + arrayInt[1];
20. }
21. }
22. --------------------------------------------------------
23.public class AbstractCalculator {
24.public int[] split(String exp, String opt) {
25.String array[] = exp.split(opt);
26.int arrayInt[] = new int[2];
27.arrayInt[0] = Integer.parseInt(array[0]);
28.arrayInt[1] = Integer.parseInt(array[1]);
29.return arrayInt;
30. }
31. }

1.public class StrategyTest {
2.public static void main(String[] args) {
3.String exp = “2+8”;
4.ICalculator cal = new Plus();
284

5.int result = cal.calculate(exp);
6.System.out.println(result); 7. }
8. }

  1. 观察者模式(Observer)

观察者模式很好理解,类似于邮件订阅和 RSS 订阅,当我们浏览一些博客或 wiki 时,经常会看到 RSS 图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。
1.public interface Observer {
2.public void update(); 3. }
4.
5.public class Observer1 implements Observer {
6.@Override
7.public void update() {
8.System.out.println(“observer1 has received!”); 9. }
10. } 11.
12.public class Observer2 implements Observer {
13.@Override
14.public void update() {
15.System.out.println(“observer2 has received!”);
16. }
17. } 18.
19.public interface Subject {
20./增加观察者/
21.public void add(Observer observer); 22.
23./删除观察者/
24.public void del(Observer observer);
25./通知所有的观察者/

  1. public void notifyObservers();

285

3./自身的操作/
4.public void operation(); 5. }
6.
7. public abstract class AbstractSubject implements Subject { 8.
9. private Vector vector = new Vector(); 10.
11.@Override
12.public void add(Observer observer) {
13.vector.add(observer);
14. } 15.
16.@Override
17.public void del(Observer observer) {
18.vector.remove(observer);
19. } 20.
21.@Override
22.public void notifyObservers() {
23.Enumeration enumo = vector.elements();
24.while (enumo.hasMoreElements()) {
25.enumo.nextElement().update();
26. }
27. }
28. } 29.
30. public class MySubject extends AbstractSubject { 31.
32.@Override
33.public void operation() {
34.System.out.println(“update self!”);
35.notifyObservers();
36. }
37. } 38.
39.public class ObserverTest {
40.public static void main(String[] args) {
41.Subject sub = new MySubject();
42.sub.add(new Observer1());
43.sub.add(new Observer2());
44.sub.operation();
45. }
286

46. }	

9.JVM 垃圾回收机制和常见算法

理论上来讲 Sun 公司只定义了垃圾回收机制规则而不局限于其实现算法,因此不同厂商生产的虚拟机采用的算法也不尽相同。
GC(Garbage Collector)在回收对象前首先必须发现那些无用的对象,如何去发现定位这些无用的对象?常用的搜索算法如下:
1)引用计数器算法(废弃)
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候, 计数器-1,当计数器为 0 的时候,JVM 就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但是不能解决循环引用问问题(A 对象引用 B 对象,B 对象又引用 A 对象,但是A,B 对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在 JDK1.1 之后, 这个算法已经不再使用了。
2)根搜索算法(使用)
根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链
(Reference Chain),当一个对象没有被 GC Roots 的引用链连接的时候,说明这个对象是不可用的。

GC Roots 对象包括:
a)虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b)方法区域中的类静态属性引用的对象。
c)方法区域中常量引用的对象。
d)本地方法栈中 JNI(Native 方法)的引用的对象。
通过上面的算法搜索到无用对象之后,就是回收过程,回收算法如下:
1)标记—清除算法(Mark-Sweep)(DVM 使用的算法)
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,标记和清除阶段的效率不高, 而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。

2)复制算法(Copying)
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上, 然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的 JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是 1:1(大概是 8:1)。

3)标记—整理算法(Mark-Compact)
标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

4)分代收集(Generational Collection)
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。
10.谈谈 JVM 的内存结构和内存分配

a)Java 内存模型

Java 虚拟机将其管辖的内存大致分三个逻辑部分:方法区(Method Area)、Java 栈和 Java 堆。
1、方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。常数池,源代码中的命名常量、String 常量和 static 变量保存在方法区。
2、Java Stack 是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。
最典型的 Stack 应用是方法的调用,Java 虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法则对应的 方法帧被弹出(pop)。栈中存储的数据也是运行时确定的。
3、Java 堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。堆中存储的数据常常是大小、数量和生命期在编译时无法确定的。Java 对象的内存总是在 heap 中分配。
我们每天都在写代码,每天都在使用 JVM 的内存。
b)java 内存分配
1、基础数据类型直接在栈空间分配;
2、方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
3、引用数据类型,需要用 new 来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
4、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
5、局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待 GC 回收;
6、方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
7、字符串常量在 DATA 区域分配 ,this 在堆空间分配;
8、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!

11.Java 中引用类型都有哪些?(重要)

Java 中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用(StrongReference)
这个就不多说,我们写代码天天在用的就是强引用。如果一个对象被被人拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
Java 的对象是位于 heap 中的,heap 中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下代码:
1.String abc=new String(“abc”); //1
2.SoftReference softRef=new SoftReference(abc); //2
3.WeakReference weakRef = new WeakReference(abc); //3
4.abc=null; //4
5.softRef.clear();//5

第一行在 heap 堆中创建内容为“abc”的对象,并建立 abc 到该对象的强引用,该对象是强可及的。
第二行和第三行分别建立对 heap 中对象的软引用和弱引用,此时 heap 中的 abc 对象已经有 3 个引用,显然此时 abc 对象仍是强可及的。
第四行之后 heap 中对象不再是强可及的,变成软可及的。第五行执行之后变成弱可及的。
软引用(SoftReference)
如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高

速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用是主要用于内存敏感的高速缓存。在 jvm 报告内存不足之前会清除所有的软引用,这样以来 gc 就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于 gc 的算法和 gc 运行时可用内存的大小。当 gc 决定要收集软引用时执行以下过程,以上面的 softRef 为例:
1首先将 softRef 的 referent(abc)设置为 null,不再引用 heap 中的 new String(“abc”)对象。
2将 heap 中的 new String(“abc”)对象设置为可结束的(finalizable)。
3当 heap 中的 new String(“abc”)对象的 finalize()方法被运行而且该对象占用的内存被释放, softRef
被添加到它的 ReferenceQueue(如果有的话)中。
注意:对 ReferenceQueue 软引用和弱引用可以有可无,但是虚引用必须有。
被 Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。一直要到 JVM 内存不足且没有 Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。如此一来
SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。
弱引用(WeakReference)
如果一个对象只具有弱引用,那该类就是可有可无的对象,因为只要该对象被 gc 扫描到了随时都会把它干掉。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过, 由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对

象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。建立虚引用之后通过 get 方法返回结果始终为 null,通过源代码你会发现,虚引用通向会把引用的对象写进
referent,只是 get 方法返回结果为 null。先看一下和 gc 交互的过程再说一下他的作用。
1 不把 referent 设置为 null, 直接把 heap 中的 new String(“abc”)对象设置为可结束的(finalizable)。
2 与软引用和弱引用不同, 先把 PhantomRefrence 对象添加到它的 ReferenceQueue 中.然后在释放虚可及的对象。
12.heap 和 stack 有什么区别(2017-2-23)

从以下几个方面阐述堆(heap)和栈(stack)的区别。
1.申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于 Java 需要手动 new Object()的形式开辟
2.申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3.申请大小的限制
stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的, 自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见, 堆获得的空间比较灵活,也比较大。
4.申请效率的比较:
stack:由系统自动分配,速度较快。但程序员是无法控制的。
heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
5.heap 和 stack 中的存储内容
stack: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址, 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

6.数据结构层面的区别
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第 1 个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
7.拓展知识(Java 中堆栈的应用)
1). 栈(stack)与堆(heap)都是 Java 用来在 Ram 中存放数据的地方。与 C++不同,Java 自动管理栈和堆,程序员不能直接地设置栈或堆。
2).栈的优势是,存取速度比堆要快,仅次于直接位于 CPU 中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第 3 点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java 的垃圾回收器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3).Java 中的数据类型有两种。
一种是基本类型(primitive types), 共有 8 种,即 int, short, long, byte, float, double, boolean, char(注意,并没有 string 的基本类型)。这种类型的定义是通过诸如 int a = 3; long b = 255L;的形式来定义的,称为自动变量(自动变量:只在定义它们的时候才创建,在定义它们的函数返回时系统回收变量所占存储空间。对这些变量存储空间的分配和回收是由系统自动完成的。)。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用, 这里并没有类的存在。如 int a = 3; 这里的 a 是一个指向 int 类型的引用,指向 3 这个字面值。这些字面值的数据, 由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义
int a = 3;

int b = 3;
编译器先处理 int a = 3;首先它会在栈中创建一个变量为 a 的引用,然后查找有没有字面值为 3 的地址,没找到,就开辟一个存放 3 这个字面值的地址,然后将 a 指向 3 的地址。接着处理 int b = 3;在创建完 b 的引用变量后,由于在栈中已经有 3 这个字面值,便将 b 直接指向 3 的地址。这样,就出现了 a 与 b 同时均指向 3 的情况。
特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完 a 与 b 的值后, 再令 a=4;那么,b 不会等于 4,还是等于 3。在编译器内部,遇到 a=4;时,它就会重新搜索栈中是否有 4 的字面值,如果没有,重新开辟地址存放 4 的值;如果已经有了,则直接将 a 指向这个地址。因此 a 值的改变不会影响到 b 的值。
另一种是包装类数据,如 Integer, String, Double 等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java 用 new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
4).每个 JVM 的线程都有自己的私有的栈空间,随线程创建而创建,java 的 stack 存放的是 frames,java 的 stack 和 c 的不同,只是存放本地变量,返回值和调用方法,不允许直接 push 和 pop frames ,因为 frames 可能是有 heap 分配的,所以java 的stack 分配的内存不需要是连续的。java 的heap 是所有线程共享的,堆存放所有 runtime data , 里面是所有的对象实例和数组,heap 是 JVM 启动时创建。
5).String 是一个特殊的包装类数据。即可以用 String str = new String(“abc”);的形式来创建,也可以用String str = “abc”;的形式来创建(作为对比,在 JDK 5.0 之前,你从未见过 Integer i = 3;的表达式,因为类与字面值是不能通用的,除了 String。而在 JDK 5.0 中,这种表达式是可以的!因为编译器在后台进行 Integer i = new
Integer(3)的转换)。前者是规范的类的创建过程,即在 Java 中,一切都是对象,而对象是类的实例,全部通过 new()

的形式来创建。那为什么在 String str = “abc”;中,并没有通过 new()来创建实例,是不是违反了上述原则?其实没有。
5.1). 关于 String str = "abc"的内部工作。Java 内部将此语句转化为以下几个步骤:
(1)先定义一个名为 str 的对 String 类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的 String 类的对象 o,并将 o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象 o。如果已经有了值为"abc"的地址,则查找对象 o,并返回 o 的地址。
(3)将 str 指向对象 o 的地址。
值得注意的是,一般 String 类中字符串值都是直接存值的。但像 String str = “abc”;这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
String str1 = “abc”; String str2 = “abc”;
System.out.println(str1==str2); //true
注意,我们这里并不用 str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据 JDK 的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1 与 str2 是否都指向了同一个对象。
结果说明,JVM 创建了两个引用 str1 和 str2,但只创建了一个对象,而且两个引用都指向了这个对象。我们再来更进一步,将以上代码改成:
String str1 = “abc”; String str2 = “abc”;

str1 = “bcd”;
System.out.println(str1 + “,” + str2); //bcd, abc System.out.println(str1==str2); //false
这就是说,赋值的变化导致了类对象引用的变化,str1 指向了另外一个新对象!而 str2 仍旧指向原来的对象。上例中,当我们将 str1 的值改为"bcd"时,JVM 发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
事实上,String 类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但 JVM 在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。
再修改原来代码: String str1 = “abc”; String str2 = “abc”; str1 = “bcd”; String str3 = str1;
System.out.println(str3); //bcd String str4 = “bcd”;
System.out.println(str1 == str4); //true
str3 这个对象的引用直接指向 str1 所指向的对象(注意,str3 并没有创建新对象)。当 str1 改完其值后,再创建一个 String 的引用 str4,并指向因 str1 修改值而创建的新的对象。可以发现,这回 str4 也没有创建新的对象,从而再次实现栈中数据的共享。
我们再接着看以下的代码。

String str1 = new String(“abc”); String str2 = “abc”;
System.out.println(str1str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用 new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
6).数据类型包装类的值不可修改。不仅仅是 String 类的值不可修改,所有的数据类型包装类都不能更改其内部的值。
7).结论与建议:
(1)我们在使用诸如 String str = “abc”;的格式定义类时,总是想当然地认为,我们创建了 String 类的对象
str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向 String 类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过 new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向 String 类的对象的引用变量 str,这个对象引用变量指向了某个值为"abc"的 String 类。清醒地认识到这一点对排除程序中难以发现的 bug 是很有帮助的。
(2)使用 String str = “abc”;的方式,可以在一定程度上提高程序的运行速度,因为 JVM 会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于 String str = new String(“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但 JDK 的内部在这里实现是否应用了这个模式,不得而知。
(3)当比较包装类里面的数值是否相等时,用 equals()方法;当测试两个包装类的引用是否指向同一个对象时,

(4)由于 String 类的 immutable 性质,当 String 变量需要经常变换其值时,应该考虑使用 StringBuffer 类,

以提高程序效率。
如果 java 不能成功分配 heap 的空间,将抛出 OutOfMemoryError。

  1. 解释内存中的栈 (stack) 、堆 (heap) 和方法区 (method area) 的用法
    (2017-11-12-wl)

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为From Survivor 和 To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、“hello” 和常量都是放在常量池中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。
String str = new String(“hello”);
上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
四、Java 的类加载器(2015-12-2)

  1. Java 的类加载器的种类都有哪些?

1、根类加载器(Bootstrap) --C++写的 ,看不到源码
2、扩展类加载器(Extension) --加载位置 :jre\lib\ext 中

302

3、系统(应用)类加载器(System\App) --加载位置 :classpath 中
4、自定义加载器(必须继承 ClassLoader)

2.类什么时候被初始化?

1)创建类的实例,也就是 new 一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM 启动时标明的启动类,即文件名和类名相同的那个类只有这 6 中情况才会导致类的类的初始化。
类的初始化步骤:
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。

3.Java 类加载体系之 ClassLoader 双亲委托机制 (2017-2-24)

java 是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全沙箱分别是:
1)类加载体系
2).class 文件检验器

3)内置于 Java 虚拟机(及语言)的安全特性
4)安全管理器及 Java API
主要讲解类的加载体系:
java 程序中的 .java 文件编译完会生成 .class 文件,而 .class 文件就是通过被称为类加载器的 ClassLoader
加载的,而 ClassLoder 在加载过程中会使用“双亲委派机制”来加载 .class 文件,先上图:

BootStrapClassLoader : 启 动 类 加 载 器 , 该 ClassLoader 是 jvm 在 启 动 时 创 建 的 , 用 于 加载 J A V A H O M E JAVA_HOME JAVAHOME/jre/lib 下面的类库(或者通过参数-Xbootclasspath 指定)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
ExtClassLoader:扩展类加载器,该ClassLoader 是在sun.misc.Launcher 里作为一个内部类ExtClassLoader
定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME/jre/lib/ext 下的类

库(或者通过参数-Djava.ext.dirs 指定)。
AppClassLoader:应用程序类加载器,该 ClassLoader 同样是在 sun.misc.Launcher 里作为一个内部类
AppClassLoader 定义的(即 sun.misc.Launcher A p p C l a s s L o a d e r ) , A p p C l a s s L o a d e r 会 加 载 j a v a 环 境 变 量 C L A S S P A T H 所 指 定 的 路 径 下 的 类 库 , 而 C L A S S P A T H 所 指 定 的 路 径 可 以 通 过 S y s t e m . g e t P r o p e r t y ( " j a v a . c l a s s . p a t h " ) 获 取 ; 当 然 , 该 变 量 也 可 以 覆 盖 , 可 以 使 用 参 数 − c p , 例 如 : j a v a − c p 路 径 ( 可 以 指 定 要 执 行 的 c l a s s 目 录 ) 。 C u s t o m C l a s s L o a d e r : 自 定 义 类 加 载 器 , 该 C l a s s L o a d e r 是 指 我 们 自 定 义 的 C l a s s L o a d e r , 比 如 t o m c a t 的 S t a n d a r d C l a s s L o a d e r 属 于 这 一 类 ; 当 然 , 大 部 分 情 况 下 使 用 A p p C l a s s L o a d e r 就 足 够 了 。 前 面 谈 到 了 C l a s s L o a d e r 的 几 类 加 载 器 , 而 C l a s s L o a d e r 使 用 双 亲 委 派 机 制 来 加 载 c l a s s 文 件 的 。 C l a s s L o a d e r 的 双 亲 委 派 机 制 是 这 样 的 ( 这 里 先 忽 略 掉 自 定 义 类 加 载 器 C u s t o m C l a s s L o a d e r ) : 1 ) 当 A p p C l a s s L o a d e r 加 载 一 个 c l a s s 时 , 它 首 先 不 会 自 己 去 尝 试 加 载 这 个 类 , 而 是 把 类 加 载 请 求 委 派 给 父 类 加 载 器 E x t C l a s s L o a d e r 去 完 成 。 2 ) 当 E x t C l a s s L o a d e r 加 载 一 个 c l a s s 时 , 它 首 先 也 不 会 自 己 去 尝 试 加 载 这 个 类 , 而 是 把 类 加 载 请 求 委 派 给 B o o t S t r a p C l a s s L o a d e r 去 完 成 。 3 ) 如 果 B o o t S t r a p C l a s s L o a d e r 加 载 失 败 ( 例 如 在 AppClassLoader),AppClassLoader 会加载 java 环境变量 CLASSPATH 所 指 定 的 路 径 下 的 类 库 , 而 CLASSPATH 所 指 定 的 路 径 可 以 通 过 System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的 class 目录)。 CustomClassLoader:自定义类加载器,该 ClassLoader 是指我们自定义的 ClassLoader,比如 tomcat 的 StandardClassLoader 属于这一类;当然,大部分情况下使用 AppClassLoader 就足够了。 前面谈到了ClassLoader 的几类加载器,而ClassLoader 使用双亲委派机制来加载class 文件的。ClassLoader 的双亲委派机制是这样的(这里先忽略掉自定义类加载器 CustomClassLoader): 1)当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader 去完成。 2)当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。 3)如果 BootStrapClassLoader 加载失败(例如在 AppClassLoaderAppClassLoaderjavaCLASSPATHCLASSPATHSystem.getProperty("java.class.path")使cpjavacpclassCustomClassLoaderClassLoaderClassLoadertomcatStandardClassLoader使AppClassLoaderClassLoaderClassLoader使classClassLoaderCustomClassLoader1AppClassLoaderclassExtClassLoader2ExtClassLoaderclassBootStrapClassLoader3BootStrapClassLoaderJAVA_HOME$/jre/lib 里未查找到该 class),会使用
ExtClassLoader 来尝试加载;
4)若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException。
下面贴下 ClassLoader 的 loadClass(String name, boolean resolve)的源码:

1.protected synchronized Class<?> loadClass(String name, boolean resolve) 2. throws ClassNotFoundException {
3. // 首先找缓存是否有 class
4. Class c = findLoadedClass(name);
5. if (c == null) {
305

6. //没有判断有没有父类
7. try {
8. if (parent != null) {
9. //有的话,用父类递归获取 class
10. c = parent.loadClass(name, false); 11. } else {
12. //没有父类。通过这个方法来加载
13. c = findBootstrapClassOrNull(name); 14. }
15. } catch (ClassNotFoundException e) {
16. // ClassNotFoundException thrown if class not found 17. // from the non-null parent class loader
18. }
19. if (c == null) {
20. // 如果还是没有找到,调用 findClass(name)去找这个类
21. c = findClass(name); 22. }
23. }
24. if (resolve) {
25. resolveClass©; 26. }
27. return c;
28. }

代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有 parent,有的话就用 parent 来递归的 loadClass,然而 ExtClassLoader 并没有设置 parent,则会通过 findBootstrapClassOrNull 来加载 class,而
findBootstrapClassOrNull 则会通过 JNI 方法”private native Class findBootstrapClass(String name)“来使用 BootStrapClassLoader 来加载 class。
然后如果 parent 未找到 class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法, 可以覆盖它以便自定义 class 加载过程。
另外, 虽然 ClassLoader 加载类是使用 loadClass 方法, 但是鼓励用 ClassLoader 的子类重写
findClass(String),而不是重写 loadClass,这样就不会覆盖了类加载默认的双亲委派机制。双亲委派托机制为什么安全
举个例子,ClassLoader 加载的 class 文件来源很多,比如编译器编译生成的 class、或者网络下载的字节码。
306

而一些来源的 class 文件是不可靠的,比如我可以自定义一个 java.lang.Integer 类来覆盖 jdk 中默认的 Integer
类,例如下面这样:

1.package java.lang;
2.public class Integer {
3.public Integer(int value) {
4.System.exit(0);
5. }
6. }
初始化这个 Integer 的构造器是会退出 JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer 类永远不会被调用,以为委托 BootStrapClassLoader 加载后会加载 JDK 中的 Integer 类而不会加载自定义的这个,可以看下下面这测试个用例:

执行时 JVM 并未在 new Integer(1)时退出,说明未使用自定义的 Integer,于是就保证了安全性。

4.描述一下 JVM 加载 class (2017-11-15-wl)

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备
(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:

如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器
(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。
从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。
下面是关于几个类加载器的说明:
•Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
•Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;
•System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath
或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

5.获得一个类对象有哪些方式?(2017-11-23-wzz)

类型.class,例如:String.class
对象.getClass(),例如:”hello”.getClass()
Class.forName(),例如:Class.forName(“java.lang.String”)

五、JVM 基础知识(2017-11-16-wl)

  1. 既然有 GC 机制,为什么还会有内存泄露的情况 (2017-11-16-wl)

理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。
例如 hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
下面例子中的代码也会导致内存泄露。

1.import java.util.Arrays;
2.import java.util.EmptyStackException;
3.public class MyStack {
4.private T[] elements;
5.private int size = 0;
6.private static final int INIT_CAPACITY = 16;
7.public MyStack() {
8.elements = (T[]) new Object[INIT_CAPACITY]; 9. }
10.public void push(T elem) {
11.ensureCapacity();
12.elements[size++] = elem;
13. }
14.public T pop() {
15.if(size == 0)throw new EmptyStackException();
16.return elements[–size];
17. }
18.private void ensureCapacity() {
19.if(elements.length == size) {
20.elements = Arrays.copyOf(elements, 2 * size + 1);
21. }
22. }
23. }

309

上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用( obsolete
reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发 Disk Paging (物理内存与硬盘的虚拟内存交换数据),甚至造成 OutOfMemoryError。
六、GC 基础知识(2017-11-16-wl)
1.Java 中为什么会有 GC 机制呢?(2017-11-16-wl)
Java 中为什么会有 GC 机制呢?
安全性考虑;-- for security.
减少内存泄露;-- erase memory leak in some degree.
减少程序员工作量。-- Programmers don’t worry about memory releasing.
2.对于 Java 的 GC 哪些内存需要回收(2017-11-16-wl)

内存运行时 JVM 会有一个运行时数据区来管理内存。它主要包括 5 大部分:程序计数器(Program Counter
Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap).
而其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构确定是哪个时就已知了,因此这 3 个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。
但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创
310

建哪些对象,这部分内存的分配和回收都是动态的,GC 主要关注的是这部分内存。总而言之,GC 主要进行回收的内存是 JVM 中的方法区和堆;
3. Java 的 GC 什么时候回收垃圾(2017-11-16-wl)

在面试中经常会碰到这样一个问题(事实上笔者也碰到过):如何判断一个对象已经死去?
很容易想到的一个答案是:对一个对象添加引用计数器。每当有地方引用它时,计数器值加 1;当引用失效时,计数器值减 1.而当计数器的值为 0 时这个对象就不会再被使用,判断为已死。是不是简单又直观。然而,很遗憾。这种做法是错误的!为什么是错的呢?事实上,用引用计数法确实在大部分情况下是一个不错的解决方案,而在实际的应用中也有不少案例,但它却无法解决对象之间的循环引用问题。比如对象 A 中有一个字段指向了对象 B,而对象 B 中也有一个字段指向了对象 A,而事实上他们俩都不再使用,但计数器的值永远都不可能为 0,也就不会被回收,然后就发生了内存泄露。
所以,正确的做法应该是怎样呢?
在 Java,C#等语言中,比较主流的判定一个对象已死的方法是:可达性分析(Reachability Analysis).
所有生成的对象都是一个称为"GC Roots"的根的子树。从 GC Roots 开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链可以到达时,就称这个对象是不可达的(不可引用的), 也就是可以被 GC 回收了。

无论是引用计数器还是可达性分析,判定对象是否存活都与引用有关!那么,如何定义对象的引用呢?
我们希望给出这样一类描述:当内存空间还够时,能够保存在内存中;如果进行了垃圾回收之后内存空间仍旧非常紧张,则可以抛弃这些对象。所以根据不同的需求,给出如下四种引用,根据引用类型的不同,GC 回收时也会有不同的操作:
1)强引用(Strong Reference):Object obj = new Object();只要强引用还存在,GC 永远不会回收掉被引用的对象。

311

2)软引用(Soft Reference):描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象列入回收范围进行二次回收(即系统将会发生内存溢出了,才会对他们进行回收。)
弱引用(Weak Reference):程度比软引用还要弱一些。这些对象只能生存到下次 GC 之前。当 GC 工作时,无论内存是否足够都会将其回收(即只要进行 GC,就会对他们进行回收。)
虚引用(Phantom Reference):一个对象是否存在虚引用,完全不会对其生存时间构成影响。关于方法区中需要回收的是一些废弃的常量和无用的类。
1.废弃的常量的回收。这里看引用计数就可以了。没有对象引用该常量就可以放心的回收了。
2.无用的类的回收。什么是无用的类呢?
A.该类所有的实例都已经被回收。也就是 Java 堆中不存在该类的任何实例;
B.加载该类的 ClassLoader 已经被回收;
C.该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。总而言之:
对于堆中的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。而根据我们实际对引用的不同需求,又分成了 4 中引用,每种引用的回收机制也是不同的。
对于方法区中的常量和类,当一个常量没有任何对象引用它,它就可以被回收了。而对于类,如果可以判定它为无用类,就可以被回收了。
七、Java8 的新特性以及使用(2017-12-02-wl)

  1. 通过 10 个示例来初步认识 Java8 中的 lambda 表达式(2017-12-02-wl)

我个人对 Java 8 发布非常激动,尤其是 lambda 表达式和流 API。越来越多的了解它们,我能写出更干净的代码。虽然一开始并不是这样。第一次看到用 lambda 表达式写出来的 Java 代码时,我对这种神秘的语法感到非常失望,认

为它们把 Java 搞得不可读,但我错了。花了一天时间做了一些 lambda 表达式和流 API 示例的练习后,我开心的看到了更清晰的 Java 代码。这有点像学习泛型,第一次见的时候我很讨厌它。我甚至继续使用老版 Java 1.4 来处理集合, 直到有一天,朋友跟我介绍了使用泛型的好处(才意识到它的好处)。所以基本立场就是,不要畏惧 lambda 表达式以及方法引用的神秘语法,做几次练习,从集合类中提取、过滤数据之后,你就会喜欢上它。下面让我们开启学习 Java 8 lambda 表达式的学习之旅吧~
本小节中先不说 lambda 表达的含义和繁琐的概念。我们先从最简单的示例来介绍 java8 中的 lambda 表达式

例 1、用 lambda 表达式实现 Runnable

// Java 8 之前:
new Thread(new Runnable() { @Override
public void run() {
System.out.println(“Before Java8, too much code for too little to do”);
}
}).start();

//Java 8 方式:
new Thread( () -> System.out.println(“In Java8, Lambda expression rocks !!”) ).start();

输出:
too much code, for too little to do
Lambda expression rocks !!
这个例子向我们展示了 Java 8 lambda 表达式的语法。你可以使用 lambda 写出如下代码:

(params) -> expression (params) -> statement
(params) -> { statements }
例如,如果你的方法不对参数进行修改、重写,只是在控制台打印点东西的话,那么可以这样写:
() -> System.out.println(“Hello Lambda Expressions”);
如果你的方法接收两个参数,那么可以写成如下这样:
(int even, int odd) -> even + odd
顺便提一句,通常都会把 lambda 表达式内部变量的名字起得短一些。这样能使代码更简短,放在同一行。所以,

在上述代码中,变量名选用 a、b 或者 x、y 会比 even、odd 要好。

例 2、使用 Java 8 lambda 表达式进行事件处理

如果你用过 Swing API 编程,你就会记得怎样写事件监听代码。这又是一个旧版本简单匿名类的经典用例,但现在可以不这样了。你可以用 lambda 表达式写出更好的事件监听代码,如下所示:

Java 开发者经常使用匿名类的另一个地方是为 Collections.sort() 定制 Comparator。在 Java 8 中,你可以用更可读的 lambda 表达式换掉丑陋的匿名类。我把这个留做练习,应该不难,可以按照我在使用 lambda 表达式实现Runnable 和 ActionListener 的过程中的套路来做。
例 3、使用 Java 8 lambda 表达式进行事件处理 使用 lambda 表达式对列表进行迭代

如果你使过几年 Java,你就知道针对集合类,最常见的操作就是进行迭代,并将业务逻辑应用于各个元素,例如处理订单、交易和事件的列表。由于 Java 是命令式语言,Java 8 之前的所有循环代码都是顺序的,即可以对其元素进行并行化处理。如果你想做并行过滤,就需要自己写代码,这并不是那么容易。通过引入 lambda 表达式和默认方法, 将做什么和怎么做的问题分开了,这意味着 Java 集合现在知道怎样做迭代,并可以在 API 层面对集合元素进行并行处理。下面的例子里,我将介绍如何在使用 lambda 或不使用 lambda 表达式的情况下迭代列表。你可以看到列表现在
有了一个 forEach() 方法,它可以迭代所有对象,并将你的 lambda 代码应用在其中。
314

// Java 8 之前:
List features = Arrays.asList(“Lambdas”, “Default Method”, “Stream API”, “Date and Time API”); for (String feature : features) {
System.out.println(feature);
}

// Java 8 之后:
List features = Arrays.asList(“Lambdas”, “Default Method”, “Stream API”, “Date and Time API”); features.forEach(n -> System.out.println(n));

// 使用 Java 8 的方法引用更方便,方法引用由::双冒号操作符标示,
// 看起来像 C++的作用域解析运算符
features.forEach(System.out::println);

输出:
Lambdas Default Method Stream API
Date and Time API

列表循环的最后一个例子展示了如何在 Java 8 中使用方法引用(method reference)。你可以看到 C++里面的双冒号、范围解析操作符现在在 Java 8 中用来表示方法引用。
例 4、使用 lambda 表达式和函数式接口 Predicate

除了在语言层面支持函数式编程风格,Java 8 也添加了一个包,叫做 java.util.function。它包含了很多类,用来支持 Java 的函数式编程。其中一个便是 Predicate,使用 java.util.function.Predicate 函数式接口以及 lambda 表达式,可以向 API 方法添加逻辑,用更少的代码支持更多的动态行为。下面是 Java 8 Predicate 的例子,展示了过滤集合数据的多种常用方法。Predicate 接口非常适用于做过滤。

315

System.out.println(“Print all languages :”); filter(languages, (str)->true);

System.out.println("Print no language : "); filter(languages, (str)->false);

System.out.println(“Print language whose length greater than 4:”); filter(languages, (str)->str.length() > 4);
}

public static void filter(List names, Predicate condition) { for(String name: names) {
if(condition.test(name)) { System.out.println(name + " ");
}
}
}

// filter 更好的办法–filter 方法改进
public static void filter(List names, Predicate condition) { names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {
System.out.println(name + " ");
});
}

可以看到,Stream API 的过滤方法也接受一个 Predicate,这意味着可以将我们定制的 filter() 方法替换成写在里面的内联代码,这就是 lambda 表达式的魔力。另外,Predicate 接口也允许进行多重条件的测试,下个例子将要讲到。
例 5、如何在 lambda 表达式中加入 Predicate

上个例子说到,java.util.function.Predicate 允许将两个或更多的 Predicate 合成一个。它提供类似于逻辑操作符 AND 和 OR 的方法,名字叫做 and()、or()和 xor(),用于将传入 filter() 方法的条件合并起来。例如,要得到所有以J 开始,长度为四个字母的语言,可以定义两个独立的 Predicate 示例分别表示每一个条件,然后用 Predicate.and()

方法将它们合并起来,如下所示:

316

// 甚至可以用 and()、or()和 xor()逻辑函数来合并 Predicate,
// 例如要找到所有以 J 开始,长度为四个字母的名字,你可以合并两个 Predicate 并传入
Predicate startsWithJ = (n) -> n.startsWith(“J”); Predicate fourLetterLong = (n) -> n.length() == 4; names.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with ‘J’ and four letter long is : " +
n));

类似地,也可以使用 or() 和 xor() 方法。本例着重介绍了如下要点:可按需要将 Predicate 作为单独条件然后将其合并起来使用。简而言之,你可以以传统 Java 命令方式使用 Predicate 接口,也可以充分利用 lambda 表达式达到事半功倍的效果。
例 6、Java 8 中使用 lambda 表达式的 Map 和 Reduce 示例

本例介绍最广为人知的函数式编程概念 map。它允许你将对象进行转换。例如在本例中,我们将 costBeforeTax 列表的每个元素转换成为税后的值。我们将 x -> x*x lambda 表达式传到 map() 方法,后者将其应用到流中的每一个元素。然后用 forEach() 将列表元素打印出来。使用流 API 的收集器类,可以得到所有含税的开销。有 toList() 这样的方法将 map 或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。你甚至可以用流 API 的 reduce() 方法将所有数字合成一个,下一个例子将会讲到。

在上面例子中,可以看到 map 将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map 和 Reduce 操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL 中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是

reduce 操作,因为它们接收多个值并返回一个值。流 API 定义的 reduceh() 函数可以接受 lambda 表达式,并对所有值进行合并。IntStream 这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有
mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个 Java
8 的 Map Reduce 示例里,我们首先对所有价格应用 12% 的 VAT,然后用 reduce() 方法计算总和。

// 为每个订单加上 12%的税
// 老方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500); double total = 0;
for (Integer cost : costBeforeTax) { double price = cost + .12*cost; total = total + price;
}
System.out.println("Total : " + total);

// 新方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost) -> sum + cost).get();
System.out.println("Total : " + bill);

例 7、通过过滤创建一个 String 列表

过滤是 Java 开发者在大规模集合上的一个常用操作,而现在使用 lambda 表达式和流 API 过滤大规模数据集合是惊人的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个 lambda 表达式作为过滤逻辑。下面的例子是用 lambda 表达式过滤 Java 集合,将帮助理解。

另外,关于 filter() 方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用 filter()方法则是

获得一个新的列表,且其每个元素符合过滤原则。

例 8、对列表的每个元素应用函数

我们通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,可以将转换逻辑以 lambda 表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了,如下所示。

例 9、复制不同的值,创建一个子列表

本例展示了如何利用流的 distinct() 方法来对集合进行去重

// 用所有不同的数字创建一个正方形列表
List numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList()); System.out.printf(“Original List : %s, Square Without duplicates : %s %n”, numbers, distinct);

输出:
Original List : [9, 10, 3, 4, 7, 3, 4], Square Without duplicates : [81, 100, 9, 16, 49]

例 10、计算集合元素的最大值、最小值、总和以及平均值

IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

319

IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics(); System.out.println("Highest prime number in List : " + stats.getMax()); System.out.println("Lowest prime number in List : " + stats.getMin()); System.out.println("Sum of all prime numbers : " + stats.getSum()); System.out.println("Average of all prime numbers : " + stats.getAverage());

输出:
Highest prime number in List : 29 Lowest prime number in List : 2 Sum of all prime numbers : 129
Average of all prime numbers : 12.9

Java 8 的 10 个 lambda 表达式,这对于新手来说是个合适的任务量,你可能需要亲自运行示例程序以便掌握。试着修改要求创建自己的例子,达到快速学习的目的。
补充:从例子中我们可以可以看到,以前写的匿名内部类都用了 lambda 表达式代替了。那么,我们简单谈谈
“lambda 表达式&匿名内部类”两者不用:
1.关键字 this
(1)匿名内部类中的 this 代表匿名类
(2)Lambda 表达式中的 this 代表 lambda 表达式的类
2.编译方式不同
(1)匿名内部类中会编译成一个.class 文件,文件命名方式为:主类+$+(1,2,3…)
(2)Java 编译器将 lambda 表达式编译成类的私有方法。使用了 Java 7 的 invokedynamic 字节码指令来动态绑定这个方法
2. Java8 中的 lambda 表达式要点(2017-12-02-wl)

通过面 10 个小示例中学习,我们下面说下 lambda 表达式的 6 个要点

要点 1:lambda 表达式的使用位置

320

预定义使用了 @Functional 注释的函数式接口,自带一个抽象函数的方法,或者 SAM(Single Abstract Method单个抽象方法)类型。这些称为 lambda 表达式的目标类型,可以用作返回类型,或 lambda 目标代码的参数。例如, 若一个方法接收 Runnable、Comparable 或者 Callable 接口,都有单个抽象方法,可以传入 lambda 表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier, 那么可以向其传 lambda 表达式。
要点 2:lambda 表达式和方法引用

lambda 表达式内可以使用方法引用,仅当该方法不修改 lambda 表达式提供的参数。本例中的 lambda 表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); // 使用方法引用
然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地 lambda 表达式,如下所示:
list.forEach((String s) -> System.out.println("" + s + ""));
事实上,可以省略这里的 lambda 参数的类型声明,编译器可以从列表的类属性推测出来。

要点 3:lambda 表达式内部引用资源

lambda 内部可以使用静态、非静态和局部变量,这称为 lambda 内的变量捕获。

要点 4:lambda 表达式也成闭包

Lambda 表达式在 Java 中又称为闭包或匿名函数,所以如果有同事把它叫闭包的时候,不用惊讶。

要点 5:lambda 表达式的编译方式

Lambda 方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用 JDK
中的 javap 工具来反编译 class 文件。使用 javap -p 或 javap -c -v 命令来看一看 lambda 表达式生成的字节码。

大致应该长这样:

321

private static java.lang.Object lambda$0(java.lang.String);	

要点 6:lambda 表达式的限制

lambda 表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在 lambda 内部修改定义在域外的变量。
List primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { factor++; });

Error:
Compile time error : “local variables referenced from a lambda expression must be final or
effectively final”
另外,只是访问它而不作修改是可以的,如下所示:

List primes = Arrays.asList(new Integer[]{2, 3,5,7}); int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });

输出:
4
6
10
14
因此,它看起来更像不可变闭包,类似于 Python。

  1. Java8 中的 Optional 类的解析(2017-12-02-wl)

身为一名 Java 程序员,大家可能都有这样的经历:调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法。我们首先要判断这个返回值是否为 null,只有在非空的前提下才能将其作为其他方法的参数。这正是一些类似 Guava 的外部 API 试图解决的问题。一些 JVM 编程语言比如 Scala、Ceylon 等已经将对在核心 API 中解决了这个问题。在我的前一篇文章中,介绍了 Scala 是如何解决了这个问题。
新版本的 Java,比如 Java 8 引入了一个新的 Optional 类。Optional 类的 Javadoc 描述如下:

322

这是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。
下面会逐个探讨 Optional 类包含的方法,并通过一两个示例展示如何使用。

方法 1:Optional.of()

作用:为非 null 的值创建一个 Optional。
说明:of 方法通过工厂方法创建 Optional 类。需要注意的是,创建对象时传入的参数不能为 null。如果传入参数为 null,则抛出 NullPointerException 。

方法 2:Optional.ofNullable()

作用:为指定的值创建一个 Optional,如果指定的值为 null,则返回一个空的 Optional。说明:ofNullable 与 of 方法相似,唯一的区别是可以接受参数为 null 的情况。

方法 3:Optional.isPresent()

作用:判断预期值是否存在
说明:如果值存在返回 true,否则返回 false。

//isPresent 方法用来检查 Optional 实例中是否包含值
Optional name = Optional.of(“Sanaulla”); if (name.isPresent()) {
//在 Optional 实例内调用 get()返回已存在的值
System.out.println(name.get());//输出 Sanaulla
}

方法 4:Optional.get()

323

作用:如果 Optional 有值则将其返回,否则抛出 NoSuchElementException。
说明:上面的示例中,get 方法用来得到 Optional 实例中的值。下面我们看一个抛出 NoSuchElementException
的例子

//执行下面的代码会输出:No value present
try {
Optional empty = Optional.ofNullable(null);
//在空的 Optional 实例上调用 get(),抛出 NoSuchElementException System.out.println(empty.get());
} catch (NoSuchElementException ex) { System.out.println(ex.getMessage());
}

方法 5:Optional.ifPresent()

作用:如果 Optional 实例有值则为其调用 consumer,否则不做处理
说明:要理解 ifPresent 方法,首先需要了解 Consumer 类。简答地说,Consumer 类包含一个抽象方法。该抽象方法对传入的值进行处理,但没有返回值。Java8 支持不用接口直接通过 lambda 表达式传入参数,如果 Optional 实例有值,调用 ifPresent()可以接受接口段或 lambda 表达式

方法 7:Optional.orElse()

作用:如果有值则将其返回,否则返回指定的其它值。
说明:如果 Optional 实例有值则将其返回,否则返回 orElse 方法传入的参数。示例如下:

Optional name = Optional.of(“Sanaulla”); Optional someNull = Optional.of(null);
//如果值不为 null,orElse 方法返回 Optional 实例的值。
//如果为 null,返回传入的消息。
324

//输出:There is no value present!
System.out.println(empty.orElse(“There is no value present!”));
//输出:Sanaulla
System.out.println(name.orElse(“There is some value!”));

方法 8:Optional.orElseGet()

作用:如果有值则将其返回,否则返回指定的其它值。
说明:orElseGet 与 orElse 方法类似,区别在于得到的默认值。orElse 方法将传入的字符串作为默认值,orElseGet 方法可以接受 Supplier 接口的实现用来生成默认值
Optional name = Optional.of(“Sanaulla”);
Optional someNull = Optional.of(null);

//orElseGet 与 orElse 方法类似,区别在于 orElse 传入的是默认值,
//orElseGet 可以接受一个 lambda 表达式生成默认值。
//输出:Default Value
System.out.println(empty.orElseGet(() -> “Default Value”));
//输出:Sanaulla
System.out.println(name.orElseGet(() -> “Default Value”));
方法 9:Optional.orElseThrow()

作用:如果有值则将其返回,否则抛出 supplier 接口创建的异常。
说明:在 orElseGet 方法中,我们传入一个 Supplier 接口。然而,在 orElseThrow 中我们可以传入一个 lambda
表达式或方法,如果值不存在来抛出异常

try {
Optional empty= Optional.of(null);

//orElseThrow 与 orElse 方法类似。与返回默认值不同,
//orElseThrow 会抛出 lambda 表达式或方法生成的异常
empty.orElseThrow(ValueAbsentException::new);
} catch (Throwable ex) {
//输出: No value present in the Optional instance System.out.println(ex.getMessage());
}

ValueAbsentException 定义如下:

class ValueAbsentException extends Throwable {

public ValueAbsentException() { super();
}

public ValueAbsentExcep

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值