Java修炼之道--并发编程

原做地址:https://github.com/frank-lam/2019_campus_applyjavascript

前言

在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通讯、并发包部分。主要分红 “并发编程” 和 “面试指南” 两 部分,在面试指南中将讨论并发相关面经。html

参考资料:java

  • 《Java并发编程实战》

第一部分:并发编程

1. 线程状态转换

在这里插入图片描述

新建(New)

建立后还没有启动。linux

可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片。git

包含了操做系统线程状态中的 运行(Running ) 和 就绪(Ready)。程序员

阻塞(Blocking)

这个状态下,是在多个线程有同步操做的场景,好比正在等待另外一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是线程在等待进入临界区。github

阻塞能够分为:等待阻塞,同步阻塞,其余阻塞面试

无限期等待(Waiting)

等待其它线程显式地唤醒,不然不会被分配 CPU 时间片。算法

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 -

限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在必定时间以后会被系统自动唤醒。数据库

调用 Thread.sleep() 方法使线程进入限期等待状态时,经常用 “使一个线程睡眠” 进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,经常用 “挂起一个线程” 进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,经过调用 Thread.sleep() 和 Object.wait() 等方法进入。

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

死亡(Terminated)

  • 线程由于 run 方法正常退出而天然死亡
  • 由于一个没有捕获的异常终止了 run 方法而意外死亡

2. Java实现多线程的方式及三种方式的区别

有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当作一个能够在线程中运行的任务,不是真正意义上的线程,所以最后还须要经过 Thread 来调用。能够说任务是经过线程驱动从而执行的。

实现 Runnable 接口

须要实现 run() 方法。

经过 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 接口

与 Runnable 相比,Callable 能够有返回值,返回值经过 FutureTask 进行封装。

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() 方法,由于 Thread 类也实现了 Runable 接口。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,由于:

  • Java 不支持多重继承,所以继承了 Thread 类就没法继承其它类,可是能够实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

三种方式的区别

  • 实现 Runnable 接口能够避免 Java 单继承特性而带来的局限;加强程序的健壮性,代码可以被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程区处理同一资源的状况。
  • 继承 Thread 类和实现 Runnable 方法启动线程都是使用 start() 方法,而后 JVM 虚拟机将此线程放到就绪队列中,若是有处理机可用,则执行 run() 方法。
  • 实现 Callable 接口要实现 call() 方法,而且线程执行完毕后会有返回值。其余的两种都是重写 run() 方法,没有返回值。

3. 基础线程机制

Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不须要进行同步操做。

主要有三种 Executor:

  • CachedThreadPool:一个任务建立一个线程;
  • FixedThreadPool:全部任务只能使用固定大小的线程;
  • SingleThreadExecutor:至关于大小为 1 的 FixedThreadPool。
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}

为何引入Executor线程池框架?

new Thread() 的缺点

  • 每次 new Thread() 耗费性能
  • 调用 new Thread() 建立的线程缺少管理,被称为野线程,并且能够无限制建立,之间相互竞争,会致使过多占用系统资源致使系统瘫痪。
  • 不利于扩展,好比如定时执行、按期执行、线程中断

采用线程池的优势

  • 重用存在的线程,减小对象建立、消亡的开销,性能佳
  • 可有效控制最大并发线程数,提升系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供定时执行、按期执行、单线程、并发数控制等功能

Daemon(守护线程)

Java 中有两类线程:User Thread (用户线程)、Daemon Thread (守护线程)

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程做用是为其余前台线程的运行提供便利服务,并且仅在普通、非守护线程仍然运行时才须要,好比垃圾回收线程就是一个守护线程。当 JVM 检测仅剩一个守护线程,而用户线程都已经退出运行时,JVM 就会退出,由于没有若是没有了被守护这,也就没有继续运行程序的必要了。若是有非守护线程仍然存活,JVM 就不会退出。

守护线程并不是只有虚拟机内部提供,用户在编写程序时也能够本身设置守护线程。用户能够用 Thread 的 setDaemon(true) 方法设置当前线程为守护线程。

虽然守护线程可能很是有用,但必须当心确保其余全部非守护线程消亡时,不会因为它的终止而产生任何危害。由于你不可能知道在全部的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦全部的用户线程退出了,虚拟机也就退出运行了。 所以,不要在守护线程中执行业务逻辑操做(好比对数据的读写等)。

另外有几点须要注意:

  • setDaemon(true) 必须在调用线程的 start() 方法以前设置,不然会跑出 IllegalThreadStateException 异常。
  • 在守护线程中产生的新线程也是守护线程。
  • 不要认为全部的应用均可以分配给守护线程来进行服务,好比读写操做或者计算逻辑。

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

当全部非守护线程结束时,程序也就终止,同时会杀死全部守护线程。

main() 属于非守护线程。

使用 setDaemon() 方法将一个线程设置为守护线程。

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();
}

线程阻塞

线程能够阻塞于四种状态:

  • 当线程执行 Thread.sleep() 时,它一直阻塞到指定的毫秒时间以后,或者阻塞被另外一个线程打断;
  • 当线程碰到一条 wait() 语句时,它会一直阻塞到接到通知 notify()、被中断或通过了指定毫秒时间为止(若制定了超时值的话)
  • 线程阻塞与不一样 I/O 的方式有多种。常见的一种方式是 InputStream 的 read() 方法,该方法一直阻塞到从流中读取一个字节的数据为止,它能够无限阻塞,所以不能指定超时时间;
  • 线程也能够阻塞等待获取某个对象锁的排他性访问权限(即等待得到 synchronized 语句必须的锁时阻塞)。

注意,并不是全部的阻塞状态都是可中断的,以上阻塞状态的前两种能够被中断,后两种不会对中断作出反应

4. 中断

一个线程执行完毕以后会自动结束,若是在运行过程当中发生异常也会提早结束。

InterruptedException

经过调用一个线程的 interrupt() 来中断该线程,若是该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提早结束该线程。可是不能中断 I/O 阻塞和 synchronized 锁阻塞。

对于如下代码,在 main() 中启动一个线程以后再中断它,因为线程中调用了 Thread.sleep() 方法,所以会抛出一个 InterruptedException,从而提早结束线程,不执行以后的语句。

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)

interrupted()

若是一个线程的 run() 方法执行一个无限循环,而且没有执行 sleep() 等会抛出 InterruptedException 的操做,那么调用线程的 interrupt() 方法就没法使线程提早结束。

可是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。所以能够在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提早结束线程。

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

Executor 的中断操做

调用 Executor 的 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)

若是只想中断 Executor 中的一个线程,能够经过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,经过调用该对象的 cancel(true) 方法就能够中断线程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);

5. 互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另外一个是 JDK 实现的 ReentrantLock。

synchronized

1. 同步一个代码块

public void func() {
    synchronized (this) {
        // ...
    }
}

它只做用于同一个对象,若是调用两个对象上的同步代码块,就不会进行同步。

对于如下代码,使用 ExecutorService 执行了两个线程,因为调用的是同一个对象的同步代码块,所以这两个线程会进行同步,当一个线程进入同步语句块时,另外一个线程就必须等待。

public class SynchronizedExample {
    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于如下代码,两个线程调用了不一样对象的同步代码块,所以这两个线程就不须要同步。从输出结果能够看出,两个线程交叉执行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

2. 同步一个方法

public synchronized void func () {
    // ...
}

它和同步代码块同样,做用于同一个对象。

3. 同步一个类

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

做用于整个类,也就是说两个线程调用同一个类的不一样对象上的这种同步语句,也会进行同步。

public class SynchronizedExample {
    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. 同步一个静态方法

