天天看点

Java并发知识点总结Java并发知识点总结

Java并发知识点总结

synchronized

  1. 理解:synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
    • Java 6 之前:重量级锁,效率低下
    • Java 6 开始:有较大优化,引入了如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁的开销
  2. 使用方式:
    • 修饰实例方法:作用于当前对象实例加锁,进入同步方法前要先获得当前对象实例的锁
    • 修饰静态方法:给当前类加锁
    • 修饰代码块:给指定对象加锁
      例:手写单例模式,解释一下双重检验方式实现单例模式的原理
      public class Singleton {
          private volatile static Singleton instance;
      
          public static Singleton getInstance() {
              if (instance == null) {
                  synchronized (instance) {
                      if (instance == null) {
                          instance = new Singleton();
                      }
                  }
              }
              return instance;
          }
      }
                 

      instance = new Singleton();

      这段代码分三步执行:
      1. 为 instance 分配内存空间
      2. 初始化 instance
      3. 将 instance 指向分配的内存地址

      因为 JVM 存在指令重排序,执行顺序可能变成 1 -> 3 -> 2 。在多线程环境下会导致一个线程获得还没有初始化的实例。例如:线程 t1 执行了 1 和 3,此时 t2 调用 getInstance() 时发现 instance 不为空,因此返回 instance,但此时 instance 还未被初始化。

      使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

  3. 底层原理

    synchronized 关键字底层原理属于 JVM 层面

    1. synchronized 同步语句块的情况
      public class SynchronizedDemo01 {
          public void method() {
              synchronized (this) {
                  System.out.println("synchronized");
              }
          }
      }
                 
      通过 jdk 自带的 javap 命令查看 SynchronizedDemo01 类的相关字节码信息:首先切换到类的对应目录执行

      javac SynchronizedDemo01.java

      命令生成编译后的 .class 文件,然后执行

      javap -c -s -v -l SynchronizedDemo01.class

Java并发知识点总结Java并发知识点总结

从上面可以看出:synchronized 同步语句块的实现是用 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

  1. synchronized 修饰方法的情况
    public class SynchronizedDemo02 {
        public synchronized void method() {
            System.out.println("synchronized");
        }
    }
               
    字节码信息:
Java并发知识点总结Java并发知识点总结
从上面可以看出:在同步方法上面存在一个 ACC_SYNCHRONIZED  标识该方法是一个同步方法
           
  1. jdk 6之后的 synchronized 做的优化

    优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁粗化等

    锁的四种状态:无锁、偏向锁、轻量级锁、重量级锁,它们会随着竞争的激烈而逐渐升级,但是不可降级。

    参考:

    https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md

  2. synchronized和ReentrantLock 的区别与联系
    1. 两者都是可重入锁
    2. synchronized依赖于 JVM 而 ReentrantLock 依赖于 API
    3. ReentrantLock 比 synchronized 增加了一些高级功能
      1. 等待可中断
      2. 可实现公平锁
      3. 可实现选择性通知(锁可以绑定多个条件)

volatile

  1. Java内存模型:在当前的Java内存模型下,线程可以把变量保存本地内存中,而不是直接在内存中进行读写。这就可能造成一个线程在内存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
Java并发知识点总结Java并发知识点总结
  1. volatile的作用:
    • 告诉 JVM 这个变量是不稳定的,每次使用它都到主存中进行读取
    • 防止指令重排序
Java并发知识点总结Java并发知识点总结
syncgronized 关键字 和 volatile 关键字比较
  • volatile 是线程同步的轻量级实现,所以性能比synchronized要更好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
  • 多线程访问 volatile 关键字不会阻塞,而 synchronized 关键字可能会发生阻塞
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证
  • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性

ThreadLocal

ThreadLocal

类可以让每个线程绑定自己的值,可以将

ThreadLocal

类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

