JAVA核心技术笔记总结--第14章 线程总结

转载自https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md

0、进程和线程的区别:

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,上下文切换大约需要上千条指令,一个进程包含1--n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,大约需要100条指令。(线程是cpu调度的最小单位)

多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。 线程安全指的是:无论各线程的相对执行次序如何,所有线程执行完的最终结果是确定的。

一、线程状态转换

新建(New)

线程创建后尚未启动。

可运行(Runnable)

线程可能正在运行,也可能等待获取 CPU。包含了操作系统线程状态中的 Running 和 Ready。

阻塞(Blocking)

当线程试图获取一个对象锁(Synchronized 锁,而不是 JUC 中的 Lock)未成功时,该线程进入阻塞状态。当其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞态。

无限期等待(Waiting)

当线程等待其他线程通知调度器满足条件时,它自己进入等待状态。如

进入方法 退出方法
JUC 中的 Lock() 其他线程执行了 unlock()
FutureTask.get() 线程执行结束
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
Condition的 await() 其他线程在本条件对象上执行了 signal()/signalAll()

限期等待(Timed Waiting)

有几个方法有超时参数。调用它们导致线程进入限期等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有:

进入方法 退出方法
Thread.sleep() 方法 超时
Lock.tryLock() 方法 超时 / 获得锁
FutureTask.get() 超时 / 线程执行结束
设置了 Timeout 参数的 Thread.join() 方法 超时 / 被调用的线程执行完毕
设置了 Timeout 参数的 Object.wait() 方法 超时 / Object.notify() / Object.notifyAll()
Condition.await() 超时 / Condition.signal() / Condition.signalAll()

线程终止

线程会以如下方式结束,结束后就处于死亡状态。

  • run() 或 call() 方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的 Exception 或 Error。

当主线程创建并启动子线程后,子线程就拥有和主线程同样的地位,不会受主线程运行状态的影响。

可以调用线程对象的 isAlive() 方法测试线程是否已经死亡,当线程处于就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于新建、死亡两种状态时,该方法将返回 false。

二、创建线程

创建线程的方法有四种:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 提交任务到线程池

实现 Runnable 和 Callable 接口的对象只能当做一个可以在线程中运行的任务,不是真正意义上的线程,最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

继承 Thread 类

步骤如下:

  1. 定义 Thread 类的子类,在子类中覆盖 run() 方法。

  2. 创建 Thread 子类对象。

  3. 调用子类对象的 start() 方法启动线程,

启动线程后,虚拟机会将该线程放入就绪队列中等待调度,当一个线程被调度时会执行该线程的 run() 方法。

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

实现 Runnable 接口

步骤如下:

  1. 需要创建一个 Runnable 接口的实现类对象(通常通过 Lambda 表达式创建)。
  2. 将 Runnable 接口对象作为参数,创建 Thread 对象。
  3. 调用 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<E> 接口

步骤如下:

  1. 需要创建一个 Callable 接口的实现类对象(通常通过 Lambda 表达式创建)。
  2. 将 Callable 接口对象作为参数,创建 FutureTask<E> 对象。
  3. 将 FutureTask 对象作为参数,创建 Thread 对象。
  4. 调用 Thread 对象的 start() 方法来启动线程。
  5. 可以通过 FutureTask 的 get() 方法获取线程运行结果。

与 Runnable 相比,Callable 可以有返回值,类型参数表示返回值类型。返回值通过 FutureTask 进行封装。此外,call() 方法可以声明抛出异常。

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

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。
  • 接口很容易的实现资源共享

三、Future<E> 接口

在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。

FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。

在 Future 接口里定义了如下几个方法来控制它关联的 Callable 任务以及获取任务执行结果:

  • boolean cancel(boolean mayInterruptIfRunning):将 Future 里关联的 Callable 任务的中断标志置位。如果任务已经开始,且 mayInterruptIfRunning 为 true,它就会被中断。如果取消成功,则返回 true。
  • V get() :获取 Callable 任务里的 call() 方法的返回值,若 call 方法未执行结束,调用该方法将导致程序阻塞,等到子线程结束后才会得到返回值。
  • V get(long timeout, TimeUnit unit) :获取 Callable 任务里的 call() 方法的返回值。该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常。
  • boolean isCancelled() :如果在 Callable 任务正常完成前被取消,则返回 true ;
  • boolean isDone() :如果任务已结束,无论是正常结束、中途取消或发生异常,都返回 true 。

四、基础线程机制

Daemon

守护线程(后台线程 Daemon Thread)是程序运行时在后台提供服务的线程。

