1.线程安全主要关注的问题:原子性和可见性。
所谓原子性,是指它要么完整的被执行,要么完全不执行。对于多线程来说,可以理解为,对于其他线程而言,它看到的是要么完整的被执行,要么完全不执行,而不会看到中间状态。实现原子性的主要方法有:原子变量:AtomicInteger,AtomicReference等;加锁,包括使用synchronized关键字;
所谓可见性,是指对于多个线程共享的变量,当一个线程做了修改之后,其他的线程在读取的时候能够立即看得到。实现可见性的主要方法有volatile 关键字;原子变量;加锁,包括使用synchronized关键字;
2.实现线程安全的方法:
原子变量;加锁;线程封闭,例如ThreadLocal;类封闭,使得变量完全封闭在类中,对外不可修改;使用final关键字使得类的各个状态为不可变,这样一定是线程安全的。
3.使用线程安全的类构造出来的代码不一定是线程安全的,使用非线程安全的类构造的代码也不一定是非线程安全的。
前者的例子:
private static AtmoicInteger count=new AtomicInteger(0);
public increaseTwice(){
count.getAndIncrease();
count.getAndIncrease();
}
AtmoicInteger 是线程安全的, 但是由于对count的两次操作没有加锁,这段代码仍然不是线程安全的。
后者的例子:将类放到ThreadLocal中使用,一定是线程安全的。但是这样也失去了共享的可能。
4.Atomic变量的实现机制:其实Atomic是对Unsafe的一个封装,装饰器模式。对于最基本的操作,例如getAndAdd,getAndIncrease,compareAndSet等都是调用Unsafe对象的相关方法。只有getAndUpdate(IntUnaryOperator updateFunction),需要传入一个lambda表达式,Atomic采用了乐观锁来保证原子性。
而对于其他方法,在Unsafe中的实现,只有最基本的compareAndSwap是使用jni调用更底层的C(C又调用汇编,test and set指令),其余的是Unsafe封装的一个乐观锁。
其中的一段代码:
int i;
do {
i = getIntVolatile(paramObject, paramLong);
} while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
return i;
5. 同步的基础构建模块
BlockingQueue; CountDonwLatch;Barrier(栅栏);信号量;其实现都依赖于AbstractQueuedSynchronizer
6. 显式锁。在Java5之前,用于同步的只有sychronized和wait机制,Java5加入了ReentrentLock。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
void unlock();
Condition newCondition();
}
7. 以下代码是否线程安全?
class ListHelper{
public synchronized void putIfAbsent(List<E> list, E x){
if(!list.contains(x)) list.add(x);
}
}
答案:不是线程安全的,即使传入的list是线程安全集合。因为,这个synchronized只是加在了ListHelper的一个对象上。对于不使用这个类的修改list的操作(无需获取这个锁),可以肆无忌惮的修改这个类。
但是下面这个就是线程安全的,尽管list是public的。因为实际上List作为一个并发安全集合,它加的锁是this,也就是list,在这个类中,锁也是加在了list上,所以是线程安全的。
class ListHelper<E>{
public List<E> list=Collections.synchronizedList(new ArrayList<E>());
public void putIfAbsent(E x){
synchronized(list){
if(!list.contains(x)) list.add(e);
}
}
}
8. wait和notify。调用wait和notify的线程必须持有对象上的监视器锁。或者说处于一个对应的对象的synchronized代码块中。调用wait会释放这个锁,然后进入阻塞。调用notify也会释放锁,然后导致wait的线程被唤醒。使用while 循环去轮询也可以实现这个功能,但是会导致CPU使用率很高。注意,如果在wait之前就调用notify/notifyAll,被阻塞的调用wait的线程会丢失这次notify。问题来了,wait和notify使用的是操作系统的什么机制实现的?
public class Worker {
public static void main(String[] args) throws InterruptedException {
PostOffice postOffice = new PostOffice();
postOffice.offer("Hello");
new Thread(() -> {
int counter = 1;
while (counter++ <= 1000) {
postOffice.offer("This is the " + counter + "th message...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> postOffice.poll()).start();
}
}
class PostOffice {
//private BlockingQueue<String> queue = new LinkedBlockingDeque<>();
private volatile Queue<String> queue = new LinkedList<>();
public void offer(String message) {
synchronized (queue) {
queue.offer(message);
queue.notify();
}
}
public String poll() {
while (true) {
synchronized (queue) {
if (queue.size() == 0) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
String message = queue.poll();
System.out.println(message);
}
}
}
}
}
9. 内置锁,内置条件和显式锁,显式条件对比。内置锁是通过synchronized关键字实现的,内置条件是通过不同条件出现时使用wait和notify来进行通知机制的。显式锁是通过ReentrantLock实现的,显式条件是通过condition实现的。
package com.amazon;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumer {
private Queue<String> queue = new LinkedList<>();
public synchronized void put(String message) {
try {
while (queue.size() > 3) {
System.out.println("Waiting for the queue to have space");
wait();
}
queue.offer(message);
notifyAll();
System.out.println("The message put is " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized String take() {
try {
while (queue.size() <= 0) {
System.out.println("Waiting for the queue to have data");
wait();
}
String message = queue.poll();
notifyAll();
System.out.println("The message taken is " + message);
return message;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
class ProducerConsumerUsingReentratLock {
private Queue<String> queue = new LinkedList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition fullCondition = lock.newCondition();
private Condition emptyCondition = lock.newCondition();
public void put(String message) {
lock.lock();
try {
while (queue.size() > 3) {
System.out.println("Waiting for the queue to have space");
fullCondition.await();
}
queue.offer(message);
emptyCondition.signal();
System.out.println("The message put is " + message);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public synchronized String take() {
lock.lock();
try {
while (queue.size() <= 0) {
System.out.println("Waiting for the queue to have data");
emptyCondition.await();
}
String message = queue.poll();
fullCondition.signal();
System.out.println("The message taken is " + message);
return message;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
lock.unlock();
}
}
}
class ProducerConsumerTest {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
new Thread(() -> {
try {
while (true) {
pc.take();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
int count = Integer.valueOf(str);
while (--count > 0) {
pc.put("Hello, " + count);
}
}
}
}
10. 优先级反转,即使一个线程的优先级很低,并且在执行中出现问题(例如缺页,调度延迟等),其他线程也要等待其执行完毕,因为它持有一个锁。
11. 重排序,如果不使用同步,则JVM在编译代码时,可能会对代码进行重排序,如果认为在单线程中执行顺序没有依赖关系或者可以进行寄存器优化,就会进行重排序。
12. 对象的构造函数可以一定程度上认为是原子的,因为构造函数返回之前,该对象对外不可见。但是如果构造函数中有对象把this指针发布出去,这对外部就可以访问。这时候就不是原子的(对象逸出)。构造函数在执行之前对象已经创建完成,构造函数中的方法在执行之前,对象已经产生了。
13. 当多个线程访问同一个可变的状态时没有使用合适的同步,就可能会出现错误,未必一定出现错误。解决方法:1)不在线程之间共享这些变量(例如使用局部变量而非全局状态)2)使用不可变对象3)使用同步
14. synchronized(lock)成为内置锁或者监视器锁,线程在进入同步代码块之前会自动获得锁,退出之后会自动释放锁,获得这个锁的唯一途径就是进入同步代码块。这是一种互斥体,同一时间只有一个线程能持有这个锁。
15. 重入:线程访问其他线程正在持有的锁时会阻塞。如果访问自己持有的锁可以成功,则称为可重入的。Java的内置锁是可重入的,这个和pthread的互斥体不同。pthread是基于调用的。重入意味着获取锁的粒度是线程,而不是调用。可重入锁的一种实现是计数每个线程进入同步代码块的次数。只要有一个同步代码块没有退出,锁就不能释放。
Questions:
1. wait 和notify采用的底层机制是什么?
2. AbstractQueuedSynchronizer 是如何实现的?以及如果产生一个具体类,来实现自定义的显式加锁?
内部是一个CLH锁,使用一个队列来维护等待的线程。如果当前线程调用lock,则会对当前线程调用park使得当前线程不参与系统调度,同时把当前线程作为节点加入到等待队列中。当一个线程要释放锁的时候,会对下一个线程调用unpark方法,恢复系统对其调度。这个很像线程切换的机制。对于信号量,除了维护这个队列,还要维护一个剩余可用的数目,当小于或等于0时,加入等待队列。
3. 如何实现公平、非公平?
对于公平锁,每次acquire之前,先查看队列尾 tail 是否为空,如果为空,且没有线程正在使用这个锁(state的值为0,因为可重入,所以如果当前线程已经持有锁,则只是将计数器加一,这是为了保证lock和unlock次数相等;如果其他线程持有锁,则不能获取该锁),当前线程才能获取这个锁。对于非公平锁,在一开始获取锁的时候,就执行compareAndSet(state, 0, 1)。也就是,看看有没有线程正在持有锁,如果没有线程持有锁,则立即获取锁,而不管队列中是否有线程等待。这样避免了唤醒其他线程的开销,效率会更高。ReentrantLock默认是非公平锁。
4. Java线程池是如何实现的?
BlockingQueue
5. Java的BlockingQueue/NonBlockingQueue如何实现的?
6. Future, Promise, CompletableFuture是如何实现的?