JUC多线程

1.概述

1.1.并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

8hiHKO.png

1.2.线程与进程

基本概述

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    • 与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈
    • 所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

注意

在 java中,每次程序运行至少启动2个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 其实在就是在操作系统中启动了一个进程。

线程的分类

  • 单线程:同一时间只能干一件事.(多件事只能等一个处理完成后才能开始处理下一个)
  • 多线程:同一时间能干多件事情。(可以辅助线程的并行理解)
  • 主线程:程序启动系统自动创建并执行 main 方法的线程。主线程的执行入口:main方法 (说起主线程在这里顺便提一下 守护线程)
  • 守护线程:指为其他线程提供服务的线程,也称为守护线程。JVM 的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
  • 子线程:除了主线程以为的其他线程,子线程的执行入口:run方法

线程调度

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

上下文切换

当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

1.3.线程的生命周期(状态)

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用 start 方法。
Runnable(可运行) 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked 状 态;当该线程持有锁时,该线程将变成 Runnable 状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用 notify 或者 notifyAll 方法才能够唤醒。
Timed Waiting(计时等待) 同 waiting 状态,有几个方法有超时参数,调用他们将进入Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait。
Teminated(被终止) 因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了run 方法而死亡。

8WYz28.jpg

提示

  • Waiting(无限等待) 状态中wait方法是空参的,而 Timed Waiting(计时等待) 中wait方法是带参的。
    • 如果没有得到(唤醒)通知,那么线程就处于Timed Waiting 状态,直到倒计时完毕自动醒来
    • 如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting 状态立刻唤醒。

1.4.线程死锁

概述

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生条件

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

  • 破坏互斥条件
    • 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件
    • 一次性申请所有的资源。
  • 破坏不剥夺条件
    • 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件
    • 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

2.多线程的实现

Java 多线程实现方式主要有四种:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口通过 FutureTask 包装器来创建 Thread 线程、
  • 使用 ExecutorService、Callable、Future 实现有返回结果的多线程。

其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。此处参考

2.1.继承 Thread 类创建线程

java.lang.Thread,Thread类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread 类的start()实例方法。

  • start()方法是一个 native 方法,它将启动一个新线程,并执行 run() 方法。
  • 这种方式实现多线程很简单,通过自己的类直接 extend Thread,并复写 run() 方法,就可以启动新线程并执行自己定义的 run() 方法。
public class MyThread extends Thread {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}  
 
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();  

构造方法

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法

  • public String getName(): 获取当前线程名称。
  • public void start():导致此线程开始执行; Java虚拟机调用此线程的 run 方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

2.2.实现 Runnable 接口创建线程

java.lang.Runnable,如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口

步骤

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

代码实现

public class MyRunnable implements Runnable{
    @Override
    public void run() {
    	for (int i = 0; i < 20; i++) {
    		System.out.println(Thread.currentThread().getName()+" "+i);
    	}
    }
}

public class Demo {
    public static void main(String[] args) {
    //创建自定义类对象 线程任务对象
    MyRunnable mr = new MyRunnable();
    //创建线程对象
    Thread t = new Thread(mr, "小强");
    t.start();
    for (int i = 0; i < 20; i++) {
        System.out.println("旺财 " + i);
        }
    }
}

小结

  • 通过实现 Runnable 接口,使得该类有了多线程类的特征。run() 方法是多线程程序的一个执行目标。所有的多线程代码都在 run 方法里面。Thread 类实际上也是实现了 Runnable 接口的类。
  • 在启动的多线程的时候,需要先通过 Thread类 的构造方法Thread(Runnable target) 构造出对象,然后调用Thread 对象的 start() 方法来运行多线程代码。
  • 实际上所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread类 还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的。
  • Runnable 对象仅仅作为 Thread 对象的 target ,Runnable 实现类里包含的 run() 方法仅作为线程执行体
    而实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run() 方法。

2.3.实现 Callable 接口

实现 Callable 接口通过 FutureTask 包装器来创建 Thread 线程

步骤

  1. 创建 Callable 接口的实现类 ,并实现 Call 方法
  2. 创建 Callable 实现类的实现,使用 FutureTask 类包装 Callable 对象,该 FutureTask 对象封装了 Callable 对象的 Call 方法的返回值
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程
  4. 调用 FutureTask 对象的 get() 来获取子线程执行结束的返回值
  • Callable接口(也只有一个方法)定义如下
public interface Callable<V>   { 
  V call() throws Exception;   } 
class Tickets<Object> implements Callable<Object>{
 
