zoukankan      html  css  js  c++  java
  • 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
    ", 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 最后一次输出后,无须再等待前继线程,直接结束即可。

  • 相关阅读:
    gitlab环境搭建
    PHP能引起安全的函数
    PHP扩展安装方法
    如果Centos没有桌面,怎么修改IP地址
    解决docker 下来镜像出现 error pulling image configuration: Get https://dseasb33srnrn.cloudfront.net的问题
    systemd详解(CentOS 7)
    正确修改MySQL最大连接数的三种好用方案
    CentOS7 yum安装mysql5.5/5.6并初始化
    CentOS7系统配置国内yum源和epel源
    导入镜像后,容器内httpd起不来
  • 原文地址:https://www.cnblogs.com/echie/p/9556398.html
Copyright © 2011-2022 走看看