线程池

  1. 为什么要使用线程池?

    池化技术的主要思想是为了减少每次获取资源的消耗,提高对资源的利用率

  2. 使用线程池的好处:
    • 降低资源消耗:提高重复利用已创建的线程降低创建和销毁造成的消耗
    • **提高响应速度:**当任务到达时,任务可以不需要等到线程创建就能立即执行
    • **提高线程的可管理性:**线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  3. 实现 Runnable 和 Callable 接口的区别

    Runnable

    接口不会返回结果或抛出检查异常,但是**

    Callable

    接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用

    Runnable

    接口,这样代码看起来会更加简洁。
    • Runnable 接口
    @FunctionalInterface
    publicinterface Runnable {
       /**
        * 被线程执行,没有返回值也无法抛出异常
        */
        public abstract void run();
    }
               
    • Callable 接口
    @FunctionalInterface
    publicinterface Callable<V> {
        /**
         * 计算结果,或在无法这样做时抛出异常。
         * @return 计算得出的结果
         * @throws 如果无法计算结果,则抛出异常
         */
        V call() throws Exception;
    }
               
    工具类

    Executors

    可以实现

    Runnable

    对象和

    Callable

    对象之间的相互转换。
    • Executors.callable(Runnable task)

    • Executors.callable(Runnable task,T result)

  4. 执行

    execute()

    方法和

    submit()

    方法的区别:
    1. execute()

      方法用于提交不需要返回值的任务,所以无法判断任务被线程池执行成功与否
    2. submit()

      方法用于提交需要返回值的任务。线程池会返回一个

      Future

      类型的对象,通过这个

      Future

      对象可以判断任务是否执行成功,并且可以通过

      Future

      get()

      方法来获取返回值,

      get()

      方法会阻塞当前线程知道任务完成,而使用

      get(long timeout,TimeUnit unit)

      方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
  5. 如何创建线程池

    《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

    Executor 返回线程池对象的弊端如下:
    • FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为

      Integer.MAX_VALUE

      ,可能堆积大量的请求,从而导致OOM
    • CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为

      Integer.MAX_VALUE

      ,可能会创建大量线程,从而导致OOM
    1. 通过构造方法实现
      Java并发知识点总结Java并发知识点总结
    2. 通过 Executor 框架的工具类 Executors 来实现,可以创建三种类型的ThreadPoolExecutor:
      • FixedThreadPool:返回一个固定线程数量的线程池,且线程池中的线程数量始终不变。当有一个新任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
      • SingleThreadPool:方法返回一个只有一个线程的线程池。若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
      • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用

        对应Executors工具类中的方法如图所示,方法内部其实是调用了ThreadPoolExecutor的构造方法

        Java并发知识点总结Java并发知识点总结
  6. 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.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
               
    1. ThreadPoolExecutor

      构造函数重要参数分析

      ThreadPoolExecutor

      3 个最重要的参数:
      • corePoolSize

        :核心线程数
      • maximumPoolSize

        :最大线程数
      • workQueue

        :阻塞队列
      其它参数:
      • keepAliveTime

        :普通线程空闲等待的时间超过了

        keepAliveTime

        会被回收销毁
      • unit

        :

        keepAliveTime

        参数的时间单位
      • threadFactory

        :executor 创建新线程的时候会用到
      • handler

        :拒绝策略
    2. 拒绝策略:

      如果当前阻塞队列已满且线程池内的线程数达到了最大线程数,此时提交任务,线程池就会执行拒绝策略,

      ThreadPoolTaskExecutor

      定义一些策略:
      • AbortPolicy

        :拒绝任务并抛出异常
      • CallerRunsPolicy

        :使用调用者所在线程来运行任务
      • ThreadPoolExecutor.DiscardOldestPolicy

        :丢弃队列里最近的一个任务,并执行当前任务
      • ThreadPoolExecutor.DiscardPolicy

        :不处理直接丢弃任务
      默认使用的是**

      AbortPolicy

      **策略
    3. 线程池执行

      execute()

      方法的过程:
      1. 工作线程数小于核心线程数时,直接新建核心线程执行任务;
      2. 大于核心线程数时,将任务添加进等待队列;
      3. 队列满时,创建非核心线程执行任务;
      4. 工作线程数大于最大线程数时,执行拒绝策略
Java并发知识点总结Java并发知识点总结
> ### 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor`
  >
  > 1. 首先创建一个 `Runnable` 接口的实现类 `MyRunnable.java`
  >
  >    ```java
  >    import java.util.Date;
  >    
  >    public class MyRunnable implements Runnable{
  >    
  >        private String command;
  >    
  >        public MyRunnable(String s) {
  >            this.command = s;
  >        }
  >    
  >        @Override
  >        public void run() {
  >            System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
  >            processCommand();
  >            System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
  >        }
  >    
  >        private void processCommand() {
  >            try {
  >                Thread.sleep(5000);
  >            } catch (InterruptedException e) {
  >                e.printStackTrace();
  >            }
  >        }
  >    
  >        @Override
  >        public String toString() {
  >            return "MyRunnable{" +
  >                    "command='" + command + '\'' +
  >                    '}';
  >        }
  >    }
  >    
  >    ```
  >
  > 2. 创建`ThreadPoolExecutorDemo.java`
  >
  >    ```java
  >    import java.util.concurrent.ArrayBlockingQueue;
  >    import java.util.concurrent.ThreadPoolExecutor;
  >    import java.util.concurrent.TimeUnit;
  >    
  >    public class ThreadPoolExecutorDemo {
  >        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());
  >    
  >            for (int i = 0; i < 10; i++) {
  >                //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
  >                Runnable worker = new MyRunnable("" + i);
  >                //执行Runnable
  >                executor.execute(worker);
  >            }
  >            //终止线程池
  >            executor.shutdown();
  >            while (!executor.isTerminated()) {
  >            }
  >            System.out.println("Finished all threads");
  >        }
  >    }
  >    
  >    ```
           

5 Atomic 原子类

  1. 介绍:

    原子类就是具有原子操作特征的类。并发包

    java.util.concurrent

    的原子类都存放在

    java.util.concurrent.atomic

    下。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ea53l9hG-1595302640206)(C:\Users\ykyu3\AppData\Roaming\Typora\typora-user-images\image-20200721110829375.png)]

  2. JUC 包中的原子类分为4类:
    • 基本类型
      • AtomicInteger
      • AtomicLong
      • AtomicBoolean
    • 数组类型
      • AtomicIntegerArray
      • AtomicLongArray
      • AtomicReferenceArray
    • 引用类型
      • AtomicReference
      • AtomicStampedReference:原子更新引用类型里的字段原子类
      • AtomicMarkableReference:原子更新带有标记为的引用类型
    • 对象的属性修改类型
      • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
      • AtomicLongFieldUpdater:原子更新长整型字段的更新器
      • AtomicStampedReference:原子更新带有版本号的引用类型