    //重写call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   
}
 public class ThreadDemo03 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
 
        Thread t = new Thread(oneTask);
 
        System.out.println(Thread.currentThread().getName());
 
        t.start(); 
    } 
}

2.4.线程池创建线程

线程池的构建:

  • 通过 ThreadPoolExecutor 构造函数实现(推荐)
  • 通过工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor
    • FixedThreadPool
    • SingleThreadExecutor
    • CachedThreadPool

步骤

此处只展示使用,后面会详细介绍,Runnable+Executors

  1. 实现 Runable 或Callable(有返回值)接口,重写 run 方法
  2. 使用 ExectorService 的相关方法创建 想要的 线程池,这里使用了newFixedThreadPool()
  3. 调用 execute 方法,执行任务

上代码


public class ThreadDemo05{
 
    private static int POOL_NUM = 10;     //线程池数量
 
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {  
            RunnableThread thread = new RunnableThread();
 
            //Thread.sleep(1000);
            executorService.execute(thread);  
        }
        //关闭线程池
        executorService.shutdown(); 
    }   
 
}
 
class RunnableThread implements Runnable  
{     
    @Override
    public void run()  
    {  
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");  
 
    }  
}  

2.5.小结

使用建议

  • 前面两种可以归结为一类:无返回值,原因很简单,通过重写 run 方法,run 方式的返回值是 void,所以没有办法返回结果
  • 后面两种可以归结成一类:有返回值,通过 Callable 接口,就要实现 call 方法,这个方法的返回值是Object,所以返回的结果可以放在 Object 对象中

3.线程池

**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

8RvjsI.png

好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池状态

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。推荐阅读

  1. RUNNING运行状态

    • 状态说明:线程池处在 RUNNING 状态时,能够接收新任务,以及对已添加的任务进行处理。
    • 状态切换:线程池的初始化状态是 RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING 状态,并且线程池中的任务数为 0!
  2. SHUTDOWN关闭状态

    • 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    • 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
  3. STOP停止状态

    • 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    • 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
  4. TIDYING整理状态

    • 状态说明:当所有的任务已终止,ctl 记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()

      terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载 terminated()函数来实现。

    • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
      当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

  5. TERMINATED已终止状态

    • 状态说明:线程池彻底终止,就变成 TERMINATED 状态。
    • 状态切换:线程池处在 TIDYING 状态时,执行完 terminated() 之后,就会由 TIDYING -> TERMINATED。

3.1. Executor 框架

此处参考

简介

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

  • this 逃逸是指在构造函数返回之前,其他线程就持有该对象的引用。调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

3 部分结构

  1. 任务(Runnable /Callable)
  • 执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。
  1. 任务的执行(Executor)

8hFmR0.jpg

  • 任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。
  • ThreadPoolExecutorScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。
  • 我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。
  • ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。 (了解)
//AbstractExecutorService实现了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

    //ScheduledExecutorService实现了ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService
  1. 异步计算的结果(Future)
  • Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。
  • 当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。(调用 submit()方法时会返回一个 FutureTask 对象)

3.2.框架的使用

8WAjVH.png

  1. 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  2. 把创建完成的实现 Runnable/Callable接口的 对象
    • 直接交给 ExecutorService执行: ExecutorService.execute(Runnable command)
    • 或者提交给 ExecutorService 执行
      • ExecutorService.submit(Runnable task)
      • 或者ExecutorService.submit(Callable <T> task)
  3. 如果执行ExecutorService.submit(…),ExecutorService 将返回一个实现 Future 接口的对象
    • 我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象。
    • 由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
  4. 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

3.3. ThreadPoolExcutor (推荐)

线程池实现类 ThreadPoolExecutor是 Executor 框架最核心的类。

3.3.1.构造方法

ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)

 /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

三个重要参数

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。

其它参数

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
  • unit : keepAliveTime参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。

各参数示意图

8WZ2Kf.jpg

饱和策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy调用执行自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
    • 因此这种策略会降低对于新任务提交速度,影响程序的整体性能。
    • 另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

举例

  • Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,
  • 当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
  • 在默认情况下,ThreadPoolExecutor将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。
  • 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。

workQueue

  • SynchronousQueue
    • 这是一个内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然。
  • LinkedBlockingQueue
    • 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。
  • ArrayBlockingQueue
    • 基于链表结构,无限队列。这个容量就是 Integer.MAX_VALUE

3.3.2.使用示例