  • 非静态同步函数的锁是:this
  • 静态的同步函数的锁是:字节码对象
public synchronized static void fun() {
    // ...
}

做用于整个类。

ReentrantLock

重入锁(ReentrantLock)是一种递归无阻塞的同步机制。

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(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了如下高级功能:

1. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程能够选择放弃等待,改成处理其余事情。

2. 可实现公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁。

synchronized 中的锁是非公平的,ReentrantLock 默认状况下也是非公平的,但能够经过带布尔值的构造函数要求使用公平锁。

3. 锁绑定多个条件

一个 ReentrantLock 对象能够同时绑定多个 Condition 对象。

synchronized 和 ReentrantLock 比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了不少优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,所以性能因素再也不是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。

3. 功能

ReentrantLock 多了一些高级功能。

4. 使用选择

除非须要使用 ReentrantLock 的高级功能,不然优先使用 synchronized。这是由于 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是全部的 JDK 版本都支持。而且使用 synchronized 不用担忧没有释放锁而致使死锁问题,由于 JVM 会确保锁的释放。

synchronized与lock的区别,使用场景。看过synchronized的源码没?

  • (用法)synchronized(隐式锁):在须要同步的对象中加入此控制,synchronized 能够加在方法上,也能够加在特定代码块中,括号中表示须要锁的对象。
  • (用法)lock(显示锁):须要显示指定起始位置和终止位置。通常使用 ReentrantLock 类作为锁,多个线程中必需要使用一个 ReentrantLock 类作为对象才能保证锁的生效。且在加锁和解锁处须要经过 lock() 和 unlock() 显示指出。因此通常会在 finally 块中写 unlock() 以防死锁。
  • (性能)synchronized 是托管给 JVM 执行的,而 lock 是 Java 写的控制锁的代码。在 Java1.5 中,synchronize 是性能低效的。由于这是一个重量级操做,须要调用操做接口,致使有可能加锁消耗的系统时间比加锁之外的操做还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。可是到了 Java1.6 ,发生了变化。synchronize 在语义上很清晰,能够进行不少优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。致使 在 Java1.6 上 synchronize 的性能并不比 Lock 差。
  • (机制)synchronized 原始采用的是 CPU 悲观锁机制,即线程得到的是独占锁。独占锁意味着其余线程只能依靠阻塞来等待线程释放锁。Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操做(Compare and Swap)。

什么是CAS

蘑菇街面试,这里简单论述一下

入门例子

在 Java 并发包中有这样一个包,java.util.concurrent.atomic,该包是对 Java 部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操做方法,保证了线程安全。下面以 AtomicInteger 为例,来看一下是如何实现的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

public final int decrementAndGet() {
    for (;;) {
        int current = get();
        int next = current - 1;
        if (compareAndSet(current, next))
            return next;
    }
}

以这两个方法为例,incrementAndGet 方法至关于原子性的 ++i,decrementAndGet 方法至关于原子性的 --i,这两个方法中都没有使用阻塞式的方式来保证原子性(如 Synchronized ),那它们是如何保证原子性的呢,下面引出 CAS。

Compare And Swap

CAS 指的是现代 CPU 普遍支持的一种对内存中的共享数据进行操做的一种特殊指令。这个指令会对内存中的共享数据作原子的读写操做。

简单介绍一下这个指令的操做过程:

  • 首先,CPU 会将内存中将要被更改的数据与指望的值作比较。
  • 而后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。不然便不作操做。
  • 最后,CPU 会将旧的数值返回。

这一系列的操做是原子的。它们虽然看似复杂,但倒是 Java 5 并发机制优于原有锁机制的根本。简单来讲,CAS 的含义是:我认为原有的值应该是什么,若是是,则将原有的值更新为新值,不然不作修改,并告诉我原来的值是多少。
​ 简单的来讲,CAS 有 3 个操做数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改成 B,不然返回 V。这是一种乐观锁的思路,它相信在它修改以前,没有其它线程去修改它;而 Synchronized 是一种悲观锁,它认为在它修改以前,必定会有其它线程去修改它,悲观锁效率很低。

什么是乐观锁和悲观锁

  • 为何须要锁(并发控制)
    • 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
    • 典型的冲突有:
      • 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户 A 把值从 6 改成 2,用户 B 把值从 2 改成 6,则用户 A 丢失了他的更新。
      • 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户 A,B 看到的值都是6,用户 B 把值改成 2,用户 A 读到的值仍为 6。
    • 为了解决这些并发带来的问题。 咱们须要引入并发控制机制。
  • 并发控制机制
    • 悲观锁:假定会发生并发冲突,独占锁,屏蔽一切可能违反数据完整性的操做。
    • 乐观锁:假设不会发生并发冲突,只在提交操做时检查是否违反数据完整性。乐观锁不能解决脏读的问题。

参考资料:

Synchronized(对象锁)和Static Synchronized(类锁)区别

  • 一个是实例锁(锁在某一个实例对象上,若是该类是单例,那么该锁也具备全局锁的概念),一个是全局锁(该锁针对的是类,不管实例多少个对象,那么线程都共享该锁)。

    实例锁对应的就是 synchronized关 键字,而类锁(全局锁)对应的就是 static synchronized(或者是锁在该类的 class 或者 classloader 对象上)。

/**
 * static synchronized 和synchronized的区别!
 * 关键是区别第四种状况!
 */
public class StaticSynchronized {

    /**
     * synchronized方法
     */
    public synchronized void isSynA(){
        System.out.println("isSynA");
    }
    public synchronized void isSynB(){
        System.out.println("isSynB");
    }

    /**
     * static synchronized方法
     */
    public static synchronized void cSynA(){
        System.out.println("cSynA");
    }
    public static synchronized void cSynB(){
        System.out.println("cSynB");
    }

    public static void main(String[] args) {
        StaticSynchronized x = new StaticSynchronized();
        StaticSynchronized y = new StaticSynchronized();
        /**
         *  x.isSynA()与x.isSynB(); 不能同时访问(同一个对象访问synchronized方法)
         *  x.isSynA()与y.isSynB(); 能同时访问(不一样对象访问synchronized方法)
         *  x.cSynA()与y.cSynB(); 不能同时访问(不一样对象也不能访问static synchronized方法)
         *  x.isSynA()与y.cSynA(); 能同时访问(static synchronized方法占用的是类锁,
         *                        而访问synchronized方法占用的是对象锁,不存在互斥现象)
         */
    }
}

6. 线程之间的协做

当多个线程能够一块儿工做去解决某个问题时,若是某些部分必须在其它部分以前完成,那么就须要对线程进行协调。

join()

在线程中调用另外一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

对于如下代码,虽然 b 线程先启动,可是由于在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,所以最后可以保证 a 线程的输出先于 b 线程的输出。

public class JoinExample {
    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {
        private A a;
        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
A
B

wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件知足,线程在等待时会被挂起,当其余线程的运行使得这个条件知足时,其它线程会调用 notify()(随机叫醒一个) 或者 notifyAll() (叫醒全部 wait 线程,争夺时间片的线程只有一个)来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用!不然会在运行时抛出 IllegalMonitorStateExeception。

使用 wait() 挂起期间,线程会释放锁。这是由于,若是没有释放锁,那么其它线程就没法进入对象的同步方法或者同步控制块中,那么就没法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,形成死锁。

public class WaitNotifyExample {
    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,能够在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 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

sleep和wait有什么区别

  • sleep 和 wait
    • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
    • wait() 会释放锁,sleep() 不会。
  • 有什么区别
    • sleep() 方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其余线程,可是对象的锁依然保持,所以休眠时间结束后会自动恢复(线程回到就绪状态)。
    • wait() 是 Object 类的方法,调用对象的 wait() 方法致使当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify() 方法(或 notifyAll() 方法)时才能唤醒等待池中的线程进入等锁池(lock pool),若是线程从新得到对象的锁就能够进入就绪状态。

7. J.U.C - AQS

AQS 是 AbstractQueuedSynchronizer 的简称,java.util.concurrent(J.U.C)大大提升了并发性能,AQS (AbstractQueuedSynchronizer) 被认为是 J.U.C 的核心。它提供了一个基于 FIFO 队列,这个队列能够用来构建锁或者其余相关的同步装置的基础框架。下图是 AQS 底层的数据结构:

它底层使用的是双向列表,是队列的一种实现 , 所以也能够将它当成一种队列。

  • Sync queue 是同步列表,它是双向列表 , 包括 head,tail 节点。其中 head 节点主要用来后续的调度 ;
  • Condition queue 是单向链表 , 不是必须的 , 只有当程序中须要 Condition 的时候,才会存在这个单向链表 , 而且可能会有多个 Condition queue。

简单的来讲:

  • AQS其实就是一个能够给咱们实现锁的框架

  • 内部实现的关键是:先进先出的队列、state 状态

  • 定义了内部类 ConditionObject

  • 拥有两种线程模式

    • 独占模式
    • 共享模式
  • 在 LOCK 包中的相关锁(经常使用的有 ReentrantLock、 ReadWriteLock )都是基于 AQS 来构建
  • 通常咱们叫 AQS 为同步器。

CountdownLatch

CountDownLatch 类位于 java.util.concurrent 包下,利用它能够实现相似计数器的功能。好比有一个任务 A,它要等待其余 4 个任务执行完毕以后才能执行,此时就能够利用 CountDownLatch 来实现这种功能了。

维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些由于调用 await() 方法而在等待的线程就会被唤醒。

CountDownLatch 类只提供了一个构造器:

public CountDownLatch(int count) {  };  // 参数count为计数值

而后下面这 3 个方法是 CountDownLatch 类中最重要的方法:

`public` `void` `await() ``throws` `InterruptedException { };   ``//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行``public` `boolean` `await(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { };  ``//和await()相似,只不过等待必定的时间后count值还没变为0的话就会继续执行``public` `void` `countDown() { };  ``//将count值减1`

下面看一个例子你们就清楚 CountDownLatch 的用法了:

`public` `class` `Test {``     ``public` `static` `void` `main(String[] args) {   ``         ``final` `CountDownLatch latch = ``new` `CountDownLatch(``2``);``         ` `         ``new` `Thread(){``             ``public` `void` `run() {``                 ``try` `{``                     ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"正在执行"``);``                    ``Thread.sleep(``3000``);``                    ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"执行完毕"``);``                    ``latch.countDown();``                ``} ``catch` `(InterruptedException e) {``                    ``e.printStackTrace();``                ``}``             ``};``         ``}.start();``         ` `         ``new` `Thread(){``             ``public` `void` `run() {``                 ``try` `{``                     ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"正在执行"``);``                     ``Thread.sleep(``3000``);``                     ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"执行完毕"``);``                     ``latch.countDown();``                ``} ``catch` `(InterruptedException e) {``                    ``e.printStackTrace();``                ``}``             ``};``         ``}.start();``         ` `         ``try` `{``             ``System.out.println(``"等待2个子线程执行完毕..."``);``            ``latch.await();``            ``System.out.println(``"2个子线程已经执行完毕"``);``            ``System.out.println(``"继续执行主线程"``);``        ``} ``catch` `(InterruptedException e) {``            ``e.printStackTrace();``        ``}``     ``}``}`

执行结果:

线程Thread-0正在执行
线程Thread-1正在执行
等待2个子线程执行完毕...
线程Thread-0执行完毕
线程Thread-1执行完毕
2个子线程已经执行完毕
继续执行主线程

CyclicBarrier

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。

和 CountdownLatch 类似,都是经过维护计数器来实现的。可是它的计数器是递增的,每次执行 await() 方法以后,计数器会加 1,直到计数器的值和设置的值相等,等待的全部线程才会继续执行。和 CountdownLatch 的另外一个区别是,CyclicBarrier 的计数器能够循环使用,因此它才叫作循环屏障。

下图应该从下往上看才正确。

public class CyclicBarrierExample {
    public static void main(String[] args) throws InterruptedException {
        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 e) {
                    e.printStackTrace();
                } catch (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..after..after..after..after..

Semaphore

Semaphore 就是操做系统中的信号量,能够控制对互斥资源的访问线程数。Semaphore 能够控同时访问的线程个数,经过 acquire() 获取一个许可,若是没有就等待,而 release() 释放一个许可。


Semaphore 类位于 java.util.concurrent 包下,它提供了2个构造器:

`public` `Semaphore(``int` `permits) {          ``//参数permits表示许可数目,即同时能够容许多少线程进行访问``    ``sync = ``new` `NonfairSync(permits);``}``public` `Semaphore(``int` `permits, ``boolean` `fair) {    ``//这个多了一个参数fair表示是不是公平的,即等待时间越久的越先获取许可``    ``sync = (fair)? ``new` `FairSync(permits) : ``new` `NonfairSync(permits);``}`

下面说一下 Semaphore 类中比较重要的几个方法,首先是 acquire()、release() 方法:

`public` `void` `acquire() ``throws` `InterruptedException {  }     ``//获取一个许可``public` `void` `acquire(``int` `permits) ``throws` `InterruptedException { }    ``//获取permits个许可``public` `void` `release() { }          ``//释放一个许可``public` `void` `release(``int` `permits) { }    ``//释放permits个许可`

  acquire() 用来获取一个许可,若无许可可以得到,则会一直等待,直到得到许可。

  release() 用来释放许可。注意,在释放许可以前,必须先获得到许可。

这 4 个方法都会被阻塞,若是想当即获得执行结果,能够使用下面几个方法:

`public` `boolean` `tryAcquire() { };    ``//尝试获取一个许可,若获取成功,则当即返回true,若获取失败,则当即返回false``public` `boolean` `tryAcquire(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { };  ``//尝试获取一个许可,若在指定的时间内获取成功,则当即返回true,不然则当即返回false``public` `boolean` `tryAcquire(``int` `permits) { }; ``//尝试获取permits个许可,若获取成功,则当即返回true,若获取失败,则当即返回false``public` `boolean` `tryAcquire(``int` `permits, ``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { }; ``//尝试获取permits个许可,若在指定的时间内获取成功,则当即返回true,不然则当即返回false`

  另外还能够经过 availablePermits() 方法获得可用的许可数目。

  下面经过一个例子来看一下 Semaphore 的具体使用:

  倘若一个工厂有 5 台机器,可是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完了,其余工人才能继续使用。那么咱们就能够经过 Semaphore 来实现:

`public` `class` `Test {``    ``public` `static` `void` `main(String[] args) {``        ``int` `N = ``8``;            ``//工人数``        ``Semaphore semaphore = ``new` `Semaphore(``5``); ``//机器数目``        ``for``(``int` `i=``0``;i<N;i++)``            ``new` `Worker(i,semaphore).start();``    ``}``    ` `    ``static` `class` `Worker ``extends` `Thread{``        ``private` `int` `num;``        ``private` `Semaphore semaphore;``        ``public` `Worker(``int` `num,Semaphore semaphore){``            ``this``.num = num;``            ``this``.semaphore = semaphore;``        ``}``        ` `        ``@Override``        ``public` `void` `run() {``            ``try` `{``                ``semaphore.acquire();``                ``System.out.println(``"工人"``+``this``.num+``"占用一个机器在生产..."``);``                ``Thread.sleep(``2000``);``                ``System.out.println(``"工人"``+``this``.num+``"释放出机器"``);``                ``semaphore.release();           ``            ``} ``catch` `(InterruptedException e) {``                ``e.printStackTrace();``            ``}``        ``}``    ``}``}`

执行结果:

工人0占用一个机器在生产...
工人1占用一个机器在生产...
工人2占用一个机器在生产...
工人4占用一个机器在生产...
工人5占用一个机器在生产...
工人0释放出机器
工人2释放出机器
工人3占用一个机器在生产...
工人7占用一个机器在生产...
工人4释放出机器
工人5释放出机器
工人1释放出机器
工人6占用一个机器在生产...
工人3释放出机器
工人7释放出机器
工人6释放出机器

总结

下面对上面说的三个辅助类进行一个总结:

  • CountDownLatch 和 CyclicBarrier 都可以实现线程之间的等待,只不过它们侧重点不一样:
    • CountDownLatch 通常用于某个线程A等待若干个其余线程执行完任务以后,它才执行;
    • CyclicBarrier 通常用于一组线程互相等待至某个状态,而后这一组线程再同时执行;
    • 另外,CountDownLatch 是不可以重用的,而 CyclicBarrier 是能够重用的。
  • Semaphore 其实和锁有点相似,它通常用于控制对某组资源的访问权限。

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 的利用率。每一个线程都维护了一个双端队列,用来存储须要执行的任务。工做窃取算法容许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例以下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。可是若是队列中只有一个任务时仍是会发生竞争。


9. 线程不安全示例

若是多个线程对同一个共享数据进行访问而不采起同步操做的话,那么操做的结果是不一致的。

如下代码演示了 1000 个线程同时对 cnt 执行自增操做,操做结束以后它的值为 997 而不是 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 内存模型(JMM)

Java 内存模型试图屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。

主内存与工做内存

处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。

加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,须要一些协议来解决这个问题。

全部的变量都存储在主内存中,每一个线程还有本身的工做内存,工做内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。

线程只能直接操做工做内存中的变量,不一样线程之间的变量值传递须要经过主内存来完成。

Java内存模型和硬件关系图

Java内存模型抽象结构图

内存间交互操做

Java 内存模型定义了 8 个操做来完成主内存和工做内存的交互操做。

  • read:把一个变量的值从主内存传输到工做内存中
  • load:在 read 以后执行,把 read 获得的值放入工做内存的变量副本中
  • use:把工做内存中一个变量的值传递给执行引擎
  • assign:把一个从执行引擎接收到的值赋给工做内存的变量
  • store:把工做内存的一个变量的值传送到主内存中
  • write:在 store 以后执行,把 store 获得的值放入主内存的变量中
  • lock:做用于主内存的变量,把一个变量标识为一条线程独占状态
  • unlock:做用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定

若是要把一个变量从主内存中复制到工做内存,就须要按顺寻地执行 read 和 load 操做,若是把变量从工做内存中同步回主内存中,就要按顺序地执行 store 和 write 操做。Java内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行。也就是 read 和 load 之间,store 和 write 之间是能够插入其余指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。

Java内存模型还规定了在执行上述8种基本操做时,必须知足以下规则:

  • 不容许 read 和 load、store 和 write 操做之一单独出现
  • 不容许一个线程丢弃它的最近 assign 的操做,即变量在工做内存中改变了以后必须同步到主内存中
  • 不容许一个线程无缘由地(没有发生过任何assign操做)把数据从工做内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不容许在工做内存中直接使用一个未被初始化(load 或 assign)的变量。即就是对一个变量实施 use 和 store 操做以前,必须先执行过了 assign 和 load 操做。
  • 一个变量在同一时刻只容许一条线程对其进行lock操做,lock 和 unlock必须成对出现
  • 若是对一个变量执行 lock 操做,将会清空工做内存中此变量的值,在执行引擎使用这个变量前须要从新执行 load 或 assign 操做初始化变量的值
  • 若是一个变量事先没有被 lock 操做锁定,则不容许对它执行 unlock 操做;也不容许去 unlock 一个被其余线程锁定的变量。
  • 对一个变量执行 unlock 操做以前,必须先把此变量同步到主内存中(执行 store 和 write 操做)。

参考资料:

volatile关键字与Java内存模型(JMM) - yzwall - 博客园

内存模型三大特性

1. 原子性

  • 概念
    • 事物有原子性,这个概念大概都清楚,即一个操做或多个操做要么执行的过程当中不被任何因素打断,要么不执行。
  • 如何实现原子性?
    • 经过同步代码块 synchronized 或者 local 锁来确保原子性

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操做具备原子性,例如对一个 int 类型的变量执行 assign 赋值操做,这个操做就是原子性的。可是 Java 内存模型容许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操做划分为两次 32 位的操做来进行,即 load、store、read 和 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 这些单个操做具有原子性。


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

2. 可见性

可见性指当一个线程修改了共享变量的值,其它线程可以当即得知这个修改。Java 内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

主要有有三种实现可见性的方式:

  • volatile
  • synchronized,对一个变量执行 unlock 操做以前,必须把变量值同步回主内存。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,而且没有发生 this 逃逸(其它线程经过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,由于 volatile 并不能保证操做的原子性。

3. 有序性

有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。

在 Java 内存模型中,容许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

volatile 关键字经过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障以前。

也能够经过 synchronized 来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关因而让线程顺序执行同步代码。

指令重排序

在执行程序时为了提升性能,编译器和处理器经常会对指令作重排序。

指令重排序包括:编译器重排序处理器重排序

重排序分三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序均可能会致使多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是全部的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,经过内存屏障指令来禁止特定类型的处理器重排序(不是全部的处理器重排序都要禁止)。

JMM 属于语言级的内存模型,它确保在不一样的编译器和不一样的处理器平台之上,经过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

数据依赖性

若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖分下列三种类型:

名称 代码示例 说明
写后读 a = 1;b = a; 写一个变量以后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量以后,再写这个变量。
读后写 a = b;b = 1; 读一个变量以后,再写这个变量。

上面三种状况,只要重排序两个操做的执行顺序,程序的执行结果将会被改变。

前面提到过,编译器和处理器可能会对操做作重排序。编译器和处理器在重排序时,会遵照数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序。

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操做,不一样处理器之间和不一样线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial语义

as-if-serial 语义的意思指:无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和 处理器 都必须遵照 as-if-serial 语义。

为了遵照 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

上面三个操做的数据依赖关系以下图所示:

如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。所以在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器能够重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:

as-if-serial 语义把单线程程序保护了起来,遵照 as-if-serial 语义的编译器,runtime 和处理器共同为编写单线程程序的程序员建立了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial 语义使单线程程序员无需担忧重排序会干扰他们,也无需担忧内存可见性问题。

程序顺序规则

根据 happens- before 的程序顺序规则,上面计算圆的面积的示例代码存在三个 happens- before 关系:

  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

这里的第 3 个 happens- before 关系,是根据 happens- before 的传递性推导出来的。

这里 A happens- before B,但实际执行时 B 却能够排在 A 以前执行(看上面的重排序后的执行顺序)。若是A happens- before B,JMM 并不要求 A 必定要在 B 以前执行。JMM 仅仅要求前一个操做(执行的结果)对后一个操做可见,且前一个操做按顺序排在第二个操做以前。这里操做 A 的执行结果不须要对操做 B 可见;并且重排序操做 A 和操做 B 后的执行结果,与操做 A 和操做 B 按 happens- before 顺序执行的结果一致。在这种状况下, JMM 会认为这种重排序并不非法(not illegal),JMM 容许这种重排序。

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽量的开发并行度。编译器和处理器听从这一目标,从 happens- before 的定义咱们能够看出,JMM 一样听从这一目标。

重排序对多线程的影响

如今让咱们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   // 1
        flag = true;             // 2
    }

    Public void reader() {
        if (flag) {                // 3
            int i =  a * a;        // 4
            ……
        }
    }
}

flag 变量是个标记,用来标识变量 a 是否已被写入。这里假设有两个线程 A 和 B,A首先执行 writer() 方法,随后 B 线程接着执行 reader() 方法。线程 B 在执行操做 4 时,可否看到线程 A 在操做 1 对共享变量 a 的写入?

答案是:不必定能看到。

因为操做 1 和操做 2 没有数据依赖关系,编译器和处理器能够对这两个操做重排序;一样,操做 3 和操做 4 没有数据依赖关系,编译器和处理器也能够对这两个操做重排序。让咱们先来看看,当操做 1 和操做 2 重排序时,可能会产生什么效果?请看下面的程序执行时序图:

如上图所示,操做 1 和操做 2 作了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。因为条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

※注:本文统一用红色的虚箭线表示错误的读操做,用绿色的虚箭线表示正确的读操做。

下面再让咱们看看,当操做 3 和操做 4 重排序时会产生什么效果(借助这个重排序,能够顺便说明控制依赖性)。下面是操做 3 和操做 4 重排序后,程序的执行时序图:

在程序中,操做 3 和操做 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜想(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜想执行为例,执行线程 B 的处理器能够提早读取并计算 a*a,而后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操做3的条件判断为真时,就把该计算结果写入变量 i 中。

从图中咱们能够看出,猜想执行实质上对操做 3 和 4 作了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操做重排序,不会改变执行结果(这也是 as-if-serial 语义容许对存在控制依赖的操做作重排序的缘由);但在多线程程序中,对存在控制依赖的操做重排序,可能会改变程序的执行结果。

参考资料:

先行发生原则(happens-before)

Happens-before 是用来指定两个操做之间的执行顺序。提供跨线程的内存可见性。

在 Java 内存模型中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必然存在 happens-before 关系。

上面提到了能够用 volatile 和 synchronized 来保证有序性。除此以外,JVM 还规定了先行发生原则,让一个操做无需控制就能先于另外一个操做完成。

主要有如下这些原则:

1. 单一线程原则

Single Thread rule

在一个线程内,在程序前面的操做先行发生于后面的操做。

2. 管程锁定规则

Monitor Lock Rule

对一个锁的解锁(unlock ),老是 happens-before 于随后对这个锁的加锁(lock)

3. volatile 变量规则

Volatile Variable Rule

对一个 volatile 变量的写操做先行发生于后面对这个变量的读操做。

4. 线程启动规则

Thread Start Rule

Thread 对象的 start() 方法调用先行发生于此线程的每个动做。

5. 线程加入规则

Thread Join Rule

Thread 对象的结束先行发生于 join() 方法返回。

6. 线程中断规则

Thread Interruption Rule

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过 interrupted() 方法检测到是否有中断发生。

7. 对象终结规则

Finalizer Rule

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

8. 传递性

Transitivity

若是操做 A 先行发生于操做 B,操做 B 先行发生于操做 C,那么操做 A 先行发生于操做 C。

11. 线程安全

线程安全定义

一个类在能够被多个线程安全调用时就是线程安全的。

线程安全分类

线程安全不是一个非真即假的命题,能够将共享数据按照安全程度的强弱顺序分红如下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1. 不可变

不可变(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();
}

多线程环境下,应当尽可能使对象成为不可变,来知足线程安全。

2. 绝对线程安全

无论运行时环境如何,调用者都不须要任何额外的同步措施。

3. 相对线程安全

相对的线程安全须要保证对这个对象单独的操做是线程安全的,在调用的时候不须要作额外的保障措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性。

在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。

对于下面的代码,若是删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。

public class VectorUnsafeExample {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 100; i++) {
                vector.add(i);
            }
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            });
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.get(i);
                }
            });
            executorService.shutdown();
        }
    }
}
Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
    at java.util.Vector.remove(Vector.java:831)
    at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
    at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

