本篇文章是对https://github.com/CyC2018/CS-Notes的学习
目录
await() singnal() singnalAll()
线程和进程
进程:一个正在执行的应用程序,是资源分配的基本单位。
线程:是进程中一个一个执行流成,是CPU调度的基本单元,一个进程至少有一个线程。
进程间是相互独立的,不能共享资源,一个进程至少有一个线程,同一个进程中的各个线程共享整个进程的资源。切换进程的开销较大,切换线程的开销较小。
1 线程状态转换
- 新建(NEW):新创建了一个线程对象,但是还没有调用start()方法。创建新线程的方法有三种,后面再说。
- 运行(Runable):就绪(Ready)和运行中(Running)都属于运行状态。就绪状态是指线程被创建之后,其它线程调用了该线程的start()方法。该状态的线程位于可运行的线程池中,等待cpu调度选中,获得cpu的使用权。就绪状态的线程获得了cpu时间片之后就变成了运行中状态。
就绪状态:
- 就绪状态就是说线程有资格运行,但是cpu没有选中你,就永远处于就绪状态
- 调用线程的start()方法线程进入就绪状态
- 当前线程的sleep()结束,其它线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程都进入就绪状态。
- 当前线程的时间片用完,调用当前线程的yeild方法,该线程进入就绪状态
- 锁池里的线程拿到对象锁之后,就进入就绪状态。
运行中状态:
- 线程调度程序从线程池中选择一个作为当前线程时线程所处的状态,这也时线程进入运行中状态的唯一一种方法。
- 阻塞(BLOCKED):线程在等待一个排它锁,线程阻塞于锁,等到其它线程释放了锁就会结束次状态。意思就是线程要运行被synchornize修饰的方法或代码块,但是其它线程占用了锁,等其它线程释放了锁才能获取该锁从而结束此状态。
- 无限期等待(WAITING)
等待其它线程显示的唤醒,否则不会分配CPU资源。
LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒。
- 限期等待(TIMED WAITING)
可以在指定的时间后自行返回,无需其它线程显示的唤醒。
通常叫thread.sleep()为使一个线程睡眠,object.wait()叫使一个线程挂起。
等待状态的进入方法和退出方法:
- 死亡(TERMINATED)
可以是线程结束任务之后自己结束,或者是产生了异常而结束。
2 使用线程
- 实现Runable接口,需要实现run方法,将接口传给新建的Thread作为构造函数,通过thread.start启动线程
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
- 实现Callable接口,实现call方法,callable可以有返回值,返回值通过FutureTask 封装,再作为Thread的构造函数,启动方法一样。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
- 继承Thread,实现run方法,通过Start调用。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
使用runable和callable接口的类只能当作一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要用thread来调用,可以说任务是通过线程驱动而执行的。
当调用start()方法启动一个线程的时候,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法。
实现接口VS继承
实现接口会更好一些,因为Java不支持多重继承,继承了Thread就无法继承其它类,但是可以实现多个接口。类可能只要求可执行就行,继承整个thread类的开销过大。
3 基础线程机制
-
Executor
Excecutor提供了一个线程池的方法来开启多个线程。Excecutor将任务的提交和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,其执行任务的线程相当于消费者。理解就是可以通过void execute(Runnable command);方法加入多个线程,然后让excutor来对这些线程进行管理。注意,excetuor管理的线程必须是异步的,不需要进行同步操作。意思就是通过Excutor来创建线程池,由线程池来对线程进行管理。
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
Excetuor提供了四种创建线程池的静态工厂方法:
CachedThreadPool线程池
创建一个可缓存的线程池,若有需求可以回收空闲的线程进行使用,如果没有空闲的线程就创建一个新的线程。
FixedThreadPool线程池
创建一个固定大小的线程池,如果业务超出线程数量,那就进行排队,排队的时候有线程任务结束就立即拿过来使用,如果没有超出线程数量就直接创建新的线程。
ScheduledThreadPool线程池
创建一个固定长度的线程池,支持定时和周期性执行
SingleThreadExecutor线程池
创建一个单线程的线程池,所有任务都是用这个线程来执行的,保证了所有任务的执行顺序,还保证了当一个任务发生异常时,还会继续执行下去。
-
Daemon
守护线程是在程序运行时提供后台服务的线程,不属于程序中不可或缺的一部分,当程序的所有非守护线程结束的时候,程序也就终止了,同时也会杀死守护线程。典型的守护线程如垃圾回收线程。main()属于非守护线程。使用setDeamon()将一个线程设置为守护线程
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
-
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep()可能会抛出InterruptedException异常,因为异常不能跨线程传回main()中,必须在本地进行处理,线程中抛出的异常也必须在本地进行处理。
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
-
yield()
对静态方法Thread.yield()的调用声明了当前线程已经完成了声明周期中最重要的部分,可以切换给其它线程来使用,这只是对线程调度器的一个建议,而且只是建议具有相同优先级的其它线程可以运行。
public void run() {
Thread.yield();
}
4 中断
-
Interrput中断
只要线程阻塞配合interrput()都能使线程停止,而且停止的方式都是抛异常。通过调用线程的interrput()方法,如果线程处于阻塞状态(sleep(),wait(),join()),就会抛出一个InterruptedException,从而提前结束线程不执行后面的语句。不能结束I/O阻塞和synchornize阻塞。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
interrput()方法不会真正的中断线程,只是在线程中打了一个中断的线程将中断表示改为true,以上阻塞方法遇到中断表示后就会抛异常中断线程。也可以调用interrupted()方法,若方法返回true,则说明中断表示为true,可以自己使程序中断,从而提前结束线程。
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();
}
Thread end
-
Executuor中的中断操作
调用Executuor的shutdown()方法会等待线程执行完毕之后再关闭,但是如果调用的是shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow();
System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
如果指向中断Executuor的一个线程,可以使用submit()方法来提交一个线程,它会返回一个Future<>对象,通过调用该对象的cancel(true)方法就可以中断线程。
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
5 互斥同步
Java提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM实现的synchronized,而另一个是JDK实现的ReentrantLock
-
Synchronized
锁其实就是一个对象,java中的每一个对象都能够作为锁。
在使用synchronized的时候,
同步一个代码块,指定锁对象:
public void func() {
synchronized (this) {
// ...
}
}
同步代码块的锁对象为this,为当前方法的对象,只能作用于同一个对象。
同步一个方法:
public synchronized void func () {
// ...
}
和同步代码块一样,也只能作用于同一个对象
同步一个类:
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
作用于整个类,也就是说两个线程调用同一个类的不同对象上的同步语句,也会进行同步
同步一个静态代码块:
public synchronized static void fun() {
// ...
}
也是作用于整个类
-
ReentrantLock
ReentrantLock是java.util.concurrent(J.U.C)包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
ReentrantLock是一种可重入且独占式的锁,于synchronized相比,增加了轮询、超时、中断等高级功能。它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外该锁还支持获取锁的公平和非公平选择。
可重入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞。
与synchronized比较:
- 锁的实现:synchronized是JVM实现的,ReentrantLock是JDK实现的
- 性能:新版的java对synchronized进行了很多优化,使之与ReentrantLock的性能大致相同
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,ReentrantLock可中断,synchronized不可中断。
- 公平锁:是指多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来一次获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但是也可以是公平的,通过不同的构造函数来实现。
- 锁绑定多个条件:一个ReentrantLock可以同时绑定多个Conditin对象,Condition对象可以实现线程之间的协作。
6 线程之间的协作
当多个线程可以一起工作去解决某个问题的时候,如果需要按照特定的顺序执行任务,比如一个任务必须要在另一个任务之后执行,就需要线程之间进行协作。
-
join()
当一个线程需要另外一个线程执行之后再执行,就调用另外一个线程的join()方法,当另外一个线程执行完之后再回来执行该线程。在线程中调用一个线程的join()方法的时候,该线程会挂起,直到目标线程结束。
-
wait() notify() notifyAll()
调用wait()使得线程等待某个条件满足,线程在等待期间会被挂起,当其它线程的运行使得这个条件满足时,其它线程会调用notify()或notifAlly()方法唤醒挂起的线程。他们是属于Object的一部分,而不是Thread。
只能用在同步方法或同步代码块中,否则会在运行时抛出IllegalMonitorStateException。
使用wait()挂起期间,线程会释放锁。不是放锁,其它方法就不能进入同步方法或同步代码块中,就无法执行notify()和notifyAll()来唤醒挂起的线程。
wait()和sleep()的区别:
wait()是Object的方法,而sleep()是Thread的静态方法
wait()会释放锁,sleep()不会。
-
await() singnal() singnalAll()
java.util.concurrent中提供了Condition类来实现多个线程的通信,可以调用conditin的await()使线程等待,其它线程调用singnal() singnalAll()来唤醒等待的线程。同样等待和唤醒要在同步代码块内。
相比于wait()这种等待方式,await()可以指定等待时间,更加灵活。
用lock对象来获取一个Condition对象
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
before
after
7 J.U.C-AQS
java.util.concurrent(J.U.C)大大提高了并发性能,AQS被认为使J.U.C的核心
什么是AQS?
AQS:AbstractQueuedSynchronizer队列同步器,它是构建锁或者其它同步组件的基础框架。
-
CountDownLatch
用来控制一个线程等待多个线程,维护了一个计数器cnt,每次调用countDown()方法会让计数器的值减1,减到0的时候,那些因为调用await()方法而在等待的线成就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
run..run..run..run..run..run..run..run..run..run..end
-
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
内部维护了一个计数器,线程执行await()方法后计数器减1,当计数器为0的时候,所有调用await()方法而在等待的线程被唤醒继续执行。
与CountDownLatch不同的是,CyclicBarrier通过调用reset()方法可以循环使用
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..af
-
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能3个客户端同时访问,请求总数为10
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
2 1 2 2 2 2 2 1 2 2
8 J.U.C-其它组件
-
FutureTask
Callable接口可以有返回值,返回值通过Future封装。FutureTask实现了RunnableFuture接口,该接口继承至Runnable和Future接口,这使得FutureTask既可以当一个任务执行,也可以有返回值。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask可用于异步获取执行结果或取消任务的场景。当一个计算任务需要执行很长时间,那么就可以用FutureTask来封装这个任务,主线程在完成自己的任务之后再去获取结果。
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
otherThread.start();
System.out.println(futureTask.get());
}
}
other task is running...
4950
-
BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- FIFO队列:LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
- 优先级队列:PriorityBlockingQueue
提供了阻塞的take()和put()方法,如果队列为空,那么take()将阻塞,直到队列中有内容,如果队列为满put()将阻塞,直到队列有了空闲位置。
使用BlockingQueue实现生产者消费者问题
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
-
ForkJoin
主要用于并行计算中,和MapReduce原理类似,都是把大的计算任务拆分成多个小任务并行计算。
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
ForkJoin使用ForkJoinPool来启动,它是一个特殊的线程池,线程数量取决于CPU核数。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool实现了工作窃取算法来提高CPU的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务,当一个线程处于空闲状态的时候,可以拿出其它线程的任务来执行,拿出的必须是最晚的任务,防止线程发生竞争。
9 线程不安全实例
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了1000个线程对同一个数据cnt进行自增操作,结果可能小于1000。
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
997
10 java内存模型
java内存模型试图屏蔽各种硬件核操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
-
主内存与工作内存
处理器上的寄存器的读写的速度比内存块几个数量级,为了解决这种速度矛盾,在他们之间加入了高速缓存。
加入高速缓存带来的问题:缓存一致性。多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在寄存器或者告诉缓存中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成
-
内存间交互操作
java内存模型定义了8个操作来完成主内存和工作内存的交互操作
- read:作用于主内存,把一个变量的值从主内存传递到工作内存中,以便后面工作内存load使用
- load:作用于工作内存,在read之后执行,把read得到的值放入工作内存的变量副本中
- use:作用于工作内存,把工作内存中的一个变量的值传递给执行引擎
- assign:作用于工作内存,把一个从执行引擎接收到的值赋给工作内存中的变量
- store:作用于工作内存,把工作内存中的一个变量的值传送到主内存中
- write:作用于主内存,在store之后执行,把store得到的值放入主内存的变量中
- lock:作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock:作用于主内存的变量,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他的线程锁定
java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write单独出现(即不允许一个变量从主内存读取了但是工作内存不接受,或者从工作内存发起写了但是主内存不接受的情况),以上操作必须按顺序执行,但是可以不连续执行,也就时说read和load之间,store和write之间可以插入其他指令。
- 不允许线程丢弃它最近的assign操作,即变量在工作内存种改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存中同步回主内存。
- 一个变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说一个变量在use和store操作之前,必须先经过load和assign操作
- 一个变量在同一时刻只允许一个线程对其执行lock操作,但lock操作可以被同一个线程反复执行多次,多次执行lock之后,只有执行相同次数的unlock操作,变量才会被解锁(所谓的ReentrantLock是一种可重入且独占式的锁)
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
- 对一个变量执行unlock操作之前,必须先把这个变量同步回主内存(执行store和write操作)
-
内存模型的三大特性
理解一下volatile?
volatile:本意是不稳定的,易挥发的,也就是说用它修饰的变量是可变的。
在多线程的环境下,线程可以将线程间共享的变量保存在本地内存(寄存器)中,而不是从内存中读取,这就可能引发不一致的问题,另一个线程可能在此线程运行期间改变了变量的值,而此线程没有看到变化。
而volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且当成员变量发生变化时,强迫线程将变化值写到共享内存中。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
java语言规范中指出:为了获得最佳速度,允许线程保存共享成员的拷贝,而且只当线程进入或者离开同步代码块时才共享成员变量的原始值变化。
也就是说,JVM会对线程变量的访问进行优化,这样当多个线程同时与某个对象交互时,就必须注意到要让线程及时的带共享成员变量的变化。
而volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应该直接与共享成员变量交互。并且禁止指令的重排序优化。
使用建议:
在两个或者更多的线程访问成员变量上使用volatile
当要访问的变量已在synchornized中,或者为常量时,不必使用
volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定要在必要时才使用此关键字。
理解:在多线程环境下,为了提高运行效率,线程可以维护一个共享变量的拷贝在工作内存中,只有当线程进入或离开同步代码块的时候才会将变量的变化与其他线程共享,这时JVM的一个优化,提高了程序的运行效率。而volatile就是不允许线程维护一共享变量的拷贝,每次都要从主内存中获取,每次都要将变化更新到主内存中,不允许JVM的优化,这样做的话能够维护变量的一致性,但是效率不高。
- 原子性
什么是原子性?原子性是指一个操作不可中断,要么全部执行完成,要么全部执行失败,有着“共同生死”的感觉。
java的存储模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个int类型的变量执行assign复制操作,这个操作就是原子性的。但是java内存模型允许虚拟机将没有被volatile修饰的64位数据(long,double)的读写操作划分为两次32位的操作来进行,即read、load、store、write不具备原子性。
有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt属于int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
理解:java内存模型中的交互操作对于原子性的类型如int单个操作是具备原子性的,在多线程中仍然有可能引发线程不安全。对于没有被volatile修饰的64位数据允许两次操作就不具备原子性。
理解一下AtomicInteger ?
AtomicInteger 原子类操作。使用volatile变量可以使线程直接与主内存进行交互,但是也不能保证线程安全,因为它不能保证造操作的原子性。AtomicInteger.incrementAndGet()方法的原子性保证了线程的安全性。
AtomicInteger 能保证多个线程修改的原子性
使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
public int get() {
return cnt.get();
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // 只修改这条语句
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
1000
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample {
private int cnt = 0;
public synchronized void add() {
cnt++;
}
public synchronized int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
1000
- 可见性
什么是可见性?可见性指当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。Java内存模型是通过变量修改后将新值同步回内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有三种实现可见性的方式:
- volatile
- synchronized,对一个变量执行unlock操作之前,必须把变量同步回主内存
- final,被final关键字修饰的字段在构造器中一旦初始完成,并且没有发生this逃逸(其他线程通过this引用访问到初始化了一半的对象),那么其他线程就能看见final字段的值。
对前面的线程不安全实例中的cnt变量使用volatile修饰,不能解决线程安全问题,因为valotile并不能保证操作的原子性。
- 有序性
什么是有序性?有序性是指,在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
-
先行发生原则
上面提到了volatile和synchronized来保证有序性。除此之外JVM还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
- 单一线程原则
在一个线程内,在程序前面的操作先行发生于后面的操作。
- 管程锁定原则
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
- volatile变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
- 线程加入规则
Thread 对象的结束先行发生于 join() 方法返回。
- 线程中断规则
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
- 对象中断规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
了解一下finalize()?是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)
- 传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
11 线程安全
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。线程安全有以下几种实现形式:
-
不可变
不可变(Immutable)的对象一定是线程安全的,不需要采取任何的线程安全保障措施。只要一个不可变的对象被正确的构建出来,永远也不会看到他在多个线程之中处于不一致的状态。多线程环境下,应该尽量使对象成为不可变,来满足线程安全。
不可变的类型:
- final关键字修饰的基本数据类型
- String
- 枚举类型
- Number部分子类,如Long和Double等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为Number的原子类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,可使用Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", 1);
}
}
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
-
互斥同步
synchronized和Reentrantlock
-
非阻塞同步
互斥同步的缺陷:线程阻塞和唤醒带来的性能问题,这个同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步选择,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的锁)、用户核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
- CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发政策:先进行操作,如果没有其它线程争用共享数据,那么操作就成功了,否则采取补偿措施(不断地尝试,直到成功为止)。这种乐观地并发策略地许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性:这里就不能使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-swap,CAS)。CAS指令需要三个操作数,分别是内存地址V,旧的预期值A和新值B。当执行操作,只有当V的值等于A的值,才将V更新为新值B。
理解:操作并检测冲突,若V与A不相等,说明在进行操作的时候有其他线程对V值进行了更改,就放弃这次操作,不断尝试,重新进行操作和冲突检测直到成功为止。
- AtomicInteger
J.U.C包中的整数原子类AtomicInteger 的方法调用了Unsafe类的CAS操作。
以下代码使用了AtomicInteger 执行了自增操作
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
以下代码是getAndAddInt()源码,var1指示对象内存地址,var2指示该字段相对对象内存地址的便宜,var4指示操作需要加的数值,这里为1.通过getIntVolatile(var1, var2)得到旧的预期值,通过调用compareAndSwapInt(var1, var2, var5, var5 + var4)来进行CAS比较,如果该字段内存地址中的值等于var5,那么就更新内存地址为var1+var2的变量为var5+var4。可以看到getAndAddInt()在一个循环中进行,发生冲突的做法是不断的进行重试。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- ABA
如果一个变量初次读取的时候是A值,它的值被改成了B,后来又被改回为A,那么CAS操作就会误认为它从来没被改变过。
J.U.C提供了一个带有标记的原子引用类AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证CAS的正确性。大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,采用传统的互斥同步可能会比原子类更高效。
-
无同步方案
要确保线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无需做任何同步措施去保证正确性。
- 栈封闭
多个线程访问同一个方法的局部变量的时候,不会出现线程安全问题,因为局部变量存在于虚拟机栈中,属于线程私有的。
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
100
100
- 线程本地存储
如果一段代码中所需要的数据必须与其它线程共享,那就看看这些共享数据的代码能否保证在同一个线程中执行。如果能保证,可以把共享数据的可减范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用问题。
理解:两个线程的两端代码需要同一个共享数据,就把这两段代码放在同一个线程中,再将线程的可减范围缩小在一个线程里面。
符合这种特点的应用并不少见,大部分使用消息队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的应用实例就是经典web交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多web服务端应用都可以使用本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 来实现线程本地存储。就是将一个线程中的数据存储在线程对应的工作内存中
对于以下代码,thread1中设置threadLocal为1,thread2中设置threadLocal为2,过了一段时间,thread1读取的threadLocal依旧为1,不受thread2的影响。
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
1
为了理解 ThreadLocal,先看以下代码:
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2);
});
thread1.start();
thread2.start();
}
}
它对应的底层结构图为
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get() 方法类似。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal 从理论上来讲并不是解决多线程并发问题的,因为根本就不存在多线程竞争。
在一些场景(尤其是使用线程池)下,由于ThreadLocal .ThreadLocalMap的底层数据结构导致ThreadLocal有内存泄漏的情况,应该尽可能在每次使用ThreadLocal后手动调用 remove(),以避免出现ThreadLocal 经典的内存泄露甚至是造成自身业务混乱风险。
理解:就是讲一个线程的变量存储在该线程的工作内存区域。
内存泄漏?申请了一块内存,后来很长的时间都不再使用(按理应该释放),但是因为一直被某个或者某些实例占用导致GC不能回收,也就是该被释放的对象没有释放。
- 可重入代码
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,后来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不能依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数传入的、不调用非可重入的方法。
12 锁优化
锁优化主要是指JVM对synchronized的优化。
-
自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态指挥持续很短的一段时间,自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果这段时间能够获得锁,就可以避免进入阻塞状态。
自旋锁虽然能够避免进入阻塞状态从而减少开销,但是它需要进行忙循环而占用CPU时间,它只适用于共享数据锁定状态很短的场景。
在jdk1.6中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数以及拥有者的状态来决定
-
锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
缩消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把他们当成私有数据对待,也就可以对它们的锁进行消除。
对于一些看起来没有加锁的数据,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String是一个不可变类,编译器会对String的拼接自动优化。在jdk1.5之前,会转化为StringBuffer 对象的连续append() 操作:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个append() 方法中都有一个同步块。虚拟机观察变量sb,很快就会发现它的动态作用域被限制在concatString() 方法内部。也就是说,sb的所有引用永远不会逃逸到concatString() 方法之外,其它线程无法访问到它,因此可以进行消除。
-
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
-
轻量级锁
jdk1.6引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
-
偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
13 多线程开发良好的实践
- 给线程起一个有意义的名字,这样可以方便查找bug
- 缩小同步范围,从而减少锁征用。例如对于synchronized,应该尽量使用同步快而不是同步方法
- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
- 使用 BlockingQueue 实现生产者消费者问题。
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
- 使用本地变量和不可变类来保证线程安全。
- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。