我们来示例Callable+ThreadPoolExecutor

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回执行当前 Callable 的线程名字
        return Thread.currentThread().getName();
    }
}
public class CallableDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任务到线程池
            Future<String> future = executor.submit(callable);
            //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //关闭线程池
        executor.shutdown();
    }
}

线程池原理示意

8WQnJg.png

3.4.工具类 Executors (不推荐)

常见的线程池:

  1. FixedThreadPool:返回固定数量线程
  2. SingleThreadExcutor:返回一个线程
  3. CashedThreadPool:按线程数分配,空闲的线程

3.4.1. FixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:

  /**
     * 创建一个可重用固定数量线程的线程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 从上面源代码可以看出新创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。

为什么不推荐

FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
  2. 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被设置为同一个值。
  3. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
  4. 运行中的 FixedThreadPool(未执行 shutdown()shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

3.4.2. SingleThreadExector

SingleThreadExecutor 是只有一个线程的线程池,下面看看SingleThreadExecutor 的实现:

/**
     *返回只有一个线程的线程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 从上面源代码可以看出新创建的 SingleThreadExecutorcorePoolSizemaximumPoolSize 都被设置为 1 .其他参数和 FixedThreadPool 相同。

为什么不推荐使用

  • SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。
  • SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。
  • 说简单点就是可能会导致 OOM,

3.4.3. CachedThreadPool

CachedThreadPool 是一个会根据需要创建新线程的线程池。下面通过源码来看看 CachedThreadPool 的实现:

/**
     * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  • CachedThreadPoolcorePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,
  • 这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

为什么不推荐使用

CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

4.线程安全

线程安全问题都是由全局变量及静态变量引起的

  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
  • 若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

4.1. synchronized 同步

同步代码块

  • 同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
  • 同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
    1. 锁对象 可以是任意类型。
    2. 多个线程对象 要使用同一把锁。
  • 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
synchronized(同步锁){
	需要同步操作的代码
}

同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
  • 同步锁
    • 对于非 static 方法,同步锁就是 this 。
    • 对于 static 方法,我们使用当前方法所在类的字节码对象(类名.class)。
public synchronized void method(){
	可能会产生线程安全问题的代码
}

4.2. ReentrantLock锁

ReentrantLock轻量级锁)也可以叫对象锁,可重入锁,互斥锁。synchronized重量级锁,JDK前期的版本lock比synchronized更快,在JDK1.5之后synchronized引入了偏向锁,轻量级锁和重量级锁。以致两种锁性能旗鼓相当,看个人喜欢。

  • java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
  • 同步代码块/同步方法具有的功能 Lock 都有,除此之外更强大,更体现面向对象。

Lock 锁也称同步锁,加锁与释放锁方法化了:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。
  • ReentrantLock.tryLock():它表示用来尝试获取锁
    • 如果获取成功,则返回true,
    • 如果获取失败(即锁已被其他线程获取),则返回 false ,
    • 这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
  • ReentrantLock.tryLock(long timeout,TimeUnit unit):和tryLock()方法是类似的
    • 这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
    • 如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
public class Ticket implements Runnable{ 
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    /*
    * 执行卖票操作
    */ @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while(true){
    		lock.lock(); 
            if(ticket>0){//有票 可以卖
                //出票操作
                //使用sleep模拟一下出票时间
                try {
                Thread.sleep(50);
                } catch (InterruptedException e) {
                // TODO Auto‐generated catch block e.printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖:"+ticket‐‐);
   		 }
   		 lock.unlock();
    	}
    }	
}

4.3.等待/唤醒机制

此处参考

  • synchronized关键字与 wait()notify() / notifyAll() 方法相结合可以实现等待/通知机制

    • 对象名.wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
    • 对象名.notify():唤醒在此对象监视器上等待的单个线程。
    • 对象名.notifyAll():唤醒在此对象监视器上等待的所有线程。
  • ReentrantLock类当然也可以实现,但是需要借助于Condition 接口与 new Condition() 方法。

    • void await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
    • void signal() :唤醒一个等待线程。
    • void signalAll(): 唤醒所有等待线程。
    class BoundedBuffer {
                final Lock lock = new ReentrantLock(); //创建一个Lock的子类对象 lock
                final Condition notFull = lock.newCondition(); //调用lock的newCondition方法,创建一个condition子类对象 notFull
                final Condition notEmpty = lock.newCondition(); //调用lock的newCondition方法,创建一个condition子类对象 notEmpty
    
                public void put(Object x) throws InterruptedException { //
                    lock.lock(); //获取锁
                    try {
                        while (判断语句)
                          notFull.await(); //判断成功,线程等待于notFull下。
    
                        操作代码
    
                        notEmpty.signal(); //唤醒notEmpty下的等待线程。
                    } finally { //保证其后语句执行。
                         lock.unlock(); //释放锁。
                    }
                }
    
                public Object take() throws InterruptedException {
                    lock.lock();
                    try {
                      while ()
                        notEmpty.await();
                      操作代码
                      notFull.signal();
    
                    } finally {
                          lock.unlock();
                    }
                }
              }
    