调用 Thread 对象的 setDaemon(true) 方法可以将指定线程设为后台线程。但必须在线程启动之前设定。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
    thread.start();
}

isDaemon() 方法,用于判断指定线程是否为后台线程。

当前台线程未全部结束时,前台线程和后台线程交替运行,但是当所有前台线程都运行结束时,所有后台线程也结束。

main() 线程默认是前台线程,但并不是所有的线程都默认是前台线程——前台线程创建的子线程默认都是前台线程,后台线程创建的子线程默认是后台线程。后台线程应该永远不去访问固有的资源,如文件、数据库,因为它会在任何时候结束。

sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

当线程调用 sleep() 后,如果被其他线程中断,会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

yield()

是 Thread 类提供的一个静态方法,它可以让当前正在执行的线程进入就绪态,重新竞争CPU,不会阻塞该线程。重新竞争 CPU 时,优先级高的线程只是获得 CPU 的机会大,但并不一定会获得 CPU。

public void run() {
    Thread.yield();
}

线程优先级

线程都具有一定的优先级,优先级高的线程获得较多的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main 线程具有普通优先级(NORM_PRIORITY)。

Thread 类提供了 setPriority(int newPriority)、getPriority() 方法来设置和返回指定线程的优先级,其中 setPriority() 方法的参数为1~10之间的整数,也可以使用 Thread 类的如下三个静态常量。

  • MAX_PRIORITY:其值是10。
  • MIN_PRIORITY:其值是1。
  • NORM_PRIORITY:其值是5。

虽然 java 提供了10个优先级级别,但是不同操作系统的优先级不能很好地和 java 的10个优先级对应,因此应该尽量避免直接用数字为线程指定优先级,而应该使用 java 提供的优先级常量。

五、线程池

创建和销毁线程时间以及系统资源的开销较大。如果程序中需要创建大量生命期短的线程,应该使用线程池。线程池中包含许多可运行的空闲线程。将一个 Runnable 或 Callable 对象提交给线程池,线程池就会启动一个线程来执行 run() 或 call() 方法。执行结束后,线程不会死亡,而是返回线程池中成为空闲状态,等待下一次执行。

注意:由于 Thread 类实现了 Runnable 接口,所以Thread类对象也可以提交给线程池。

此外,线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致虚拟机崩溃,而创建一个固定数量的线程池可以控制系统中并发线程数。

线程池创建

执行器( Executor )类有许多静态方法用来构建线程池。创建出的线程池分为两种:提交任务后立即执行 ( ExecutorService ) 类对象和提交任务后延迟指定时间再执行 ( ScheduledExecutorService ) 类对象。

线程池对象类型 创建线程池的静态方法 方法描述
ExecutorService newCachedThreadPool() 创建一个具有缓存功能的线程池。如果线程池中有空闲线程,就利用空闲线程创建任务,否则,创建新线程,线程完成后放入线程池。 缓存型池子通常用于执行一些生存期很短的异步型任务。 空闲线程会被保留60秒。 超过60s,线程实例将被终止及移出池。
ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用的、具有固定线程数的线程池。空闲线程会一直被保留。如果提交的任务数多于空闲线程,那么未服务的任务放入等待队列中。
ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池,顺序执行提交的每一个任务
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 用于预定执行而构建的固定线程池
ScheduledExecutorService newSingleThreadScheduledExecutor() 用于预定执行而构建的单线程池

任务提交

ExecutorService 线程池

对于 ExecutorService 类的线程池而言,有如下三个提交任务给 ExecutorService 线程池的方法:

  • Future<?> submit(Runnable task):任务没有返回值
  • <T> Future<T> submit(Runnable task, T result):task 任务的返回值为 result
  • <T> Future<T> submit(Callable<T> task):可以通过调用返回的 Future 对象的 get() 方法获取任务的返回值。

ExecutorService 类的 submit() 方法返回一个 Future 泛型对象。由于 Runnable 对象没有返回值,所以返回值类型是泛型通配符。此外,可以通过调用 Future 对象的 cancel()、isCancelled()、isDone() 来查询和控制任务的状态。

用完线程池后,应该调用线程池的 shutdown() 方法关闭线程池,调用 shutdown() 方法后,线程池不再接收新任务,但会将以前所有已提交任务执行完成。当线程池中的所有任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程池的 shutdownNow() 方法来关闭线程池,相当于调用每个线程的 interrupt() 方法。

