1. 线程

1.1 概念

  • 与进程的关系
    • 进程:程序的一次执行过程,是系统运行程序的基本单位,具有独立性(地址空间)、动态性(生命周期)、并发性;
    • 线程:轻量级进程,一个进程的执行过程中可产生多个线程,具有独立性、抢占性、并发性;
  • 与进程的区别

    • 进程是系统进行资源分配和调度的基本单位,线程是其父进程的执行单位;
    • 同类的多个线程共享进程的堆、方法区、直接内存资源,单个线程独立拥有程序计数器、虚拟机栈和本地方法;
      • 补充
        • 堆主要用于存放新创建的对象,方法区主要用于存放已被加载的类信息、常量、静态变量等数据;
        • 程序计数器保证线程私有性:多线程下,保证线程切换后能恢复到正确的执行位置;
        • 虚拟机栈和本地方法栈保证线程私有性:保证线程中的局部变量不被别的线程访问;

          1.2 jdk1.8 内存管理

          1.2.1 jvm 启动流程

          Java 多线程 - 图1

          1.2.2 jvm 运行时的数据区域

          Java 多线程 - 图2

          1.2.3 GC 垃圾回收机制

          1.3 生命周期

          image.png
  • 补充

    • 阻塞:IO操作需要彻底完成后才返回到用户空间;
    • 非阻塞:IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成;

      1.4 通信方式

  • 类型

    • 阻塞:IO操作需要彻底完成后才返回到用户空间;
  • 同步与异步

    • 同步与异步,重点在于消息通知的方式(等待;轮训/被窗口通知);
    • 同步:发送一个请求,等待返回,然后再发送下一个请求;
    • 异步:发送一个请求,不等待返回,随时可以再发送下一个请求;
    • 同步可避免死锁,异步可提高并发执行的效率;

      1.5 调度策略

  • 先来先服务;

  • 【优先顺序】短作业优先、高响应比、优先级;
  • 时间片轮转;
  • 多级反馈队列(集大成);

    1.6 上下文切换

  • 概念:CPU从一个进程或线程切换到另一个进程或线程;

    • 上下文 是指某一时间点 CPU 寄存器和程序计数器的内容;
    • 寄存器是 CPU 内部的数量较少但速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存);
    • 程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统;
  • 步骤:

    • 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处;
    • 恢复一个进程,在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复;
    • 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程;

      2. 多线程

      2.1 概念

  • 多个线程同时运行(多核 CPU)或交替运行(单核 CPU);

    2.2 背景

  • 为什么引入

    • 提高系统整体的并发能力及性能,提高了 CPU 的利用率【多】;
    • 线程间的切换和调度的成本远小于进程【线程】;
  • 存在的问题
    • 内存泄漏、上下文切换、死锁、受限于硬件和软件的资源闲置问题;
  • 并发与并行

    • 并行概念是并发概念的子集,在多任务执行下,并发是针对某一时间段并行是针对某一时刻
    • 都可以是很多个线程,若可同时被多个 cpu 执行则为并行,若只能被一个 cpu 轮流切换执行则为并发;

      2.3 使用

      2.3.1 创建

      2.3.1.1 Thread

  • 使用继承子 Thread 类的子类来创建线程类时,多个线程无法共享线程类的实例变量;

    1. class FirstThread extends Thread{
    2. private int i;
    3. @Override
    4. public void run(){
    5. for (; i < 10; i++){
    6. System.out.println(getName() + ":" + i);
    7. }
    8. }
    9. }
    10. public static void testFirstThread(){
    11. FirstThread thread1 = new FirstThread();
    12. FirstThread thread2 = new FirstThread();
    13. thread1.start();
    14. thread2.start();
    15. }

    2.3.1.2 Runnable

  • 使用 Runnable 接口的方式创建多个线程可以共享线程类的实例变量,此方式创建的 Runnable 对象只是线程的 target,多个线程可以共享一个 target;

    1. class SecondThread implements Runnable{
    2. private int i;
    3. @Override
    4. public void run(){
    5. for (; i < 10; i++){
    6. System.out.println(Thread.currentThread().getName() + ":" + i);
    7. }
    8. }
    9. }
    10. public static void testSecondThread(){
    11. SecondThread thread1 = new SecondThread();
    12. Thread thread11 = new Thread(thread1, "thread--1");
    13. Thread thread12 = new Thread(thread1, "thread--2");
    14. thread11.start();
    15. thread12.start();
    16. }

    2.3.1.3 Callable

  • 相比 Runnable 接口, 线程执行体由 run() 变为 call(),可通过 FutureTask 获取返回值 and 捕获异常;

    1. class ThirdThread implements Callable<String>{
    2. private int i;
    3. @Override
    4. public String call() throws Exception{
    5. for (; i < 10 ; i++){
    6. System.out.println(Thread.currentThread().getName() + ":" + i);
    7. }
    8. return "final result : " + i;
    9. }
    10. }
    11. public static void testThirdThread(){
    12. ThirdThread callable = new ThirdThread();
    13. FutureTask<String> task = new FutureTask(callable);
    14. Thread thread = new Thread(task, "new thread");
    15. Thread thread1 = new Thread(task, "new thread-1");
    16. thread.start();
    17. try {
    18. Thread.sleep(1);
    19. }catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. thread1.start();
    23. try {
    24. System.out.println(task.get());
    25. }catch (Exception e) {
    26. e.printStackTrace();
    27. }
    28. }

    2.3.2 阻塞方法

  • sleep、wait、join、yield 使用

    • 小结:wait 可以实现两个线程整体阻塞,中间切换; join 可以保证子线程结束之后,主线程再执行;yield 可以让渡执行权,然后重新参与竞争当中;
  • 对比