4.4.两者的区别

  1. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
    • synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
    • ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
  2. ReentrantLock 比 synchronized 增加了一些高级功能
    • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
    • 可实现选择性通知(锁可以绑定多个条件) 在使用 notify()/notifyAll() 方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”

5.拓展

5.1. ConcurrentHashMap 锁

我们发现 HashTable 虽然能保证线程安全但是效率低下,而 HashMap 虽然效率高于 HashTable 但是是非线程安全的。

HashMap

HashMap:它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。

  • HashMap最多只允许一条记录的键为null**,允许多条记录的值为null**。
  • HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,
    • 可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,
    • 或者使用ConcurrentHashMap。

HashTable

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

  • 确保同一时间只有一个线程对同步方法的占用,避免多个线程同时对数据的修改,确保线程的安全性。
  • HashTable 对 get,put,remove 方法都使用了同步操作,这就造成如果两个线程都只想使用 get 方法去读取数据时,因为一个线程先到进行了锁操作,另一个线程就不得不等待,这样必然导致效率低下,而且竞争越激烈,效率越低下。

并发又安全的 ConcurrentHashMap

ConcourrentHashMap 保证线程安全的方法是:分段锁技术

  • 在hashMap 的基础上,ConcurrentHashMap 将数据分为多个segment(默认16个),然后每次操作对一个segment 加锁

    • 由于所有访问HashTable的线程都必须竞争同一把锁,而ConcurrentHashMap 将数据分到多个segment 中(默认16,也可在申明时自己设置,不过一旦设定就不能更改,扩容都是扩充各个segment 的容量)
    • 每个segment 都有一个自己的锁,只要多个线程访问的不是同一个segment 就没有锁争用,就没有堵塞,也就是允许16个线程并发的更新而尽量没有锁争用。
  • ConcurrentHashMap 的 segment 就类似一个HashTable,但比HashTable 更加优化,

    • 前面说过 HashTable 对 get,put,remove 方法都会使用锁,
    • 而 ConcurrnetHashMap 中get 方法是不涉及到锁的。在并发读取时,除了key 对应的 value 为 null 外,并没有用到锁,所以对于读操作无论多少线程并发都是安全高效的。

5.2. CountDownLatch

CountDownLatch 是在 java1.5 被引入,存在于 java.util.cucurrent 包下。是一个同步工具类,用来协调多个线程之间的同步请参考

  • CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就 -1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

应用场景

  1. 某一线程在开始运行前等待n个线程执行完毕。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
    • 将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1, 当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。
  2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。
    • 做法是初始化一个共享的 CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用 countDown()时,计数器变为0,多个线程同时被唤醒。

不足

  • CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值
  • 当CountDownLatch使用完毕后,它不能再次被使用

常用方法

  • CountDownLatch(int count):构造方法,创建一个值为count 的计数器。
  • await():阻塞当前线程,将当前线程加入阻塞队列。
  • await(long timeout, TimeUnit unit):在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
  • countDown():对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
  • getCount():获取 当前 count 值,是个会变的东西,在循环里使用时需注意

使用示例

public class TestCountDownLatch {

    public static void main(String[] args){
		//CountDownLatch 为唯一的、共享的资源
        final CountDownLatch latch = new CountDownLatch(5);
		
        LatchDemo latchDemo = new LatchDemo(latch);

        long begin = System.currentTimeMillis();

        for (int i = 0; i <5 ; i++) {
            new Thread(latchDemo).start();
        }
        try {
            //多线程运行结束前一直等待
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        
        System.out.println("耗费时间:"+(end-begin));

    }
}

class LatchDemo implements  Runnable{

    private CountDownLatch latch;

    public LatchDemo(CountDownLatch latch){
        this.latch=latch;
    }
    public LatchDemo(){
        super();
    }