若是要保证上面的代码能正确执行下去,就须要对删除元素和获取元素的代码进行同步。

executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
        }
    }
});
executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.get(i);
        }
    }
});

4. 线程兼容

线程兼容是指对象自己并非线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用,咱们日常说一个类不是线程安全的,绝大多数时候指的是这一种状况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

5. 线程对立

线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码。因为 Java 语言天生就具有多线程特性,线程对立这种排斥多线程的代码是不多出现的,并且一般都是有害的,应当尽可能避免。

线程安全的实现方法

1. 阻塞同步(互斥同步)

synchronized 和 ReentrantLock。

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,所以这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,老是认为只要不去作正确的同步措施,那就确定会出现问题。不管共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分没必要要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要唤醒等操做。

2. 非阻塞同步

随着硬件指令集的发展,咱们能够使用基于冲突检测的乐观并发策略:先进行操做,若是没有其它线程争用共享数据,那操做就成功了,不然采起补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不须要把线程挂起,所以这种同步操做称为非阻塞同步。

乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。

硬件支持的原子性操做最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令须要有 3 个操做数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操做时,只有当 V 的值等于 A,才将 V 的值更新为 B。

J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 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() 来进行 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 问题,改用传统的互斥同步可能会比原子类更高效。

3. 无同步方案