image.png

2.4 线程安全

2.4.1 问题

  • 并发编程
    • 三个问题
      • 原子性问题:一个或多个操作,不被中断地全部执行 or 不执行,eg. 自增操作、银行转账等;
      • 可见性问题:多个线程访问共享变量,一个线程的修改能被其他线程立刻看到;
      • 有序性问题:程序的执行顺序按照代码的先后顺序执行;
    • 解决
      • volatile 关键字
        • 本质:缓存一致性协议,当 CPU 数据时,如果发现操作的变量是共享变量,即在其他 CPU 中存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行无效,则会从内存中重新读取;
        • 作用:1)保证不同线程对共享变量的可见性; 2)禁止进行指令重排(保证有序性);

Java 多线程 - 图5

  1. - 缺陷:只有写操作后才更新内存,若读操作后发生阻塞,则变量更新无效,即**无法保证原子性**;
  2. - 使用要求:1)对变量的写操作不依赖当前值;2)该变量没有包含在具有其他变量的不变式中;
  3. - 在总线加锁;
  • 死锁

    • 概念
      • 多线程在运行过程中,因争夺资源而造成的一种僵局,互相等待;
    • 产生原因
      • 1)资源少,竞争大; 2)进程推进顺序非法; 3)资源分配不当;
    • 产生的四个必要条件:1)互斥; 2)请求与保持; 3)不可剥夺; 4)循环等待;

      2.4.2 java 锁机制

  • 悲观锁 适合写操作多的场景,先加锁可以保证写操作时数据正确;

  • 乐观锁 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升;
  • 自旋锁 适用于同步代码块内容简单,锁被占用时间较短的场景,其优点是减少 CPU 切换和恢复现场导致的消耗,缺点是占用处理器资源,使用时需有等待时间或自旋次数限制;【jdk 1.6 自适应自旋锁】
  • jdk1.6 synchronized ,有四种锁的状态:无锁、偏向锁、轻量级锁、重量级锁
    • 实现机制
      • 偏向锁:锁总是由同一线程多次获得,不存在多线程竞争;
      • 轻量级锁:存在其他线程尝试竞争偏向锁,偏向锁升级为轻量级锁,其他线程通过自旋尝试获得锁;
      • 重量级锁:当自旋超过一定次数,轻量级锁升级成重量级锁。等待锁的线程进入阻塞状态;
      • 锁只能升级,不能降级;
    • 优势:避免线程阻塞和唤醒影响性能;
  • 公平锁与非公平锁
    • 两者区别在于对于最新的等待线程,若锁可以,则非公平锁可能先获取锁,而公平锁必须排队等待;
    • 公平锁优点:等待线程不会饿死;
    • 非公平锁优点:吞吐效率相较高;
  • 可重入锁 同一个线程在外层方法获取锁时,再进入该线程的内层方法会自动获取锁,不会因之前已经获取过还没释放而阻塞,一定程度上避免死锁;
  • 互斥锁 锁一次只能被一个线程持有,有互斥锁的不能再加其他任何锁,读写数据都 ok;
  • 共享锁 锁可被多个线程持有,可再加共享锁,只能读数据;

    2.4.3 Synchronized

  • 作用

    • 保证同一时刻最多只有一个线程执行该段代码, 防止多线程干扰和内存一致性错误;
    • 若一个对象变量对多个线程可见,则对其所有读写均通过同步方法完成;
  • 锁的性质:悲观锁,可重入锁,互斥锁;
  • 分类
    • 对象锁:java 中每个对象均有一个 monitor 对象,即为 java 对象的锁,通常称为“内置锁”或“对象锁”,类对象可有多个,相应地对象锁也有多个,具有独立性,互不干扰;
      • 形式一:在方法内有代码块 synchronized(this|object){ },仅代码块内同步,方法内的其他异步;
      • 形式二:synchronized 修饰非静态方法
    • 类锁:每个类只有一个 class 对象,所以每个类只有一个类锁;
      • 形式一: synchronized(类.class){ };
      • 形式二:;synchronized 修饰静态方法
  • 使用
    • 多个线程若访问同一对象,则对象锁和类锁实现的线程同步效果一致;
    • 多个线程若访问同一类的多个对象,则对象锁实现多线程异步执行,而类锁实现多线程同步执行;
    • 两个线程访问静态同步方法:针对该类的所有对象锁一致,同步有效;
    • 案例
  • 注意

    • synchronized 关键字不能继承;
    • 在定义接口方法时不能使用 synchronized 关键字;
    • 构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块来进行同步;

      2.4.4 Lock

  • 产生背景

    • jdk 1.6 之前,使用 synchronized ,在一个线程锁定过程中若发生阻塞,下一个线程只能被动等待;
    • 多线程读写需求,写操作互斥,读操作并发;
  • 锁的性质:悲观锁,可重入锁
  • lock 与 synchronized 的区别
    | 类别 | synchronized | Lock | | :—- | :—- | :—- | | 存在层次 | Java的关键字,在jvm层面上 | 是一个可实现同步访问的接口类 | | 锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须手动释放锁,不然容易造成线程死锁 | | 锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,可以尝试获得锁,线程可以不用一直等待 | | 锁状态 | 无法判断 | 可以判断 | | 锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) | | 性能 | 少量同步 | 大量同步 |

  • 一般使用

    • lock.lock( )
    • lock.tryLock(long timeout, TimeUnit unit ) 限时,避免死锁;
    • lock.interruptlock()
      --------------------Thread.sleep() 时不会释放 Lock 锁--------------------------
      public static void testtry() {
         Lock lock = new ReentrantLock();
         Thread t = new Thread(new Runnable() {
             @Override
             public void run() {
                 lock.lock();
                 System.out.println("get");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } finally {
                     lock.unlock();
                     System.out.println("release");
                 }}});
         Thread t1 = new Thread(new Runnable() {
             @Override
             public void run() {                
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 while (true) {
                     if (lock.tryLock()) {
                             System.out.println("get success");
                             lock.unlock();
                             break;
                     }else {
                         System.out.println("get faile ... ");
                         try {
                             Thread.sleep(100);
                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }}}}});
         t.start(); t1.start();
      }
      输出结果:
         get
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         get faile ... 
         release
         get success
      

      2.4.4.1 ReentrankLock

  • ReentrantLock 不是一种替代内置加锁的方法,而是作为一种可选择的高级功能;

  • 锁的性质:悲观锁、可重入锁、互斥锁;
  • 有公平锁与非公平锁两种;
  • 线程间协作 lock.condition

      Lock lock = new ReentrantLock();
      Condition cdn_1 = lock.newCondition();
    //await() 和 signal()方法,都必须在 lock.lock() 和 lock.unlock 之间才可以使用;
    //一个 Lock 对象中可以创建多个 Condition 实例,线程对象可以注册在指定的 Condition 中;
    //涉及两个线程,执行 signal 只能唤醒其中一个,使程序无法结束,需使用 signalAll
      public void await_1(){
          lock.lock();
          try {
              System.out.println(Thread.currentThread().getName() + " for await_1 , the starting time: " + System.currentTimeMillis());
              cdn_1.await();
              System.out.println(Thread.currentThread().getName() + " for await_1 , the ending time: " + System.currentTimeMillis());
          } catch (InterruptedException e) {
              e.printStackTrace();
          }finally {
              lock.unlock();
          }
      }
      public void signal_1(){
          lock.lock();
          try {
              System.out.println(Thread.currentThread().getName() + " for signal_1 , the starting time: " + System.currentTimeMillis());
              cdn_1.signal();
              Thread.sleep(3000);
              System.out.println(Thread.currentThread().getName() + " for signal_1 , the ending time: " + System.currentTimeMillis());
          } catch (InterruptedException e) {
              e.printStackTrace();
          }finally {
              lock.unlock();
          }
      }
    
  • lock.condition 与 (synchronized + object.wait、notify or notifyAll)区别

    • 一个线程被唤醒不代表立即获取了对象的 monitor ,只有等调用完 notify() 或 notifyAll() 并退出synchronized 块,释放对象锁后,其余线程才可获得锁执行;
    • notify() 和 notifyAll() 方法只是唤醒等待该对象的 monitor 的线程,并不决定哪个线程能够获取到monitor;

      2.4.4.2 ReentrantReadWriteLock

  • 读锁是共享锁,写锁时互斥锁;

  • ReentrantReadWriteLock 和 ReentrantLock的区别
    • 相同点:均使用了实现 AbstractQueuedSynchronizer 接口的 Sync;
    • 不同点:ReentrantReadWriteLock 使用两个锁(公平锁与非公平锁)分别实现 AQS;
      • ReentrantReadWriteLock.WriteLock 和 ReentrantLock一样,采用独占锁,区别在于 WriteLock 需要同时考虑是否有其他读锁或写锁占用,而ReentrankLock 只需考虑自身是否被占用;
      • ReentrantReadWriteLock.ReadLock 和 Semaphore 一样,采用共享锁,读锁只要没有写锁占用且不超过最大获取数量都可尝试获取;
  • 使用

    • 案例1:实现一个简单缓存

      public static void ReentrantReadWriteLockCacheSystem() {
      Map<String, String> cacheMap = new HashMap<>(4); //将缓存大小设置为4
      ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
      for (int i = 0; i < 20; i++) {
         final String key = String.valueOf(i % 4); 
         Thread thread = new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     readWriteLock.readLock().lock();  //读取缓存时获取读锁 
                     String valueStr = cacheMap.get(key); //获取读锁后通过key获取缓存中的值
                     if (valueStr == null) { //缓存值不存在
                         readWriteLock.readLock().unlock(); //释放读锁后再尝试获取写锁
                         try {
                             readWriteLock.writeLock().lock(); //获取写锁来写入不存在的key值 
                             valueStr = cacheMap.get(key);
                             if (valueStr == null) {
                                 valueStr = key + " --- value";
                                 cacheMap.put(key, valueStr); //写入值
                                 System.out.println(Thread.currentThread().getName() + " --------- put " + valueStr);
                             }
                             readWriteLock.readLock().lock(); //锁降级,避免被其他写线程抢占后再次更新值,保证这一次操作的原子性
                             System.out.println(Thread.currentThread().getName() + " --------- get new " + valueStr);
                         } finally {
                             readWriteLock.writeLock().unlock(); //释放写锁
                         }
                     } else {
                         System.out.println(Thread.currentThread().getName() + " ------ get cache value");
                     }
                 } finally {
                     readWriteLock.readLock().unlock();  //释放读锁
                 }
             }
         }, String.valueOf(i));
         thread.start();
      }
      }
      
    • 案例2:实现一个简单读写锁

    • 实现过程
      • 1 定义一个读写锁共享变量 state;
      • 2 state高16位为读锁数量,低16位为写锁数量。尽量模拟ReentrantReadWriteLock的实现;
      • 3 获取读锁时先判断是否有写锁,有则等待,没有则将读锁数量加1;
      • 4 释放读锁数量减1,通知所有等待线程;
      • 5 获取写锁时需要判断读锁和写锁是否都存在,有则等待,没有则将写锁数量加1;
      • 6 释放写锁数量减1,通知所有等待线程;
  • 参考

    2.4.5 并发工具类

    2.4.6 并发集合(详见 Java 容器)

    2.4.7 CAS 与 AQS 算法

  • CAS(Compare And Swap)

    • 概念:在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步;
    • 局限
      • ABA问题:如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,解决思路就是在变量前面添加版本号,JDK 提供 AtomicStampedReference 类来解决ABA问题,具体操作封装在 compareAndSet() 中;
      • 循环时间长开销大,CAS 操作如果长时间不成功,会导致其一直自旋;
      • 只能保证一个共享变量的原子操作
    • 应用:乐观锁、自旋锁、轻量级锁;
  • AQS (AbstractQueuedSynchronizer)

    • 用于构建锁和同步器的框架,内部定义一个 int state 变量来表示同步状态;
    • 应用:ReentrantLock、ReentrantReadWriteLock 继承 Sync 类,Sync 类继承 AQS;

      2.4.6 案例(自增操作)

  • 实现一:synchronized

           Thread t1 = new Thread(new Runnable() {
              @Override
              public void run(){
                  synchronized (this) {
                      for (int j = 0; j < 1000; j++) {
                          inc++;
                      }
                      System.out.println("the final result : " + inc);
                  }
              }
          });
    
  • 实现二:Lock

           Thread t1 = new Thread(new Runnable() {
              Lock lock = new ReentrantLock();
              @Override
              public void run() {    
                  lock.lock();
                  try {
                      for (int j = 0; j < 1000; j++) {
                          inc++;
                      }
                  } finally {
                      lock.unlock();
                  }
                  System.out.println("the final result : " + inc);
          });
    
  • 实现三:AutomicInteger

           Thread t1 = new Thread(new Runnable() {
              public AtomicInteger auto_inc = new AtomicInteger();
              @Override
              public void run(){
                  for (int j = 0; j < 1000; j++) {
                      System.out.println("the final result : " + auto_inc.getAndIncrement());
                  }
              }
          });
    

    3. 线程池

    4. 案例(生产者消费者模型)

    参考

    Java中的进程与线程(总结篇)
    啃碎并发(三):Java线程上下文切换
    并行与并发
    Java中的进程与线程(总结篇) 线程篇2:[- sleep、wait、notify、join、yield -]
    线程篇3:[-synchronized-]
    java 之 syncronized 详解
    Java并发编程:volatile关键字解析