    @Override
    public void run() {
        //当前对象唯一,使用当前对象加锁,避免多线程问题
        synchronized (this){
            try {
                for (int i = 0; i < 50000; i++) {
                    if (i%2==0){
                        System.out.println(i);
                    }
                }
            }finally {
                //保证肯定执行
                latch.countDown();
            }
        }
    }
}

5.3. ReadWriteLock 读写锁

我们编程想要实现的最好效果是,可以做到读和读互不影响读和写互斥写和写互斥,提高读写的效率,如何实现呢? 推荐阅读

java并发包中 ReadWriteLock 是一个接口,

  • ReadWriteLock 管理一组锁,一个是只读的锁,一个是写锁。
  • Java 并发库中ReetrantReadWriteLock 实现了 ReadWriteLock 接口并添加了可重入的特性。

主要有两个方法,如下:

  • Lock readLock();
  • Lock writeLock();

进入条件

  • 线程进入读锁的前提条件:
    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
  • 线程进入写锁的前提条件:
    • 没有其他线程的读锁
    • 没有其他线程的写锁

重要特性

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 可重进入:读锁和写锁都支持线程重进入。
  • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

使用示例

public class TestReadWriteLock {

    public static void main(String[] args){
        ReadWriteLockDemo rwd = new ReadWriteLockDemo();
		//启动100个读线程
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rwd.get();
                }
            }).start();
        }
        //写线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                rwd.set((int)(Math.random()*101));
            }
        },"Write").start();
    }
}

class ReadWriteLockDemo{
	//模拟共享资源--Number
    private int number = 0;
	// 实际实现类--ReentrantReadWriteLock,默认非公平模式
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //读
    public void get(){
    	//使用读锁
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+" : "+number);
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
    //写
    public void set(int number){
        readWriteLock.writeLock().lock();
        try {
            this.number = number;
            System.out.println(Thread.currentThread().getName()+" : "+number);
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

5.4.线程八锁

  • 一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其他的线程都只能等待,换句话说,某一时刻内,只能有唯一一个线程去访问这些 synchronized 方法。

  • 锁的是当前对象 this,被锁定后,其他线程都不能进入到当前对象的其他的 synchronized 方法。

  • 加个普通方法后发现和同步锁无关。

  • 换成静态同步方法后,情况又变化

  • 所有的非静态同步方法用的都是同一把锁,即实例对象本身,或者说 this 对象,

    • 如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁。
    • 如果别的对象的非静态同步方法与该实例对象的非静态同步方法获取不同的锁,则不需要等待。
  • 所有的静态同步方法用的也是同一把锁,即类对象本身,所以静态同步方法与非静态同步方法之间是不会有竞态条件的,但是一个静态同步方法获取Class实例的锁后,其他静态同步方法都必须等待该方法释放锁才能获取锁。

  • 所谓线程八锁实际上对应于是否加上 synchronized,是否加上 static 等8种常见情况,

代码如下

/*
 * 题目:判断打印的 "one" or "two" ?
 * 
 * 1. 两个普通同步方法,两个线程,标准打印, 打印? //one  two
 * 2. 新增 Thread.sleep() 给 getOne() ,打印? //one  two
 * 3. 新增普通方法 getThree() , 打印? //three  one   two
 * 4. 两个普通同步方法,两个 Number 对象,打印?  //two  one
 * 5. 修改 getOne() 为静态同步方法,打印?  //two   one
 * 6. 修改两个方法均为静态同步方法,一个 Number 对象?  //one   two
 * 7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象?  //two  one
 * 8. 两个静态同步方法,两个 Number 对象?   //one  two
 * 
 * 线程八锁的关键:
 * ①非静态方法的锁默认为  this,  静态方法的锁为 对应的 Class 实例
 * ②某一个时刻内,只能有一个线程持有锁,无论几个方法。
 */
public class TestThread8Monitor {

    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();

        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            } 
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
//              number.getTwo();
                number2.getTwo();
            }
        }).start();

        /*new Thread(new Runnable() {
            @Override
            public void run() {
                number.getThree();
            }
        }).start();*/

    }

}

class Number{

    public static synchronized void getOne(){//Number.class
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }

        System.out.println("one");
    }

    public synchronized void getTwo(){//this
        System.out.println("two");
    }

    public void getThree(){
        System.out.println("three");
    }

}

5.5. volatile

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。 参考

原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  • 对于可见性,Java提供了volatile关键字来保证可见性
  • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 通过 synchronized 和 Lock 也能够保证可见性

有序性

即程序执行的顺序按照代码的先后顺序执行。

  • 在 Java 里面,可以通过 volatile 关键字来保证一定的“有序性”。
  • 另外可以通过 synchronized 和 Lock 来保证有序性。