使用 ExecutorService 线程池来执行线程任务的步骤如下。

  1. 调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象,该对象代表一个线程池。
  2. 创建 Runnable 或 Callable 对象,作为线程执行任务。
  3. 调用 ExecutorService 对象的 submit() 方法来提交 Runnable 或 Callable 对象。通过 submit() 方法返回的 Future 对象获取任务的执行结果。
  4. 当不想提交任何任务时,调用 ExecutorService 对象的 shutdown() 方法来关闭线程池。
public class Main implements Serializable {
    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(5);
        Callable<Integer> ca = ()->{System.out.println("hello world!");return 1;};
        Future<Integer> future = pool.submit(ca);
        System.out.println(future.get());
        pool.shutdown();
    }
}

ScheduledExecutorService 线程池

对于 ScheduledExecutorService 类的线程池而言,有如下三个提交任务给 ScheduledExecutorService 线程池的方法:

  • ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit): Runnable 任务将在 delay 延迟后执行。
  • ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit): Callable 任务将在 delay 延迟后执行。
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):Runnable 任务将在 delay 延迟后执行,而且周期性的运行此任务。即在 initialDelay 后开始执行,依次在 initialDelay + period、initialDelay + 2*period··· 处重复执行。如果上次的线程还没有执行完成,那么会阻塞下一个线程的执行,即使有空闲线程。因此period可看作是线程重复执行的最小周期。
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):创建并执行一个在 initialDelay 初始延迟后首次启用的定期操作,随后在每一次执行终止和下一次执行开始之间都暂停 delay 时间。如果任务在任一次执行时遇到异常,就会取消后续执行;否则只能通过程序来显式取消或终止该任务。

六、中断

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。中断操作常用于让等待的线程结束运行。

interrupt()

当对一个线程调用 interrupt() 方法时,

  • 对于可运行态的线程而言,只是将线程的中断状态被置为 true 。
  • 当被中断线程处于可中断的等待态时(即此线程调用了 sleep() 、 wait() 、 join() 等而处于无限等待或限期等待状态时)
    1. 被中断线程将清除中断标志。
    2. 并抛出 InterruptionException 异常。

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