要保证线程安全,并非必定就要进行同步,二者没有因果关系。同步只是保证共享数据争用时的正确性的手段,若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性,所以会有一些代码天生就是线程安全的。

(一)可重入代码(Reentrant Code)

这种代码也叫作纯代码(Pure Code),能够在代码执行的任什么时候刻中断它,转而去执行另一段代码(包括递归调用它自己),而在控制权返回后,原来的程序不会出现任何错误。

可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

(二)栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,由于局部变量存储在栈中,属于线程私有的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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
(三)线程本地存储(Thread Local Storage)

若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特色的应用并很多见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽可能在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的 “一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的普遍应用使得不少 Web 服务端应用均可以使用线程本地存储来解决线程安全问题。

能够使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

这是一个很是好的例题,请参考整理:

关于ThreadLocal类如下说法正确的是?_迅雷笔试题_牛客网

示例用法

先经过下面这个实例来理解 ThreadLocal 的用法。先声明一个 ThreadLocal 对象,存储布尔类型的数值。而后分别在main线程、Thread一、Thread2中为 ThreadLocal 对象设置不一样的数值:

public class ThreadLocalDemo {
    public static void main(String[] args) {

        // 声明 ThreadLocal对象
        ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

        // 在主线程、子线程一、子线程2中去设置访问它的值
        mThreadLocal.set(true);

        System.out.println("Main " + mThreadLocal.get());

        new Thread("Thread#1"){
            @Override
            public void run() {
                mThreadLocal.set(false);
                System.out.println("Thread#1 " + mThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2"){
            @Override
            public void run() {
                System.out.println("Thread#2 " + mThreadLocal.get());
            }
        }.start();
    }
}

打印的结果输出以下所示:

MainThread true
Thread#1 false
Thread#2 null

能够看见,在不一样线程对同一个 ThreadLocal对象设置数值,在不一样的线程中取出来的值不同。接下来就分析一下源码,看看其内部结构。

结构概览


清晰的看到一个线程 Thread 中存在一个 ThreadLocalMap,ThreadLocalMap 中的 key 对应 ThreadLocal,在此处可见 Map 能够存储多个 key 即 (ThreadLocal)。另外 Value 就对应着在 ThreadLocal 中存储的 Value。

所以总结出:每一个 Thread 中都具有一个 ThreadLocalMap,而 ThreadLocalMap 能够存储以 ThreadLocal 为key的键值对。这里解释了为何每一个线程访问同一个 ThreadLocal,获得的确是不一样的数值。若是此处你以为有点突兀,接下来看源码分析!

源码分析

1. ThreadLocal#set

public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 根据当前线程的对象获取其内部Map
    ThreadLocalMap map = getMap(t);
    
    // 注释1
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

如上所示,大部分解释已经在代码中作出,注意注释1处,获得 map 对象以后,用的 this 做为 key,this 在这里表明的是当前线程的 ThreadLocal 对象。 另外就是第二句根据 getMap 获取一个 ThreadLocalMap,其中getMap 中传入了参数 t (当前线程对象),这样就可以获取每一个线程的 ThreadLocal 了。

继续跟进到 ThreadLocalMap 中查看 set 方法:

2. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的一个内部类,在分析其 set 方法以前,查看一下其类结构和成员变量。

static class ThreadLocalMap {
     // Entry类继承了WeakReference<ThreadLocal<?>>
     // 即每一个Entry对象都有一个ThreadLocal的弱引用(做为key),这是为了防止内存泄露。
     // 一旦线程结束,key变为一个不可达的对象,这个Entry就能够被GC了。
     static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         Object value;
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }
     // ThreadLocalMap 的初始容量,必须为2的倍数
     private static final int INITIAL_CAPACITY = 16;

     // resized时候须要的table
     private Entry[] table;

     // table中的entry个数
     private int size = 0;

     // 扩容数值
     private int threshold; // Default to 0
 }

一块儿看一下其经常使用的构造函数:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

构造函数的第一个参数就是本 ThreadLocal 实例 (this),第二个参数就是要保存的线程本地变量。构造函数首先建立一个长度为16的 Entry 数组,而后计算出 firstKey 对应的哈希值,而后存储到 table 中,并设置 size 和 threshold。

注意一个细节,计算 hash 的时候里面采用了 hashCode & (size - 1) 的算法,这至关于取模运算 hashCode % size 的一个更高效的实现(和HashMap中的思路相同)。正是由于这种算法,咱们要求 size必须是 2 的指数,由于这能够使得 hash 发生冲突的次数减少。

3. ThreadLocalMap#set

ThreadLocal 中 put 函数最终调用了 ThreadLocalMap 中的 set 函数,跟进去看一看:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         // 冲突了
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

在上述代码中若是 Entry 在存放过程当中冲突了,调用 nextIndex 来处理,以下所示。是否还记得 hashmap 中对待冲突的处理?这里好像是另外一种套路:只要 i 的数值小于 len,就加1取值,官方术语称为:线性探测法。

private static int nextIndex(int i, int len) {
     return ((i + 1 < len) ? i + 1 : 0);
 }

以上步骤ok了以后,再次关注一下源码中的 cleanSomeSlots,该函数主要的做用就是清理无用的 entry,避免出现内存泄露:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

4. ThreadLocal#get

看完了 set 函数,确定是要关注 get 的,源码以下所示:

public T get() {
    // 获取Thread对象t
    Thread t = Thread.currentThread();
    // 获取t中的map
    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;
        }
    }
    // 若是t中的map为空
    return setInitialValue();
}