volatile

  • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。

volatile 的特性

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性
  • 更多细节:参考

5.6.悲观锁与乐观锁

此处参考

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
  • 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

使用场景

  • 乐观锁适用于写比较少的情况下(多读场景)
  • 一般多写的场景下用悲观锁就比较合适。

乐观锁的实现

  • 版本号机制
    • 一般是在数据表中加上一个数据版本号 version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取 version值,在提交更新时,若刚才读取到的version值为当前数据库中的 version值相等时才更新,否则重试更新操作,直到更新成功。
  • CAS 算法

乐观锁的缺点(同时也是CAS的缺点)

  1. ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference类就提供了此种能力,其中的 compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

  1. 循环时间长开销大

    自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  2. 只能保证一个共享变量的原子操作

    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

5.7. CAS 算法

即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 参考

CAS 算法会先对一个内存变量(位置) V 和一个给定的值进行比较 A ,如果相等,则用一个新值 B 去修改这个内存变量(位置)。上述过程会作为一个原子操作完成 (intel处理器通过 cmpxchg 指令系列实现)。CAS 原子性保证了新值的计算是基于上一个有效值,期间如果内存变量(位置) V 被其他线程更新了,本线程的 CAS 更新操作将会失败。CAS 操作必须告诉调用者成功与否,可以返回一个 boolean 值来表示,或者返回一个从内存变量读到的值 (应该是上一次有效值)

CAS 操作数有三个:

  • 内存变量(位置) V,表示被更新的变量
  • 线程上一次读到的旧值 A
  • 用来覆盖 V 的新值 B

CAS 表示:“我认为现在 V 的值还是之前我读到的旧值 A,若是则用新值 B 覆盖内存变量 V,否则不做任何动作并告诉调用者操作失败”。CAS 是一项乐观锁技术,他在更新的时候总是希望能成功 (没有冲突),但也能检测出来自其他线程的冲突和干扰

++i 自增操作

incrementAndGet

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

这里采用了 CAS 操作,每次从中读取数据都会将此数据和 +1 后的结果进行 CAS 操作,如果成功则返回结果否则重试到成功为止,而这里的compareAndSet利用 JNI( JNI: Java Native Interface为 JAVA 本地调用,允许java 调用其他语言。)来完成CPU的指令操作

compareAndSet 的代码

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

整体的过程就是这样子的,利用 CPU的CAS指令,同时借助 JNI 来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。

compareAndSwapInt (native)类似这样的逻辑:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

if (this == expect) {
  this = update
 return true;
} else {
return false;
} 

CAS是通过调用 JNI 代码实现的,而 compareAndSwapInt 就是就是借用 CAS 来调用 CPU底层指令实现的,调用了Atomic::cmpxchg方法

5.8. Atomic 原子类

Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

所以,所谓原子类说简单点就是具有原子/原子操作特征的类。

并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic

JUC 包中的原子类主要分为4类:

基本类型:使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型:使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新引用类型里的字段原子类
  • AtomicMarkableReference:原子更新带有标记位的引用类型,该类将 boolean 标记与引用关联起来,不能解决ABA的问题,只是会降低ABA问题发生的几率

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicStampedReference原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

暂时码到这里

留个参考地址

5.9. ThreadLocal

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

特点:

  • ThreadLocal 可以像Map 一样存储数据(它的 key 就是当前线程对象)
  • 一般 ThreadLocal 定义的变量都是 static 类型
  • 当线程销毁后,ThreadLocal 中保存到的数据将自动的被 jvm 释放。

6.常见的区分

6.1 Thread vs Runnable

如果一个类继承 Thread ,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现 Runnable 接口比继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免 java 中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类。

6.2 Runnable vs Callable

  • Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
  • 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
    • Executors.callable(Runnable task)
    • Executors.callable(Runnable task,Object resule)

6.3. sleep vs wait

  • 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁
  • 两者都可以暂停线程的执行。
  • wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
  • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

6.4. start vs run

  • 调用 start 方法方可启动线程并使线程进入就绪状态,
  • 而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

所以,我们调用 start() 方法时会自动执行 run() 方法,但不能直接调用 run() 方法

6.5. excute vs submit

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
  • submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

6.6. shutdown 关闭

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

以后在拓展:插个眼

发布了17 篇原创文章 · 获赞 6 · 访问量 640

猜你喜欢

转载自blog.csdn.net/black210/article/details/105177906
今日推荐