public class InterruptExample {
    private static class MyThread1 extends Thread {        
        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()

interrupted() 查询当前线程是否被中断。此方法为 Thread 类的静态方法。interrupted() 方法会清除线程的中断状态。方法声明为:static boolean interrupted();

isInterrupted()

是 Thread 类的实例方法。查询线程的中断状态,此方法不会改变线程的中断状态。方法声明为:boolean isInterrupted()。

java 没有要求被中断的线程应该终止。中断线程仅仅是将中断标志置位,被中断的线程决定如何响应中断。但普遍的情况是,线程简单地将中断作为一个终止信号,此种线程的 run 方法模板如下:

Runnable r = () -> {
    try{
        ...
        while(!Thread.currentThread().isInterrupted() && more work to do ){
           	do more work
        }
    }
    catch(InterruptedException e){
        //thread was interrupted during sleep or wait
    }
    finally{
        //cleanup,if required
    }
}

如果在 do more work 中调用了 sleep() or wait() 方法,那么无须通过检查中断状态来结束线程,而需要捕获InterruptedException,例如:

Runnable r = () -> {
    try{
        ...
		while(more work to do ){
			do more work
            Thread.sleep(delay);
		}
    }
    catch(InterruptedException e){
        //thread was interrupted during sleep or wait
    }
    finally{
        //cleanup,if required
    }
}

线程池中的线程中断

调用线程池的 shutdownNow() 方法,相当于调用每个线程的 interrupt() 方法,可用于中断所有线程。

如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

七、互斥同步

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

synchronized

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 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

3.同步实例方法

同步方法在方法定义的返回值类型前添加synchronized 关键字。效果相当于同步代码块中同步监视器为this。

Class Foo{
  public synchronized static void methodA(){
    ///
  }
  public void methodB(){
    synchronized(Foo.class)//两者效果相同
  }
}

synchronized关键字不能继承,基类的方法synchronized f(){}在继承类中并不自动是synchronized,而仍是f(){}

4. 同步一个静态方法

public synchronized static void fun() {
    // ...
}

静态和非静态方法的锁互不干预。即类对象的锁和实例对象的锁互补干预。

Lock

锁提供了对共享资源的独占式访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。Lock只作用于使用相同 Lock 对象的各线程。

某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁)。 Lock、ReadWriteLock 是 Java 5 提供的两种根接口,并为 Lock 提供了 ReentrantLock 实现类(可重入锁)。

可重入锁

ReentrantLock 锁具有可重入性,即一个线程可以对已被加锁的 ReentrantLock 对象再次加锁, ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法,即在执行对象中所有同步方法不用再次获得锁。

public class LockExample {
    private Lock lock = new ReentrantLock();
    public void func() {
        lock.lock();
        try {
            //do work
        } 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

把 unlock() 放在 fimally 代码块至关重要,目的是:即使代码出现异常,也能保证线程释放锁,不会出现死锁的情况。

读写锁

ReentrantReadWriteLock 允许对共享资源并发访问,即允许多个读者线程同时访问,而每次只允许一个写者线程访问。

下面是使用读写锁的必要步骤:

  • 构造一个 ReentrantReadWriteLock 对象:

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
  • 抽取读锁和写锁

    private lock readLock = rwl.readLock();
    private lock writeLock = rwl.writeLock();
    
  • 对所有的获取方法加读锁:

    public double getTotalBalance(){
      readLock.lock();
      try{...}
      finally{readLock.unlock();}
    }
    
  • 对所有的修改方法加写锁

    public void transfer(...){
      writeLock.lock();
      try{...}
      finaly{writeLock.unlock();}
    }
    

方法说明:

  • Lock readLock()

    得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。

  • Lock writeLock()

    得到一个写锁,排斥所有其他的读操作和写操作。

tryLock()

tryLock 与 Lock 相比的优点时,当线程成功获得锁时,返回 true,否则,立即返回 false,线程不会阻塞。例如:

if(myLock.tryLock()){
  try{...}
  finally{myLock.unlock();}
}
else
  //do something else

此外,调用 tryLock 时,可以使用超时参数,例如:

if(myLock.tryLock(100,TimeUnit.MILLISECONDS))
  ...

TimeUnit 是一个枚举类型,可以取得值包括:SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS。

lock() 方法不能被中断,如果一个线程在等待一个锁时被中断,被中断的线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么 lock() 方法将无法终止。

而调用带有超时参数的 tryLock(),在等待期间被中断,将抛出中断异常,从而允许程序打破死锁。

比较

1. 锁的实现

synchronized 是一种悲观锁,是 JVM 实现的机制,JVM 会将 synchronized 解释为两条语句:monitorenter 和 monitorexit。由于synchronized 代码出现异常时,JVM 也会释放锁,所以有两条 monitorexit 指令。一条用于正常执行结束时释放锁,一条用于出现异常时释放锁。且每个对象内部都有一个对象锁。

而 ReentrantLock 是 JDK 实现的类。本质上属于乐观锁。它底层实现为 CAS 和 volatile。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如线程自旋和适应性自旋,锁消除 , 锁粗化,轻量级锁和偏向所等。使得 synchronized 与 ReentrantLock 性能相差不大。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,可设置超时,而 synchronized 不可中断,且不能设置超时。

4. 公平锁

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

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

八、线程之间的协作

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

join()

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

public class JoinExample {
    private class A extends Thread {        
        public void run() {
            System.out.println("A");
        }
    }
    private class B extends Thread {
        private A a;
        B(A a) {
            this.a = a;
        }        
        public void run() throws Exception{
            a.join();
            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()

Object 类提供了 wait()、notify() 和 notifyAll() 三个方法,三个方法必须由同步监视器来调用,即类对象或 this。

它们都属于 Object 的实例方法,不属于 Thread 的方法,且只能用在当前同步监视器的同步方法或者同步代码块中,否则会抛出 IllegalMonitorStateExeception。

方法说明:

  • wait():导致当前线程阻塞,直到其他线程调用该同步监视器的 notify() 方法或 notifyAll() 方法来唤醒该线程。该 wait() 方法有三种形式

    1. 无时间参数的 wait (一直等待,知道其他线程通知)

    2. 带毫秒参数的 wait() 。

    3. 带毫秒、毫微秒参数的 wait()(这两种方法都是等待指定时间后自动苏醒)。

调用 wait() 方法的当前线程会释放对同步监视器的锁定。调用wait()方法的线程在被唤醒且获得了锁之后,会从下一句继续执行。被唤醒线程应该再次测试条件,因为无法保证等待条件已经满足。

  • notify():唤醒在此同步监视器上等待的单个线程。如果有多个线程都在此同步监视器上等待,则会随机唤醒一个线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

await() signal() signalAll()

当使用 Lock 进行线程同步时,java 使用 Condition 类进行线程通信。通过调用 Lock 对象的 newCondition() 可以创建该对 Lock 对象的 Condition 实例,一个 Lock 对象可以创建多个 Condition 实例。

Condition类提供了如下三个方法:

  • await():类似于同步监视器上的 wait() 方法,导致当前线程等待,直到其他线程调用该 Condition 的 singnal 方法或 signalAll() 方法来唤醒该线程。该 await() 方法有更多变体,如 long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline) 等。
  • signal():唤醒在此 Lock 对象上等待的单个线程。
  • signalAll():唤醒在此 Lock 对象上等待的所有线程。
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

九、线程局部变量

ThreadLocal 泛型类是线程局部变量的意思,主要功能是,为每一个使用该变量的线程都提供一个变量值的副本,使得每一个线程都可以独立地改变自己的副本,而不会与其他线程冲突,从线程角度看,就好象每一个线程都完全拥有该变量一样。

ThreadLocal 类的用法非常简单,它只提供了如下三个 public 方法。

  • protected T initialValue():返回当前线程中线程局部变量的初始值。线程第一次调用 get() 方法时将调用此方法。但如果线程之前调用了 set() 方法,则不会对该线程再调用 initialValue() 方法,但如果局部变量有了初始值之后,又调用了 remove() 方法,则下次调用 get() 方法时,可能要再次调用此方法。
  • T get():返回此线程局部变量中当前线程副本中的值。如果变量没有赋初始值,则先调用 initialValue() 进行初始化。
  • void remove():删除此线程局部变量中当前线程的值。
  • void set(T value):设置此线程局部变量中当前线程副本中的值。大部分子类不需要重写此方法,他们只依靠 initialValue() 方法来设置线程局部变量的值。

ThreadLocal 将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而无需对该变量进行同步。示例如下:

public class SafeTask implements Runnable {
    private static ThreadLocal<Date> startDate = new ThreadLocal<Date>(){        
        protected Date initialValue() {
            return new Date();
        }        
    };    
    public void run() {        
        System.out.printf("Thread Finished: %s : %s\n", Thread.currentThread().getId(), startDate.get());
    }
    public static void main(String[] args){
        SafeTask st = new SafeTask();
        for(int i=0;i<10;i++){
          Thread t = new Thread(st);
          t.start();
    	}
    }
}

十、J.U.C - AQS(AbstractQueueSynchronizer)

java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

CountdownLatch

用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。

你可以向 CountdownLatch 对象设置一个初始计数值,任何在该对象上调用 wait() 的方法都阻塞,直到这个技术到达0.其他任务在结束其工作时,可以在该对象上调用 countDown() 来减小这个计数值。CountdownLatch 被设计为只出发依次,计数值不能重置。如果需要能够重置计数值的版本,则可以使用 CyclicBarrier。

调用 countDown() 的任务在产生这个调用时并没有被阻塞,只有对 await() 的调用会被阻塞,直至计数值到达0.

CountdownLatch 的典型用法是将一个程序分为 n 个互相独立的可解决任务,并创建值为0的 CountdownLatch。每当任务完成时,都会在这个锁存器上调用 countDown()。等待问题被解决的任务在这个锁存器上调用 await(),将它们自己拦住,直至锁存器技术结束。即,维护了一个计数器 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

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

和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 awati() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 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...

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

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 {        
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }
    private static class Consumer extends Thread {
        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..

十一、多线程经典题目

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

/**
 *@functon 多线程学习 yield
 *@author 林炳文
 *@time 2015.3.9
 */
public class Main {
    public static void main(String[] args) throws Exception {
        Object a = new Object(), b = new Object(), c = new Object();
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);
        new Thread(pa).start();
        //确保按顺序A、B、C执行,sleep参数取值,取决于各线程执行时间。参数值过小,可能出现打印顺序混乱或者死锁
        Thread.sleep(100);  
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
    }
}
class MyThreadPrinter2 implements Runnable {
    private String name;
    private Object prev;
    private Object self;
    MyThreadPrinter2(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (prev) {
                synchronized (self) {
                    System.out.print(name);
                    count--;
                    self.notify();
                }
                try{
                    //保证程序正常结束
                    if(count > 0){
                        prev.wait();
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

代码分析:

该问题为三线程间的同步唤醒操作,主要的目的就是 ThreadA->ThreadB->ThreadC->ThreadA 循环执行。为了控制线程执行的顺序,每个线程执行结束时,要先唤醒后继线程,然后 wait 前继线程。所以每个线程必须持有两个锁。

为了保证三个线程按照 ThreadA,ThreadB,ThreadC 的顺序启动。必须在线程启动代码之间插入睡眠时间,睡眠时间要保证 ThreadA 获取两把锁之后,ThreadB 才能启动。ThreadB 获取两把锁之后 ThreadC 才能启动。然后三线程就可以轮流输出。

此外,还需要注意,当 Thread 最后一次输出后,无须再等待前继线程,直接结束即可。

posted @ 2018-08-29 19:24  Echie  阅读(133)  评论(0编辑  收藏  举报