6、CountDownLatch的工作原理
答:CountDownLatch采用AQS(AbstractQueuedSynchronizer)队列实现,先初始化Count,再countDown,当计数器值到达0时,表示所有任务都执行完了。
/**
* 用CountDownLatch实现多个任务并发计算,并汇总结果
* @author changtan.sun
*
*/
public class MyComputeTask {
static ConcurrentLinkedQueue<Long> sums = new ConcurrentLinkedQueue<Long>();
public MyComputeTask() {
}
public long compute(int number, int part) throws Exception {
number = number + 1;
int parts = (number + part - 1) / part;
CountDownLatch latch = new CountDownLatch(parts + 1);
for (int i = 0; i < parts; i++) {
long min = i * part;
long max = (i + 1) * part < number ? (i + 1) * part : number;
new Thread(new MyTask(min, max, latch)).start();
}
latch.countDown();
latch.await();
int sum = 0;
for (Long s : sums) {
sum += s;
}
return sum;
}
private static class MyTask implements Runnable {
private long min;
private long max;
private CountDownLatch latch;
public MyTask(long min, long max, CountDownLatch latch) {
this.min = min;
this.max = max;
this.latch = latch;
}
@Override
public void run() {
long sum = 0;
for (long i = min; i < max; i++) {
sum += i;
}
sums.add(sum);
latch.countDown();
}
}
}
7、wait和notify
答:(1)wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
当前的线程必须拥有当前对象的monitor,也即lock,就是锁。
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
(2)notify()方法会唤醒一个等待当前对象的锁的线程。
notify方法调用必须放在synchronized方法或synchronized块中。
8、用 wait-notify 写一段代码来解决生产者-消费者问题
9、什么是线程局部变量
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。
10、用 Java 写一个线程安全的单例模式(Singleton)
public class Test4 {
private static volatile Test4 instance; //懒汉模式
private Test4() {
}
public static Test4 getInstance() {
if (instance == null) { //双重否定加锁
synchronized (Test4.class) {
if (instance == null) {
instance = new Test4();
}
}
}
return instance;
}
}
这里为什么要使用volatile修饰instance?主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情:
(1)给instance分配内存
(2)调用Singleton的构造函数来初始化成员变量
(3)将instance对象指向分配的内存空间(执行完这步instance就为非null了)。
但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错。
11、Java内存逃逸
答:(1)当变量(或者对象)在方法中分配后,其指针被返回或者被全局引用(这样就会被其他过程或者线程所引用),这种现象称作指针(或者引用)的逃逸(Escape)。
(2)指针逃逸场景:全局变量赋值,方法返回值,实例引用传递
class A {
public static B b;
public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸
b = new B();
}
public B methodPointerEscape() { // 方法返回值,发生逃逸
return new B();
}
public void instancePassPointerEscape() {
methodPointerEscape().printClassName(this); // 实例引用传递,发生逃逸
}
}
class B {
public void printClassName(A a) {
System.out.println(a.class.getName());
}
}
(3)this逃逸
this逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误, 因此应该避免this逃逸的发生.
public class ThisEscape {
public ThisEscape() {
new Thread(new EscapeRunnable()).start();
// ...
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸
}
}
}
解决方案:
public class ThisEscape {
private Thread t;
public ThisEscape() {
t = new Thread(new EscapeRunnable());
// ...
}
public void init() {
t.start();
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// 通过ThisEscape.this就可以引用外围类对象, 此时可以保证外围类对象已经构造完成
}
}
}
12、final关键词
答:(1)final的作用:
final关键字提高了性能。JVM和Java应用都会缓存final变量。
final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
使用final关键字,JVM会对方法、变量及类进行优化。
(2)final的内存模型
final域,编译器和处理器要遵守两个重排序规则:
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域的重排序规则会要求译编器在final域的写之后,构造函数return之前,插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。
(3) final引用不能从构造函数内“逸出”
13、volatile关键字
答:(1)volatile关键字来保证可见性
1)当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
2)禁止进行指令重排序
(2)volatile不能保证原子性
可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
(3)volatile一定程度上保证有序性
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
(4)使用volatile必须具备以下2个条件:
对变量的写操作不依赖于当前值
该变量没有包含在具有其他变量的不变式中
volatile boolean inited = false;
private volatile static Singleton instance= null;
也就是最简单的形式