若是 map 为 null,就返回 setInitialValue() 这个方法,跟进这个方法看一下:

private T setInitialValue() {
     T value = initialValue();
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
     return value;
 }

最后返回的是 value,而 value 来自 initialValue(),进入这个源码中查看:

protected T initialValue() {
    return null;
}

原来如此,若是不设置 ThreadLocal 的数值,默认就是 null,来自于此。

ThreadLocal 从理论上讲并非用来解决多线程并发问题的,由于根本不存在多线程竞争。在一些场景 (尤为是使用线程池) 下,因为 ThreadLocal.ThreadLocalMap 的底层数据结构致使 ThreadLocal 有内存泄漏的状况,尽量在每次使用 ThreadLocal 后手动调用 remove(),以免出现 ThreadLocal 经典的内存泄漏甚至是形成自身业务混乱的风险。

参考资料:

12. 锁优化

这里的锁优化主要是指虚拟机对 synchronized 的优化。

自旋锁

互斥同步的进入阻塞状态的开销都很大,应该尽可能避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,若是在这段时间内能得到锁,就能够避免进入阻塞状态。

自旋锁虽然能避免进入阻塞状态从而减小开销,可是它须要进行忙循环操做占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。

在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数再也不固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。

锁消除主要是经过逃逸分析来支持,若是堆上的共享数据不可能逃逸出去被其它线程访问到,那么就能够把它们当成私有数据对待,也就能够将它们的锁进行消除。

对于一些看起来没有加锁的代码,其实隐式的加了不少锁。例以下面的字符串拼接代码就隐式加了锁:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.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() 操做以后,这样只须要加锁一次就能够了。

轻量级锁

JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。

如下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 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 中还会不断优化和完善,使用这些更高等级的同步工具你的程序能够不费吹灰之力得到优化。
  • 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
  • 使用本地变量和不可变类来保证线程安全。
  • 使用线程池而不是直接建立 Thread 对象,这是由于建立线程代价很高,线程池能够有效地利用有限的线程来启动任务。
  • 使用 BlockingQueue 实现生产者消费者问题。

14. 线程池实现原理

蘑菇街面试,设计一个线程池

ThrealpoolExecutor_framework

并发队列

入队

非阻塞队列:当队列中满了时候,放入数据,数据丢失

阻塞队列:当队列满了的时候,进行等待,何时队列中有出队的数据,那么第11个再放进去

出队

非阻塞队列:若是如今队列中没有元素,取元素,获得的是null

阻塞队列:等待,何时放进去,再取出来

线程池使用的是阻塞队列

线程池概念

线程是稀缺资源,若是被无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有如下好处:

  1. 下降资源消耗;
  2. 提升响应速度;
  3. 提升线程的可管理性。

Java1.5 中引入的 Executor 框架把任务的提交和执行进行解耦,只须要定义好任务,而后提交给线程池,而不用关心该任务是如何执行、被哪一个线程执行,以及何时执行。

Executor类图

线程池工做原理

线程池中的核心线程数,当提交一个任务时,线程池建立一个新线程执行任务,直到当前线程数等于corePoolSize;若是当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;若是阻塞队列满了,那就建立新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。

初始化线程池

  • newFixedThreadPool()
    说明:初始化一个指定线程数的线程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 做为阻塞队列
    特色:即便当线程池没有可执行任务时,也不会释放线程。
  • newCachedThreadPool()
    说明:初始化一个能够缓存线程的线程池,默认缓存60s,线程池的线程数可达到 Integer.MAX_VALUE,即 2147483647,内部使用 SynchronousQueue 做为阻塞队列;
    特色:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,若是没有空闲线程,则建立新线程执行任务,会致使必定的系统开销;
    所以,使用时要注意控制并发的任务数,防止因建立大量的线程致使而下降性能。
  • newSingleThreadExecutor()
    说明:初始化只有一个线程的线程池,内部使用 LinkedBlockingQueue 做为阻塞队列。
    特色:若是该线程异常结束,会从新建立一个新的线程继续执行任务,惟一的线程能够保证所提交任务的顺序执行
  • newScheduledThreadPool()
    特色:初始化的线程池能够在指定的时间内周期性的执行所提交的任务,在实际的业务场景中能够使用该线程池按期的同步数据。

初始化方法

// 使用Executors静态方法进行初始化
ExecutorService service = Executors.newSingleThreadExecutor();
// 经常使用方法
service.execute(new Thread());
service.submit(new Thread());
service.shutDown();
service.shutDownNow();

经常使用方法

execute与submit的区别

  1. 接收的参数不同
  2. submit有返回值,而execute没有

用到返回值的例子,好比说我有不少个作 validation 的 task,我但愿全部的 task 执行完,而后每一个 task 告诉我它的执行结果,是成功仍是失败,若是是失败,缘由是什么。而后我就能够把全部失败的缘由综合起来发给调用者。

  1. submit方便Exception处理

若是你在你的 task 里会抛出 checked 或者 unchecked exception,而你又但愿外面的调用者可以感知这些 exception 并作出及时的处理,那么就须要用到 submit,经过捕获 Future.get 抛出的异常。

shutDown与shutDownNow的区别

当线程池调用该方法时,线程池的状态则马上变成 SHUTDOWN 状态。此时,则不能再往线程池中添加任何任务,不然将会抛出 RejectedExecutionException 异常。可是,此时线程池不会马上退出,直到添加到线程池中的任务都已经处理完成,才会退出。

内部实现

public ThreadPoolExecutor(
    int corePoolSize,     // 核心线程数
    int maximumPoolSize,  // 最大线程数
    long keepAliveTime,   // 线程存活时间(在 corePore<*<maxPoolSize 状况下有用)
    TimeUnit unit,        // 存活时间的时间单位
    BlockingQueue<Runnable> workQueue    // 阻塞队列(用来保存等待被执行的任务)
    ThreadFactory threadFactory,    // 线程工厂,主要用来建立线程;
    RejectedExecutionHandler handler // 当拒绝处理任务时的策略
){
    
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

关于 workQueue 参数,有四种队列可供选择:

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务;
  • LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务;
  • SynchronousQuene:一个不存储元素的阻塞队列,每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于 ArrayBlockingQuene;
  • PriorityBlockingQuene:具备优先级的无界阻塞队列;

关于 handler 参数,线程池的饱和策略,当阻塞队列满了,且没有空闲的工做线程,若是继续提交任务,必须采起一种策略处理该任务,线程池提供了 4 种策略:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:丢弃任务,可是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

固然也能够根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

线程池的状态

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

其中 AtomicInteger 变量 ctl 的功能很是强大:利用低 29 位表示线程池中线程数,经过高 3 位表示线程池的运行状态:

  • RUNNING:-1 << COUNT_BITS,即高 3 位为 111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • SHUTDOWN: 0 << COUNT_BITS,即高 3 位为 000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • STOP : 1 << COUNT_BITS,即高 3 位为 001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,并且会中断正在运行的任务;
  • TIDYING : 2 << COUNT_BITS,即高 3 位为 010,该状态表示线程池对线程进行整理优化;
  • TERMINATED: 3 << COUNT_BITS,即高 3 位为 011,该状态表示线程池中止工做;

线程池其余经常使用方法

若是执行了线程池的 prestartAllCoreThreads() 方法,线程池会提早建立并启动全部核心线程。
ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize()。

如何合理设置线程池的大小

通常须要根据任务的类型来配置线程池大小:
若是是 CPU 密集型任务,就须要尽可能压榨 CPU,参考值能够设为 NCPU+1
若是是 IO 密集型任务,参考值能够设置为 2*NCPU

第二部分:面试指南

在这里将总结面试中和并发编程相关的常见知识点,如在第一部分中出现的这里将不进行详细阐述。面试指南中,我将用最简洁的语言描述,更可能是以一种大纲的形式列出问答点,根据本身掌握的状况回答。

参考资料:

1. volatile 与 synchronized 的区别

(1)仅靠volatile不能保证线程的安全性。(原子性)

  • ① volatile 轻量级,只能修饰变量。synchronized重量级,还可修饰方法
  • ② volatile 只能保证数据的可见性,不能用来同步,由于多个线程并发访问 volatile 修饰的变量不会阻塞。

synchronized 不只保证可见性,并且还保证原子性,由于,只有得到了锁的线程才能进入临界区,从而保证临界区中的全部语句都所有执行。多个线程争抢 synchronized 锁对象时,会出现阻塞。

(2)线程安全性

线程安全性包括两个方面,①可见性。②原子性。

从上面自增的例子中能够看出:仅仅使用 volatile 并不能保证线程安全性。而 synchronized 则可实现线程的安全性。

2. 什么是线程池?若是让你设计一个动态大小的线程池,如何设计,应该有哪些方法?线程池建立的方式?

  • 什么是线程池

    • 线程池顾名思义就是事先建立若干个可执行的线程放入一个池(容器)中,须要的时候从池中获取线程不用自行建立,使用完毕不须要销毁线程而是放回池中,从而减小建立和销毁线程对象的开销。
  • 设计一个动态大小的线程池,如何设计,应该有哪些方法

    • 一个线程池包括如下四个基本组成部分:
      • 线程管理器 (ThreadPool):用于建立并管理线程池,包括建立线程,销毁线程池,添加新任务;
      • 工做线程 (PoolWorker):线程池中线程,在没有任务时处于等待状态,能够循环的执行任务;
      • 任务接口 (Task):每一个任务必须实现的接口,以供工做线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工做,任务的执行状态等;
      • 任务队列 (TaskQueue):用于存放没有处理的任务。提供一种缓冲机制;
    • 所包含的方法
      • private ThreadPool() 建立线程池
      • public static ThreadPool getThreadPool() 得到一个默认线程个数的线程池
      • public void execute(Runnable task) 执行任务,其实只是把任务加入任务队列,何时执行有线程池管理器决定
      • public void execute(Runnable[] task) 批量执行任务,其实只是把任务加入任务队列,何时执行有线程池管理器决定
      • public void destroy() 销毁线程池,该方法保证在全部任务都完成的状况下才销毁全部线程,不然等待任务完成才销毁
      • public int getWorkThreadNumber() 返回工做线程的个数
      • public int getFinishedTasknumber() 返回已完成任务的个数,这里的已完成是只出了任务队列的任务个数,可能该任务并无实际执行完成
      • public void addThread() 在保证线程池中全部线程正在执行,而且要执行线程的个数大于某一值时。增长线程池中线程的个数
      • public void reduceThread() 在保证线程池中有很大一部分线程处于空闲状态,而且空闲状态的线程在小于某一值时,减小线程池中线程的个数
  • 线程池四种建立方式

    Java 经过 Executors 提供四种线程池,分别为:

    • new CachedThreadPool 建立一个可缓存线程池,若是线程池长度超过处理须要,可灵活回收空闲线程,若无可回收,则新建线程。
    • new FixedThreadPool 建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    • new ScheduledThreadPool 建立一个定长线程池,支持定时及周期性任务执行。
    • new SingleThreadExecutor 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行。

3. 什么是并发和并行


并发

  • 并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,因为时间间隔较短,令人感受两个任务都在运行。
  • 若是用一台电脑我先给甲发个消息,而后马上再给乙发消息,而后再跟甲聊,再跟乙聊。这就叫并发。
  • 多个线程操做相同的资源,保证线程安全,合理使用资源

并行

  • 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(须要多核CPU)

  • 好比我跟两个网友聊天,左手操做一个电脑跟甲聊,同时右手用另外一台电脑跟乙聊天,这就叫并行。

  • 服务能同时处理不少请求,提升程序性能

参考资料:

4. 什么是线程安全

当多个线程访问同一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替运行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以获取正确的结果,那这个对象是线程安全的。——来自《深刻理解Java虚拟机》

  • 定义

    • 某个类的行为与其规范一致。
    • 无论多个线程是怎样的执行顺序和优先级,或是 wait , sleep , join 等控制方式,若是一个类在多线程访问下运转一切正常,而且访问类不须要进行额外的同步处理或者协调,那么咱们就认为它是线程安全的。
  • 如何保证线程安全?(更加详细的请转向第一部分 11. 线程安全

    • 对变量使用 volitate
    • 对程序段进行加锁 (synchronized , lock)
  • 注意

    • 非线程安全的集合在多线程环境下能够使用,但并不能做为多个线程共享的属性,能够做为某个线程独享的属性。
    • 例如 Vector 是线程安全的,ArrayList 不是线程安全的。若是每个线程中 new 一个 ArrayList,而这个ArrayList 只是在这一个线程中使用,确定没问题。

非线程安全!=不安全?

有人在使用过程当中有一个不正确的观点:个人程序是多线程的,不能使用 ArrayList 要使用 Vector,这样才安全。

非线程安全并非多线程环境下就不能使用。注意我上面有说到:多线程操做同一个对象。注意是同一个对象。好比最上面那个模拟,就是在主线程中 new 的一个 ArrayList 而后多个线程操做同一个 ArrayList 对象。

若是是每一个线程中 new 一个 ArrayList,而这个 ArrayList 只在这一个线程中使用,那么确定是没问题的。

线程安全十万个为何?

问:平时项目中使用锁和 synchronized 比较多,而不多使用 volatile,难道就没有保证可见性?
答:锁和 synchronized 便可以保证原子性,也能够保证可见性。都是经过保证同一时间只有一个线程执行目标代码段来实现的。

问:锁和 synchronized 为什么能保证可见性?
答:根据 JDK 7的Java doc 中对 concurrent 包的说明,一个线程的写结果保证对另外线程的读操做可见,只要该写操做能够由 happen-before 原则推断出在读操做以前发生。

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

问:既然锁和 synchronized 便可保证原子性也可保证可见性,为什么还须要 volatile?
答:synchronized和锁须要经过操做系统来仲裁谁得到锁,开销比较高,而 volatile 开销小不少。所以在只须要保证可见性的条件下,使用 volatile 的性能要比使用锁和 synchronized 高得多。

问:既然锁和 synchronized 能够保证原子性,为何还须要 AtomicInteger 这种的类来保证原子操做?
答:锁和 synchronized 须要经过操做系统来仲裁谁得到锁,开销比较高,而 AtomicInteger 是经过CPU级的CAS操做来保证原子性,开销比较小。因此使用 AtomicInteger 的目的仍是为了提升性能。

问:还有没有别的办法保证线程安全
答:有。尽量避免引发非线程安全的条件——共享变量。若是能从设计上避免共享变量的使用,便可避免非线程安全的发生,也就无须经过锁或者 synchronized 以及 volatile 解决原子性、可见性和顺序性的问题。

问:synchronized 便可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized 修饰非静态同步方法时,锁住的是当前实例;synchronized 修饰静态同步方法时,锁住的是该类的 Class 对象;synchronized 修饰静态代码块时,锁住的是 synchronized 关键字后面括号内的对象。

参考资料:

5. volatile 关键字的如何保证内存可见性

  • volatile 关键字的做用

    • 保证内存的可见性
    • 防止指令重排
    • 注意:volatile 并不保证原子性
  • 内存可见性

    • volatile 保证可见性的原理是在每次访问变量时都会进行一次刷新,所以每次访问都是主内存中最新的版本。因此 volatile 关键字的做用之一就是保证变量修改的实时可见性。
  • 当且仅当知足如下全部条件时,才应该使用 volatile 变量

    • 对变量的写入操做不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量没有包含在具备其余变量的不变式中。
  • volatile 使用建议

    • 在两个或者更多的线程须要访问的成员变量上使用 volatile。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用volatile。
    • 因为使用 volatile 屏蔽掉了 JVM 中必要的代码优化,因此在效率上比较低,所以必定在必要时才使用此关键字。
  • volatile 和 synchronized区别

    • volatile 不会进行加锁操做:

      volatile 变量是一种稍弱的同步机制在访问 volatile 变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制。

    • volatile 变量做用相似于同步变量读写操做:

      从内存可见性的角度看,写入 volatile 变量至关于退出同步代码块,而读取 volatile 变量至关于进入同步代码块。

    • volatile 不如 synchronized安全:

      在代码中若是过分依赖 volatile 变量来控制状态的可见性,一般会比使用锁的代码更脆弱,也更难以理解。仅当 volatile 变量能简化代码的实现以及对同步策略的验证时,才应该使用它。通常来讲,用同步机制会更安全些。

    • volatile 没法同时保证内存可见性和原则性:

      加锁机制(即同步机制)既能够确保可见性又能够确保原子性,而 volatile 变量只能确保可见性,缘由是声明为volatile的简单变量若是当前值与该变量之前的值相关,那么 volatile 关键字不起做用,也就是说以下的表达式都不是原子操做:“count++”、“count = count+1”。

5. 什么是线程?线程和进程有什么区别?为何要使用多线程

(1)线程和进程

  • 进程是操做系统分配资源的最小单位
  • 线程是CPU调度的最小单位

(2)使用线程的缘由

  • 使用多线程能够减小程序的响应时间;
  • 与进程相比,线程的建立和切换开销更小;
  • 多核电脑上,能够同时执行多个线程,提升资源利用率;
  • 简化程序的结构,使程序便于理解和维护;

6. 多线程共用一个数据变量须要注意什么?

  • 当咱们在线程对象(Runnable)中定义了全局变量,run方法会修改该变量时,若是有多个线程同时使用该线程对象,那么就会形成全局变量的值被同时修改,形成错误.
  • ThreadLocal 是JDK引入的一种机制,它用于解决线程间共享变量,使用 ThreadLocal 声明的变量,即便在线程中属于全局变量,针对每一个线程来说,这个变量也是独立的。
  • volatile 变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不一样的线程都能及时的看到该变量的最新值。

7. 内存泄漏与内存溢出

Java内存回收机制

  不论哪一种语言的内存分配方式,都须要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用 new、反射、clone、反序列化等方法建立的, 这些对象的建立都是在堆(Heap)中分配的,全部对象的回收都是由Java虚拟机经过垃圾回收机制完成的。GC 为了可以正确释放对象,会监控每一个对象的运行情况,对他们的申请、引用、被引用、赋值等情况进行监控,Java 会使用有向图的方法进行管理内存,实时监控对象是否能够达到,若是不可到达,则就将其回收,这样也能够消除引用循环的问题。

  在 Java 语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值 null,如下再没有调用过,另外一个是给对象赋予了新值,这样从新分配了内存空间。

Java内存泄露引发缘由

  首先,什么是内存泄露?常常听人谈起内存泄露,但要问什么是内存泄露,没几个说得清楚。

  内存泄露:是指无用对象(再也不使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而形成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示 Out of memory

  内存溢出:指程序运行过程当中没法申请到足够的内存而致使的一种错误。内存泄露是内存溢出的一种诱因,不是惟一因素
  那么,Java 内存泄露根本缘由是什么呢?长生命周期的对象持有短生命周期对象的引用就极可能发生内存泄露,尽管短生命周期对象已经再也不须要,可是由于长生命周期对象持有它的引用而致使不能被回收,这就是 Java 中内存泄露的发生场景。具体主要有以下几大类

静态集合类

  静态集合类,使用Set、Vector、HashMap等集合类的时候须要特别注意。当这些类被定义成静态的时候,因为他们的生命周期跟应用程序同样长,这时候就有可能发生内存泄漏。

// 例子 
class StaticTest 
{ 
    private static Vector v = new Vector(10); 
    public void init() 
    { 
        for (int i = 1; i < 100; i++) 
        { 
            Object object = new Object(); 
            v.add(object); 
            object = null; 
        } 
    } 
}

  在上面的代码中,循环申请object对象,并添加到Vector中,而后设置object=null(就是清除栈中引用变量object),可是这些对象被vector引用着,必然不能被GC回收,形成内存泄露。所以要释放这些对象,还须要将它们从vector中删除,最简单的方法就是将vector=null,清空集合类中的引用。

监听器

  在 Java 编程中,咱们都须要和监听器打交道,一般一个应用中会用到不少监听器,咱们会调用一个控件,诸如 addXXXListener() 等方法来增长监听器,但每每在释放的时候却没有去删除这些监听器,从而增长了内存泄漏的机会。

各类链接

  好比数据库链接(dataSourse.getConnection()),网络链接 (socket) 和 IO 链接,除非其显式的调用了其close() 方 法将其链接关闭,不然是不会自动被 GC 回收的。对于 Resultset 和 Statement 对象能够不进行显式回收,但 Connection 必定要显式回收,由于 Connection 在任什么时候候都没法自动回收,而 Connection一旦回收,Resultset 和 Statement 对象就会当即为 NULL。可是若是使用链接池,状况就不同了,除了要显式地关闭链接,还必须显式地关闭 Resultset Statement 对象(关闭其中一个,另一个也会关闭),不然就会形成大量的 Statement 对象没法释放,从而引发内存泄漏。这种状况下通常都会在 try 里面去的链接,在 finally 里面释放链接。

内部类和外部模块等的引用

  内部类的引用是比较容易遗忘的一种,并且一旦没释放可能致使一系列的后继类对象没有释放。在调用外部模块的时候,也应该注意防止内存泄漏,若是模块A调用了外部模块B的一个方法,如: public void register(Object o) 这个方法有可能就使得A模块持有传入对象的引用,这时候须要查看B模块是否提供了出去引用的方法,这种状况容易忽略,并且发生内存泄漏的话,还比较难察觉。

单例模式

  由于单利对象初始化后将在 JVM 的整个生命周期内存在,若是它持有一个外部对象的(生命周期比较短)引用,那么这个外部对象就不能被回收,从而致使内存泄漏。若是这个外部对象还持有其余对象的引用,那么内存泄漏更严重。

8. 如何减小线程上下文切换

使用多线程时,不是多线程能提高程序的执行速度,使用多线程是为了更好地利用 CPU 资源

程序在执行时,多线程是 CPU 经过给每一个线程分配 CPU 时间片来实现的,时间片是CPU分配给每一个线程执行的时间,因时间片很是短,因此CPU 经过不停地切换线程执行

线程不是越多就越好的,由于线程上下文切换是有性能损耗的,在使用多线程的同时须要考虑如何减小上下文切换

通常来讲有如下几条经验

  • 无锁并发编程。多线程竞争时,会引发上下文切换,因此多线程处理数据时,能够用一些办法来避免使用锁,如将数据的 ID 按照Hash取模分段,不一样的线程处理不一样段的数据
  • CAS算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不须要加锁
  • 控制线程数量。避免建立不须要的线程,好比任务不多,可是建立了不少线程来处理,这样会形成大量线程都处于等待状态
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
  • 协程能够当作是用户态自管理的“线程”不会参与CPU时间调度,没有均衡分配到时间。非抢占式

还能够考虑咱们的应用是IO密集型的仍是CPU密集型的。

  • 若是是IO密集型的话,线程能够多一些。
  • 若是是CPU密集型的话,线程不宜太多。

9. 线程间通讯和进程间通讯

线程间通讯

  • synchronized 同步

    • 这种方式,本质上就是 “共享内存” 式的通讯。多个线程须要访问同一个共享变量,谁拿到了锁(得到了访问权限),谁就能够执行。
  • while 轮询的方式

    • 在这种方式下,线程A不断地改变条件,线程 ThreadB 不停地经过 while 语句检测这个条件(list.size()==5) 是否成立 ,从而实现了线程间的通讯。可是这种方式会浪费 CPU 资源。之因此说它浪费资源,是由于 JVM 调度器将 CPU 交给线程B执行时,它没作啥“有用”的工做,只是在不断地测试某个条件是否成立。就相似于现实生活中,某我的一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。
  • wait/notify 机制

    • 当条件未知足时,线程A调用 wait() 放弃CPU,并进入阻塞状态。(不像 while 轮询那样占用 CPU)

      当条件知足时,线程B调用 notify() 通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。

  • 管道通讯

    • java.io.PipedInputStream 和 java.io.PipedOutputStream 进行通讯

进程间通讯

  • 管道(Pipe) :管道可用于具备亲缘关系进程间的通讯,容许一个进程和另外一个与它有共同祖先的进程之间进行通讯。
  • 命名管道(named pipe) :命名管道克服了管道没有名字的限制,所以,除具备管道所具备的功能外,它还容许无亲缘关 系 进程间的通讯。命名管道在文件系统中有对应的文件名。命名管道经过命令mkfifo或系统调用mkfifo来建立。
  • 信号(Signal) :信号是比较复杂的通讯方式,用于通知接受进程有某种事件发生,除了用于进程间通讯外,进程还能够发送 信号给进程自己;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又可以统一对外接口,用sigaction函数从新实现了signal函数)。
  • 消息(Message)队列 :消息队列是消息的连接表,包括Posix消息队列system V消息队列。有足够权限的进程能够向队列中添加消息,被赋予读权限的进程则能够读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
  • 共享内存 :使得多个进程能够访问同一块内存空间,是最快的可用IPC形式。是针对其余通讯机制运行效率较低而设计的。每每与其它通讯机制,如信号量结合使用,来达到进程间的同步及互斥。
  • 内存映射(mapped memory) :内存映射容许任何多个进程间通讯,每个使用该机制的进程经过把一个共享的文件映射到本身的进程地址空间来实现它。
  • 信号量(semaphore) :主要做为进程间以及同一进程不一样线程之间的同步手段。
  • 套接口(Socket) :更为通常的进程间通讯机制,可用于不一样机器之间的进程间通讯。起初是由Unix系统的BSD分支开发出来的,但如今通常能够移植到其它类Unix系统上:linux和System V的变种都支持套接字。

参考资料:

10. 什么是同步和异步,阻塞和非阻塞?

同步和异步关注的是消息通讯机制 (synchronous communication/ asynchronous communication)

同步

  • 在发出一个同步调用时,在没有获得结果以前,该调用就不返回。
  • 例如:按下电饭锅的煮饭按钮,而后等待饭煮好,把饭盛出来,而后再去炒菜。

异步

  • 在发出一个异步调用后,调用者不会马上获得结果,该调用就返回了。
  • 例如:按下电钮锅的煮饭按钮,直接去炒菜或者作别的事情,当电饭锅“滴滴滴”响的时候,再回去把饭盛出来。显然,异步式编程要比同步式编程高效得多。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞

  • 调用结果返回以前,当前线程会被挂起。调用线程只有在获得结果以后才会返回。
  • 例子:你打电话问书店老板有没有《分布式系统》这本书,你若是是阻塞式调用,你会一直把本身“挂起”,直到获得这本书有没有的结果

非阻塞

  • 在不能马上获得结果以前,该调用不会阻塞当前线程。
  • 例子:你打电话问书店老板有没有《分布式系统》这本书,你无论老板有没有告诉你,你本身先一边去玩了, 固然你也要偶尔过几分钟check一下老板有没有返回结果。

参考资料:

11. Java中的锁

本小结参考:Java 中的锁 - Java 并发性和多线程 - 极客学院Wiki

  锁像 synchronized 同步块同样,是一种线程同步机制,但比 Java 中的 synchronized 同步块更复杂。由于锁(以及其它更高级的线程同步机制)是由 synchronized 同步块的方式实现的,因此咱们还不能彻底摆脱 synchronized 关键字(译者注:这说的是 Java 5 以前的状况)。

  自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,所以你不用去实现本身的锁了。可是你仍然须要去了解怎样使用这些锁,且了解这些实现背后的理论也是颇有用处的。能够参考我对 java.util.concurrent.locks.Lock 的介绍,以了解更多关于锁的信息。

一个简单的锁

让咱们从 java 中的一个同步块开始:

public class Counter{
    private int count = 0;

    public int inc(){
        synchronized(this){
            return ++count;
        }
    }
}

能够看到在 inc()方法中有一个 synchronized(this)代码块。该代码块能够保证在同一时间只有一个线程能够执行 return ++count。虽然在 synchronized 的同步块中的代码能够更加复杂,可是++count 这种简单的操做已经足以表达出线程同步的意思。

如下的 Counter 类用 Lock 代替 synchronized 达到了一样的目的:

public class Counter{
    private Lock lock = new Lock();
    private int count = 0;

    public int inc(){
        lock.lock();
        int newCount = ++count;
        lock.unlock();
        return newCount;
    }
}

lock()方法会对 Lock 实例对象进行加锁,所以全部对该对象调用 lock()方法的线程都会被阻塞,直到该 Lock 对象的 unlock()方法被调用。

这里有一个 Lock 类的简单实现:

public class Counter{
public class Lock{
    private boolean isLocked = false;

    public synchronized void lock()
        throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }

    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

注意其中的 while(isLocked) 循环,它又被叫作 “自旋锁”。自旋锁以及 wait() 和 notify() 方法在线程通讯这篇文章中有更加详细的介绍。当 isLocked 为 true 时,调用 lock() 的线程在 wait() 调用上阻塞等待。为防止该线程没有收到 notify() 调用也从 wait() 中返回(也称做虚假唤醒),这个线程会从新去检查 isLocked 条件以决定当前是否能够安全地继续执行仍是须要从新保持等待,而不是认为线程被唤醒了就能够安全地继续执行了。若是 isLocked 为 false,当前线程会退出 while(isLocked) 循环,并将 isLocked 设回 true,让其它正在调用 lock() 方法的线程可以在 Lock 实例上加锁。

当线程完成了临界区(位于 lock()和 unlock()之间)中的代码,就会调用 unlock()。执行 unlock()会从新将 isLocked 设置为 false,而且通知(唤醒)其中一个(如有的话)在 lock()方法中调用了 wait()函数而处于等待状态的线程。

锁的可重入性

Java 中的 synchronized 同步块是可重入的。这意味着若是一个 Java 线程进入了代码中的 synchronized 同步块,并所以得到了该同步块使用的同步对象对应的管程上的锁,那么这个线程能够进入由同一个管程对象所同步的另外一个 java 代码块。下面是一个例子:

public class Reentrant{
    public synchronized outer(){
        inner();
    }

    public synchronized inner(){
        //do something
    }
}

注意 outer()和 inner()都被声明为 synchronized,这在 Java 中和 synchronized(this) 块等效。若是一个线程调用了 outer(),在 outer()里调用 inner()就没有什么问题,由于这两个方法(代码块)都由同一个管程对象(”this”) 所同步。若是一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的全部代码块。这就是可重入。线程能够进入任何一个它已经拥有的锁所同步着的代码块。

前面给出的锁实现不是可重入的。若是咱们像下面这样重写 Reentrant 类,当线程调用 outer() 时,会在 inner()方法的 lock.lock() 处阻塞住。

public class Reentrant2{
    Lock lock = new Lock();

    public outer(){
        lock.lock();
        inner();
        lock.unlock();
    }

    public synchronized inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

调用 outer() 的线程首先会锁住 Lock 实例,而后继续调用 inner()。inner()方法中该线程将再一次尝试锁住 Lock 实例,结果该动做会失败(也就是说该线程会被阻塞),由于这个 Lock 实例已经在 outer()方法中被锁住了。

两次 lock()之间没有调用 unlock(),第二次调用 lock 就会阻塞,看过 lock() 实现后,会发现缘由很明显:

public class Lock{
    boolean isLocked = false;

    public synchronized void lock()
        throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }

    ...
}

一个线程是否被容许退出 lock()方法是由 while 循环(自旋锁)中的条件决定的。当前的判断条件是只有当 isLocked 为 false 时 lock 操做才被容许,而没有考虑是哪一个线程锁住了它。

为了让这个 Lock 类具备可重入性,咱们须要对它作一点小的改动:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;

    public synchronized void lock()
        throws InterruptedException{
        Thread callingThread =
            Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
  }

    public synchronized void unlock(){
        if(Thread.curentThread() ==
            this.lockedBy){
            lockedCount--;

            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }

    ...
}

注意到如今的 while 循环(自旋锁)也考虑到了已锁住该 Lock 实例的线程。若是当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该 Lock 实例加了锁,那么 while 循环就不会被执行,调用 lock()的线程就能够退出该方法(译者注:“被容许退出该方法”在当前语义下就是指不会调用 wait()而致使阻塞)。

除此以外,咱们须要记录同一个线程重复对一个锁对象加锁的次数。不然,一次 unblock()调用就会解除整个锁,即便当前锁已经被加锁过屡次。在 unlock()调用没有达到对应 lock()调用的次数以前,咱们不但愿锁被解除。

如今这个 Lock 类就是可重入的了。

锁的公平性

Java 的 synchronized 块并不保证尝试进入它们的线程的顺序。所以,若是多个线程不断竞争访问相同的 synchronized 同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权老是分配给了其它线程。这种状况被称做线程饥饿。为了不这种问题,锁须要实现公平性。本文所展示的锁在内部是用 synchronized 同步块实现的,所以它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。

在 finally 语句中调用 unlock()

若是用 Lock 来保护临界区,而且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得很是重要了。这样能够保证这个锁对象能够被解锁以便其它线程能继续对其加锁。如下是一个示例:

lock.lock();
try{
    //do critical section code,
    //which may throw exception
} finally {
    lock.unlock();
}

这个简单的结构能够保证当临界区抛出异常时 Lock 对象能够被解锁。若是不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会致使其它全部在该 Lock 对象上调用 lock()的线程一直阻塞。

12. 并发包(J.U.C)下面,都用过什么

  • concurrent下面的包
    • Executor 用来建立线程池,在实现Callable接口时,添加线程。
    • FeatureTask 此 FutureTask 的 get 方法所返回的结果类型。
    • TimeUnit
    • Semaphore
    • LinkedBlockingQueue
  • 所用过的类
    • Executor

13. 从volatile说到,i++原子操做,线程安全问题

从 volatile 说到,i++原子操做,线程安全问题 - CSDN博客
https://blog.csdn.net/zbw18297786698/article/details/53420780

参考资料

相关文章
相关标签/搜索