精华内容
下载资源
问答
  • 并发与多线程

    2019-11-07 23:40:11
    并发与多线程 文章目录并发与多线程基本概念并发与并行进程与线程线程创建线程ThreadRunnable匿名类Callable线程方法线程状态线程同步阻塞同步synchronizedReentrantLock非阻塞同步actomic 类无同步方案可重入代码...

    并发与多线程

    基本概念

    并发与并行

    1. 并发:指两个或多个事件在同一时间间隔内发生 。当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式称之为并发(Concurrent)
    2. 并行:指两个或者多个事件在同一时刻发生 。当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式称之为并行(Parallel)

    进程与线程

    1. 一个程序可能有多个进程,一个进程由多个线程和共享资源组成
    2. 进程:拥有资源的基本单位
    3. 线程:独立调度分派的基本单位

    线程

    创建线程

    Thread

    1. 继承 Thread 类(Thread 实现了 Runnable 接口)

    2. 重写 run 方法

    3. start( ) 方法启动线程

    Runnable

    1. 实现 Runnable 接口
    2. 重写 run 方法
    3. new Thread(Runnable target),new Thread(Runnable target,String name)

    多个 Thread 实例共用一个 Runnable,这些线程的 run 方法相同,可以共享相同的数据

    但是存在线程同步问题

    public class RunnableTest implements Runnable
    {
        private int ticket = 10;
        public void run()
        {
            while (true)
            {
                if (ticket > 0)
                {
                    System.out.println(Thread.currentThread().getName() + "售出" + ticket + "号票");
                    ticket--;
                }
                else System.exit(0);
            }
        }
        public static void main(String[] args)
        {
            RunnableTest rt = new RunnableTest();
            Thread t1 = new Thread(rt, "1号窗口");
            Thread t2 = new Thread(rt, "2号窗口");
            t1.start();
            t2.start();
        }
    }
    

    print

    1号窗口售出10号票
    1号窗口售出9号票
    1号窗口售出8号票
    1号窗口售出7号票
    2号窗口售出7号票
    2号窗口售出5号票
    1号窗口售出6号票
    2号窗口售出4号票
    1号窗口售出3号票
    2号窗口售出2号票
    1号窗口售出1号票
    

    匿名类

    匿名类可以方便的访问方法的局部变量,但是必须声明为 final,因为匿名类和普通局部变量生命周期不一致

    jdk7 中已不再需要显示声明为 final,实际上被虚拟机自动隐式声明了

    public static void main(String[] args)
    {
        new Thread()
        {
            public void run()
            {
                //内容
            }
        }.start();
        new Thread(new Runnable()
        {
            public void run()
            {
                //内容
            }
    	}).start();
    }
    

    Callable

    1. 创建 Callable 的实现类,并冲写 call( ) 方法,该方法为线程执行体,并且该方法有返回值

    2. 创建 Callable 实现类的实例,并用 FutuerTask 类来包装 Callable 对象,该 FutuerTask 封装了 Callable 对象 call( ) 方法的返回值

    3. 实例化 FutuerTask 类,参数为 FutuerTask 接口实现类的对象来启动线程

    4. 通过 FutuerTask 类的对象的 get( ) 方法来获取线程结束后的返回值

      public class CallableTest implements Callable<Integer>
      {
          //重写执行体 call()
          public Integer call() throws Exception
          {
              int i = 0;
              for (; i < 10; i++)
              {
                 //
              }
              return i;
          }
          public static void main(String[] args)
          {
              Callable call = new CallableTest();
              FutureTask<Integer> f = new FutureTask<Integer>(call);
              Thread t = new Thread(f);
              t.start();
              //得到返回值
              try
              {
                  System.out.println("返回值:" + f.get());
              }
              catch (Exception e)
              {
                  e.printStackTrace();
              }
          }
      }
      

      print

      返回值:10
      

    线程方法

    1. 线程执行体:run( )

    2. 启动线程:start( )

    3. Thread 类方法

      方法 描述
      public final void setName(String name) 改变线程名称
      public final void setPriority(int priority) 设置优先级
      public final void setDaemon(boolean on) 设为守护线程,当只剩下守护线程时自动结束
      public final boolean isAlive( ) 测试线程是否处于活动状态
      public static void yield( ) 暂停当前线程(回到就绪状态)
      public static void sleep(long millisec) 进入休眠状态
      public final void join( ) 暂停当前线程,等待调用该方法线程执行完毕
      public final void join(long millisec) 暂停当前线程指定时间
      public static Thread currentThread() 返回对当前正在执行的线程对象的引用

    线程状态

    1. 就绪状态:

      • start( ) 方法进入就绪状态,等待虚拟机调度
      • 运行状态调用 yield 方法会进入就绪状态
      • lock 池中的线程获得锁后进入就绪状态
    2. 运行状态:就绪状态经过线程调度进去运行状态

    3. 阻塞状态:

      • 休眠:调用 sleep 方法
      • 对象 wait 池:调用 wait 或 join 方法,被 notify 后进入 lock 池
      • 对象 lock 池:未获得锁
    4. 死亡状态:run 方法执行完毕

      start方法
      线程调度
      yield方法
      sleep方法
      wait或join方法
      未获得锁
      run方法执行完
      时间到
      notify方法
      获得锁
      新线程
      就绪状态
      运行状态
      阻塞:休眠
      阻塞:wait池
      阻塞:lock池
      死亡状态

    线程同步

    保证程序原子性、可见性、有序性的过程

    阻塞同步

    基于加锁争用的悲观并发策略

    synchronized

    1. synchronized 含义

      • 使用 synchronized 可以锁住某一对象, 当其他线程也想锁住该对象以执行某段代码时,必须等待已经持有锁的线程释放锁

      • 释放锁的方式有互斥代码执行完毕、抛出异常、锁对象调用 wait 方法

    2. 不同的使用方式代表不同的锁粒度

      • 修饰普通方法 = synchronized(this)
      • 修饰静态方法 = synchronized(X.class)
      • 修饰代码块(对象 extends Object)

    ReentrantLock

    1. 创建 Lock 锁

      ReentrantLock 实现了 Lock 接口, Lock lock = new ReentrantLock( )

    2. Lock 含义

      • 使用 lock( ) 方法表示当前线程占有 lock 对象

      • 释放该对象要显示掉用 unlock( ) 方法 ,多在 finally 块中进行释放

    3. trylock 方法

      • synchronized 会一直等待锁,而 Lock 提供了 trylock 方法,在指定时间内试图占用
      • 使用 trylock, 释放锁时要判断,若占用失败,unlock 会抛出异常
    4. Lock 的线程交互

      • 通过 lock 对象得到一个 Condition 对象,Condition condition = lock.newCondition( )

      • 调用这个Condition对象的:await,signal,signalAll 方法

    5. 示例

      public class LockTest
      {
          public static void log(String msg)//日志方法
          {
              SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              Date date = new Date();
              String dateStr = sdf.format(date);
              System.out.println(dateStr + " " + Thread.currentThread().getName() + " " + msg);
          }
          public static void main(String[] args)
          {
              Lock lock = new ReentrantLock();
              new Thread("t1")
              {
                  public void run()
                  {
                      boolean flag = false;
                      try
                      {
                          log("线程已启动");
                          log("尝试占有lock");
                          flag = lock.tryLock(1, TimeUnit.SECONDS);
                          if (flag)
                          {
                              log("成功占有lock");
                              log("执行3秒业务操作");
                              Thread.sleep(3000);
                          }
                          else
                          {
                              log("经过1秒钟尝试,占有lock失败,放弃占有");
                          }
                      }
                      catch (InterruptedException e)
                      {
                          e.printStackTrace();
                      }
                      finally
                      {
                          if (flag)
                          {
                              log("释放lock");
                              lock.unlock();
                          }
                      }
                      log("线程结束");
                  }
              }.start();
              try
              {
                  //先让 t1 先执行两秒
                  Thread.sleep(2000);
              }
              catch (InterruptedException e1)
              {
                  e1.printStackTrace();
              }
              new Thread("t2")
              {
                  public void run()
                  {
                      boolean flag = false;
                      try
                      {
                          log("线程启动");
                          log("尝试占有lock");
      
                          flag = lock.tryLock(1, TimeUnit.SECONDS);
                          if (flag)
                          {
                              log("成功占有lock");
                              log("执行3秒的业务操作");
                              Thread.sleep(3000);
                          }
                          else
                          {
                              log("经过1秒钟的尝试,占有lock失败,放弃占有");
                          }
                      }
                      catch (InterruptedException e)
                      {
                          e.printStackTrace();
                      }
                      finally
                      {
                          if (flag)
                          {
                              log("释放lock");
                              lock.unlock();
                          }
                      }
                      log("线程结束");
                  }
              }.start();
          }
      }
      

      print

      2019-11-07 15:50:01 t1 线程已启动
      2019-11-07 15:50:01 t1 尝试占有lock
      2019-11-07 15:50:01 t1 成功占有lock
      2019-11-07 15:50:01 t1 执行3秒业务操作
      2019-11-07 15:50:03 t2 线程启动
      2019-11-07 15:50:03 t2 尝试占有lock
      2019-11-07 15:50:04 t2 经过1秒钟的尝试,占有lock失败,放弃占有
      2019-11-07 15:50:04 t2 线程结束
      2019-11-07 15:50:04 t1 释放lock
      2019-11-07 15:50:04 t1 线程结束
      
    6. synchronized 和 Lock 区别

      • synchronized 是关键字,Lock 是接口, synchronized是内置的语言实现,Lock是代码层面的实现
      • synchronized 执行完毕自动释放锁,Lock 需要显示 unlock( )
      • synchronized 会一直等待,尝试占用锁,Lock 可以使用 trylock,在一段时间内尝试占用,时间到占用失败则放弃

    非阻塞同步

    非阻塞同步是一种基于冲突检测和数据更新的乐观并发策略

    actomic 类

    1. 原子操作

      • 原子操作是不可中断的操作,必须一次性执行完成
      • 赋值操作是原子操作,但 a++ 不是原子操作, 而是取值、加一、赋值三个步骤
      • 一个线程取 i 的值后,还没来得及加一,第二个线程也来取值,就产生了线程安全问题
    2. actomic 类的使用

      • jdk6 以后,新增包 java.util.concurrent.atomic,里面有各种原子类,比如 AtomicInteger
      • AtomicInteger 提供了各种自增,自减等方法,这些方法都是原子性的。换句话说,自增方法 incrementAndGet 是线程安全的
      • 10000 个线程做 value 加一的操作,用 a++ 方式得出不准确的结果,用原子类 AtomicInteger 的 addAndGet( ) 方法得出正确结果
      public class ThreadTest
      {
          static int value1 = 0;
          static AtomicInteger value2 = new AtomicInteger(0);//原子整型类
          public static void main(String[] args)
          {
              for (int i = 0; i < 100000; i++)
              {
                  new Thread()
                  {
                      public void run()
                      {
                          value1++;
                      }
                  }.start();
                  new Thread()
                  {
                      public void run()
                      {
                          value2.addAndGet(1);//value++的原子操作
                      }
                  }.start();
              }
              while (Thread.activeCount() > 2)
              {
                  Thread.yield();
              }
              System.out.println(value1);
              System.out.println(value2);
          }
      }
      
      

      print

      99996
      100000
      

    无同步方案

    如果一个方法不涉及共享数据,那么他天生就是线程安全的

    可重入代码

    可以在代码执行的任何时刻中断它,转而去执行另外一段代码,在控制权返回之后,原来的程序不会出现任何的错误

    1. 一个方法返回结果是可以预测的,输入了相同的数据,就能返回相同的结果,那这个方法就具有可重入性,也就是线程安全的

    2. 栈封闭是一种可重用代码

      多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量保存在虚拟机栈中,属于线程的私有区域,所以不会出现线程安全性

      public class ThreadTest
      {
          static void add()
          {
              int value = 0;
              for (int i = 0; i < 1000; i++)
              {
                  value++;
              }
              System.out.println(value);
          }
      
          public static void main(String[] args)
          {
              ExecutorService threadPool = Executors.newCachedThreadPool();
              threadPool.execute(() -> add());
              threadPool.execute(() -> add());
              threadPool.shutdown();
          }
      }
      
      

      print

      1000
      1000
      

    线程本地存储

    1. 把共享数据的可见范围限制在同一个线程之内,即便无同步也能做到避免数据争用

    2. 使用 java.lang.ThreadLocal 类来实现线程本地存储功能

      • ThreadLocal 变量是一个不同线程可以拥有不同值的变量,所有的线程可以共享一个ThreadLocal对象
      • 任意一个线程的 ThreadLocal 值发生变化,不会影响其他的线程
      • 用set()和get()方法对ThreadLocal变量进行赋值和查看其值
      public class ThreadLocalDemo
      {
          public static void main(String[] args)
          {
              ThreadLocal threadLocal1 = new ThreadLocal();
              Thread t1 = new Thread(() ->
              {
                  threadLocal1.set(1);
                  try
                  {
                      Thread.sleep(3000);
                  }
                  catch (InterruptedException e)
                  {
                      e.printStackTrace();
                  }
                  System.out.println(threadLocal1.get());
              });
              Thread t2 = new Thread(() -> threadLocal1.set(2));
              t1.start();
              t2.start();
          }
      }
      
      

      print

      1
      
    3. ThreadLocal 原理

      • 每个线程都有一个 ThreadLocal.ThreadLocalMap 对象,调用 threadLocal1.set(T value) 方法时,将 threadLoacl1 和 value 键值对存入 map
      • ThreadLocalMap 底层数据结构可能导致内存泄露,尽可能在使用 ThreadLocal 后调用 remove( )方法

    死锁

    死锁条件

    1. 互斥条件
    2. 请求与保持条件
    3. 不可剥夺条件
    4. 循环等待条件(环路条件)

    Java死锁示例

    public static void main(String[] args)
    {
        Object o1 = new Object();
        Object o2 = new Object();
    
        Thread t1 = new Thread()
        {
            public void run()
            {
                synchronized (o1)//占有 o1
                {
                    System.out.println("t1 已占有 O1");
                    try
                    {
                        Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o1
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    System.out.println("t1 试图占有 o2");
                    System.out.println("t1 等待中");
                    synchronized (o2)
                    {
                        System.out.println("t1 已占有 O2");
                    }
                }
            }
        };
        Thread t2 = new Thread()
        {
            public void run()
            {
                synchronized (o2)  //占有 o2
                {
                    System.out.println("t2 已占有 o2");
                    try
                    {
                        Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o2
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    System.out.println("t2 试图占有 o1");
                    System.out.println("t2 等待中");
                    synchronized (o1)
                    {
                        System.out.println("t2 已占有 O1");
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }
    
    

    print

    t1 已占有 O1
    t2 已占有 o2
    t1 试图占有 o2
    t1 等待中
    t2 试图占有 o1
    t2 等待中
    

    线程通信

    1. Object 类方法

      方法 描述
      wait( ) 线程进入等待池
      notify( ) 唤醒等待当前线程锁的线程
      notifyAll( ) 唤醒所有线程,优先级高的优先唤醒

      为什么这些方法设置在 Object 对象上?

      表面上看,因为任何对象都可以加锁

      底层上说,java 多线程同步的 Object Monitor 机制,每个对象上都设置有类似于集合的数据结构,储存当前获得锁的线程、等待获得锁的线程(lock set)、等待被唤醒的线程(wait set)

    2. 生产者消费者模型

      • sleep 方法,让出 cpu,但不放下锁
      • wait 方法,进入锁对象的等待池,放下锁
    public class ProducerAndConsumer
    {
        public static void main(String[] args)
        {
            Goods goods = new Goods();
            Thread producer = new Thread()//生产者线程
            {
                public void run()
                {
                    while (true) goods.put();
                }
            };
            Thread consumer = new Thread()//消费者线程
            {
                public void run()
                {
                    while (true) goods.take();
                }
            };
            consumer.start();
            producer.start();
        }
    }
    class Goods//商品类
    {
        int num = 0;//商品数目
        int space = 10;//空位总数
        public synchronized void put()
        {
            if (num < space)//有空位可放,可以生产
            {
                num++;
                System.out.println("放入一个商品,现有" + num + "个商品," + (space - num) + "个空位");
                notify();//唤醒等待该锁的线程
            }
            else//无空位可放,等待空位
            {
                try
                {
                    System.out.println("没有空位可放,等待拿出");
                    wait();//进入该锁对象的等待池
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void take()
        {
            if (num > 0)//有商品可拿
            {
                num--;
                System.out.println("拿出一个商品,现有" + num + "个商品," + (space - num) + "个空位");
                notify();//唤醒等待该锁的线程
            }
            else///等待生产产品
            {
                try
                {
                    System.out.println("没有商品可拿,等待放入");
                    wait();//进入该锁对象的等待池
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    print

    没有商品可拿,等待放入
    放入一个商品,现有1个商品,9个空位
    放入一个商品,现有2个商品,8个空位
    拿出一个商品,现有1个商品,9个空位
    放入一个商品,现有2个商品,8个空位
    放入一个商品,现有3个商品,7个空位
    放入一个商品,现有4个商品,6个空位
    拿出一个商品,现有3个商品,7个空位
    放入一个商品,现有4个商品,6个空位
    ···
    

    线程池

    线程的启动和结束都是比较消耗时间和占用资源的,如果在系统中用到了很多的线程,大量的启动和结束动作会严重影响性能

    线程池很像生产者消费者模式,消费的对象是一个一个的能够运行的任务

    1. 设计思路

      • 准备任务容器,可用 List,存放任务
      • 线程池类构造方法中创建多个执行者线程
      • 任务容器为空时,所有线程 wait
      • 当外部线程向任务容器加入任务,就会有执行者线程被 notify
      • 执行任务完毕后,没有接到新任务,就回归等待状态
    2. 实现一个线程池

      public class ThreadPool
      {
          int poolSize;// 线程池大小
          LinkedList<Runnable> tasks = new LinkedList<Runnable>();// 任务容器
          public ThreadPool(int poolSize)
          {
              this.poolSize = poolSize;
              synchronized (tasks)//启动 poolSize 个任务执行者线程
              {
                  for (int i = 0; i < poolSize; i++)
                  {
                      new ExecuteThread("执行者线程 " + i).start();
                  }
              }
          }
          public void add(Runnable r)//添加任务
          {
              synchronized (tasks)
              {
                  tasks.add(r);
                  System.out.println("加入新任务");
                  tasks.notifyAll();// 唤醒等待的任务执行者线程
              }
          }
          class ExecuteThread extends Thread//等待执行任务的线程
          {
              Runnable task;
              public ExecuteThread(String name)
              {
                  super(name);
              }
              public void run()
              {
                  System.out.println("启动:" + this.getName());
                  while (true)
                  {
                      synchronized (tasks)
                      {
                          while (tasks.isEmpty())
                          {
                              try
                              {
                                  tasks.wait();
                              }
                              catch (InterruptedException e)
                              {
                                  e.printStackTrace();
                              }
                          }
                          task = tasks.removeLast();
                          tasks.notifyAll(); // 允许添加任务的线程可以继续添加任务
                      }
                      System.out.println(this.getName() + " 接到任务");
                      task.run();//执行任务
                  }
              }
          }
          public static void main(String[] args)
          {
              ThreadPool pool = new ThreadPool(3);
              for (int i = 0; i < 5; i++)
              {
                  Runnable task = new Runnable()//创建任务
                  {
                      public void run()//任务内容
                      {
                          System.out.println(Thread.currentThread().getName()+" 执行任务");
                      }
                  };
                  pool.add(task);//加入任务
                  try
                  {
                      Thread.sleep(1000);
                  }
                  catch (InterruptedException e)
                  {
                      e.printStackTrace();
                  }
              }
          }
      }
      

      print

      main 加入新任务
      启动:执行者线程 0
      执行者线程 0 接到任务
      执行者线程 0 执行任务
      启动:执行者线程 1
      启动:执行者线程 2
      main 加入新任务
      执行者线程 2 接到任务
      执行者线程 2 执行任务
      main 加入新任务
      执行者线程 2 接到任务
      执行者线程 2 执行任务
      
    3. java 线程池类

      • 默认线程池类 ThreadPoolExecutor 在 java.util.concurrent 包下

        ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        /*
        第一个参数 int 类型, 10 表示这个线程池初始化了 10 个线程在里面工作
        第二个参数 int 类型, 15 表示如果 10 个线程不够用了,就会自动增加到最多 15个 线程
        第三个参数 60 结合第四个参数 TimeUnit.SECONDS,表示经过 60 秒,多出来的线程还没有接到任务,就会回收,最后保持池子里就 10 个
        第五个参数 BlockingQueue 类型,new LinkedBlockingQueue() 用来放任务的集合
        */
        
      • execute( ) 方法添加新任务

        public class TestThread 
        {   
            public static void main(String[] args) throws InterruptedException 
            {
                ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
                threadPool.execute(new Runnable()
                {//添加任务
                    public void run() 
                    {
                        System.out.println("执行任务");
                    }    
                });
            }
        }
        
    4. java 中几种线程池

      java 线程池的顶级接口是 Executor ,子接口是 ExecutorService ,子接口使用更广泛

      Executors 类提供了一系列工厂方法用于创建线程池,返回的线程池实现了 ExecutorService 接口

      • newCachedThreadPool,有缓冲的线程池,线程数 JVM 控制,有线程可使用时不会创建新线程
      • newFixedThreadPool,固定大小的线程池,任务量超过线程数时,任务存入等待队列
      • newScheduledThreadPool,创建一个线程池,可安排在给定延迟后运行命令或者定期地执行
      • newSingleThreadExecutor,只有一个线程,顺序执行多个任务,若意外终止,则会新创建一个
      ExecutorService threadPool = null;
      threadPool = Executors.newCachedThreadPool();//缓冲线程池
      threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
      threadPool = Executors.newScheduledThreadPool(2);//定时任务线程池
      threadPool = Executors.newSingleThreadExecutor();//单线程的线程池
      threadPool = new ThreadPoolExecutor(···);//默认线程池,多个可控参数
      

    线程安全类

    1. StringBuffer:内部方法用 synchronized 修饰
    2. Vetort:继承于 AbstractList
    3. Stack:继承于 Vector
    4. HashTable:继承于 Dictionary,实现了 Map 接口
    5. Property:继承于 HashTable,实现了 Map 接口
    6. concurrentHashMap:分段加锁机制
    展开全文
  • Java并发与多线程

    2020-01-26 15:50:46
    一、并发与多线程简介 1、多线程优点 2、多线程的代价 3、并发编程模型 4、进程与线程 4.1、进程 4.2、线程 4.3、区别 4.4、Java进程和线程的关系 5、产生并发的原因 二、Java 多线程 1、...

    1、多线程优点

    • 资源利用率更好:文件读写操作
    • 程序设计在某些情况下更简单;
    • 程序响应更快:端口监听操作

    2、多线程的代价

    • 设计更复杂:多线程共享数据时尤其需要注意

    • 上下文切换的开销:

      CPU 会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程;上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。

      上下文切换:从任务保存到再加载的过程就是一次上下文切换;上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。如何减少上下文切换:

      • 无锁并发编程:多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
      • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁;
      • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态;
      • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
      • Java的线程机制主要还是基于Native Thread,Go的Coroutine是进程里自己管理的一种"Thread",所以在高并发的场景下,Coroutine可以有效的降低比较重的native的线程上下文切换,从而来提高并发处理能力
    • 增加资源消耗:如线程管理中消耗的资源

    3、并发编程模型

    Java 的并发采用的是共享内存模型

    3.1、并行工作者:传入的作业会被分配到不同的工作者上

    • 并行工作者模型中,委派者(Delegator)将传入的作业分配给不同的工作者。每个工作者完成整个任务。工作者们并行运作在不同的线程上,甚至可能在不同的CPU上
    • 优点:容易理解,只需要添加更多的工作者来提高系统的并行度
    • 缺点:
      • ①、共享状态可能会很复杂:等待访问共享数据结构时,线程之间的互相等待将会丢失部分并行性。许多并发数据结构是阻塞的,意味着在任何一个时间只有一个或者很少的线程能够访问;
      • ②、无状态的工作者:工作者无法在内部保存这个状态(但是每次需要的时候可以重读)称为无状态的共享状态能够被系统中得其他线程修改。所以工作者在每次需要的时候必须重读状态,以确保每次都能访问到最新的副本,不管共享状态是保存在内存中的还是在外部数据库中。
      • ③、任务顺序是不确定的:使得很难在任何特定的时间点推断系统的状态.这也使得它也更难(如果不是不可能的话)保证一个作业在其他作业之前被执行

    3.2、流水线模式(反应器系统,或事件驱动系统):类似于工厂中生产线上的工人们那样组织工作者

    • 每个工作者只负责作业中的部分工作。当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。有时也被成为"无共享并行模型"。

    • 通常使用"非阻塞的IO"来设计使用流水线并发模型的系统:非阻塞IO意味着:一旦某个工作者开始一个IO操作的时候,这个工作者不会一直等待IO操作的结束有了非阻塞IO,就可以使用IO操作确定工作者之间的边界;

    • 流水作业甚至也有可能被转发到超过一个工作者上并发处理:

    • 反应器,事件驱动系统:用流水线并发模型的系统有时候也称为反应器系统或事件驱动系统:

      • ①、系统内的工作者对系统内出现的事件做出反应,这些事件也有可能来自于外部世界或者发自其他工作者;
      • ②、流行的反应器/事件驱动平台:Vert.x,AKKa,Node.JS(JavaScript)
    • Actors 和 Channels:两种比较类似的流水线(或反应器/事件驱动)模型:

      • ①、在Actor模型中每个工作者被称为actor.Actor 之间可以直接异步地发送和处理消息。Actor 可以被用来实现一个或多个像前文描述的那样的作业处理流水线;
      • ②、在Channel模型中,工作者之间不直接进行通信。相反,它们在不同的通道中发布自己的消息(事件)。其他工作者们可以在这些通道上监听消息,发送者无需知道谁在监听
    • 优点:

      • ①、无需共享的状态:工作者之间无需共享状态,意味着实现的时候无需考虑所有因并发访问共享对象而产生的并发性问题;
      • ②、有状态的工作者:可以在内存中保存它们需要操作的数据,只需在最后将更改写回到外部存储系统;
      • ③、较好的硬件整合:单线程代码在整合底层硬件的时候往往具有更好的优势当能确定代码只在单线程模式下执行的时候,通常能够创建更优化的数据结构和算法单线程有状态的工作者能够在内存中缓存数据
      • ④、合理的作业顺序:作业的有序性使得它更容易地推出系统在某个特定时间点的状态
    • 缺点:

      • ①、最大缺点:作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上。这样导致在追踪某个作业到底被什么代码执行时变得困难
      • ②、加大了代码编写的难度,有时会将工作者的代码写成回调处理的形式。若在代码中嵌入过多的回调处理,往往会出现所谓的回调地狱(callback hell)现象

    3.3、函数式并行(Functional Parallelism)

    • 基本思想:是采用函数调用实现程序;
    • 函数式并行里面最难的是确定需要并行的那个函数调用:跨CPU协调函数调用需要一定的开销。某个函数完成的工作单元需要达到某个大小以弥补这个开销。如果函数调用作用非常小,将它并行化可能比单线程、单CPU执行还慢

    3.4、并行与并发

    • 并行:表示两个线程同时做事情,就是同时执行的意思;具有并发的含义,而并发则不一定并行;多个处理器或者是多核的处理器同时处理多个不同的任务
    • 并发:是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生;一个处理器同时处理多个任务。
    • 并发是逻辑上的同时发生(simultaneous);并行、是物理上的同时发生
    • “并发”指的是程序的结构,“并行”指的是程序运行时的状态

    4、进程与线程

    要从调度、并发性、拥有的资源和系统开销等方面;

    • 进程是操作系统分配资源的最小单元;
    • 线程是操作系统(CPU)调度的最小单元

    4.1、进程

    • 进程表示一个逻辑控制流,就是一种计算过程,它造成一个假象,好像这个进程一直在独占CPU资源;
    • 进程拥有一个独立的虚拟内存地址空间,它造成一个假象,好像这个进程一致在独占存储器资源;
    • 操作系统利用fork系统调用来创建一个子进程。fork所创建的子进程会复制父进程的虚拟地址空间;
      • fork刚创建的子进程采用了共享的方式,只用指针指向了父进程的物理资源。当子进程真正要对某些物理资源写操作时,才会真正的复制一块物理资源来供子进程使用;
      • fork不只是复制了页表结构,还复制了父进程的文件描述符表,信号控制表,进程信息,寄存器资源等等;
      • fork创建的子进程开始执行的位置是fork函数返回的位置
    • 进程不能通过直接共享内存的方式来进行进程间通信,只能采用信号、管道等方式来进行进程间通信;
    • 进程上下文切换的资源:
      页表 -- 对应虚拟内存资源
      文件描述符表/打开文件表 -- 对应打开的文件资源
      寄存器 -- 对应运行时数据
      信号控制信息/进程运行信息
      
    • 进程间通信:
      • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
      • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信;
      • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;
      • 文件
      • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信;
      • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;
      • 套接字Socket:套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信
      • 信号(sinal) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

    4.2、线程

    • 线程解决的最大问题就是它可以很简单地表示共享资源的问题,这里说的资源指的是存储器资源,资源最后都会加载到物理内存,一个进程的所有线程都是共享这个进程的同一个虚拟地址空间的,也就是说从线程的角度来说,它们看到的物理资源都是一样的,这样就可以通过共享变量的方式来表示共享资源,也就是直接共享内存的方式解决了线程通信的问题;
    • 在Linux系统中,线程是使用clone系统调用,clone是一个轻量级的fork;它提供了一系列的参数来表示线程可以共享父类的哪些资源;
    • 线程上下文切换需保存的内容:线程的id、寄存器中的值、栈数据

    4.3、协程

    • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。
    • 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快;

    4.4、区别

    • 进程采用fork创建,线程采用clone创建;
    • 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
    • 进程fork创建的子进程的逻辑流位置在fork返回的位置,线程clone创建的KSE的逻辑流位置在clone调用传入的方法位置,比如Java的Thread的run方法位置;
    • 进程拥有独立的虚拟内存地址空间和内核数据结构(页表,打开文件表等),当子进程修改了虚拟页之后,会通过写时拷贝创建真正的物理页。线程共享进程的虚拟地址空间和内核数据结构,共享同样的物理页;
    • 多个进程通信只能采用进程间通信的方式,比如信号,管道,而不能直接采用简单的共享内存方式,原因是每个进程维护独立的虚拟内存空间,所以每个进程的变量采用的虚拟地址是不同的。多个线程通信就很简单,直接采用共享内存的方式,因为不同线程共享一个虚拟内存地址空间,变量寻址采用同一个虚拟内存;
    • 进程上下文切换需要切换页表等重量级资源,线程上下文切换只需要切换寄存器等轻量级数据;
    • 进程的用户栈独享栈空间,线程的用户栈共享虚拟内存中的栈空间,没有进程高效;
    • 一个应用程序可以有多个进程,执行多个程序代码,多个线程只能执行一个程序代码,共享进程的代码段;
    • 进程采用父子结构,线程采用对等结构

    4.4、Java进程和线程的关系

    • 运行一个程序产生一个进程,进程包含至少一个线程;
    • 诶个进程对应一个JVM实例,多个线程共享JVM里的堆;
    • Java采用单线程编程模型,程序会自动创建主线程;
    • 主线程可以创建子线程,原则上要后于子线程完成执行;

    5、产生并发的原因

    CPU、 内存、磁盘速度的差异,CPU缓存 -> 可见性,线程切换 -> 原子性,编译优化 -> 重排序;

    并发的解决方式:

    • 可见性:volatile、synchronized;
    • 原子性:互斥锁
    • 重排序:volatile、禁止重排序

    6、并发编程的三要素:

    • 原子性:指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
    • 可见性:指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
      实现可见性的方法:synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。
    • 有序性:即程序的执行顺序按照代码的先后顺序来执行

    二、Java 多线程

    1、Java线程

    继承自java.lang.Thread或其子类的

    Thread thread = new Thread();

    thread.start();// 调用线程

    2、线程Thread

    2.1、创建线程的方式

    2.1.1、继承Thread子类

    重写run方法:run方法会在调用start()方法之后被执行

    • 方式1:继承类
    class MyThread extends Thread{
    	public void run(){
    		System.out.println("my thread is running");
    	}
    }
    // 可用如下方式创建并运行上述thread的子类:
    MyThread myThread = new MyThread();
    myTread.start();
    • 方式2:创建一个Thread的匿名子类
    Thread thread = new Thread(){
    	public void run(){
    		System.out.println("my thread is running");
    	}
    };
    thread.start();

    2.1.2、实现Runnable接口

    • 新建一个实现了 java.lang.Runnable 接口的类的实例
    class MyRunnable implements Runnable{
    	public void run(){
    		System.out.println("my thread is running");
    	}
    }
    // 为了使线程能够执行run()方法,需要在 Thread 类的构造函数中传入 MyRunnable 的实例对象
    Thread thread = new Thread(new MyRunnable());
    thread.start();
    • 创建一个实现了 Runnable 接口的匿名类:
    Runnable myRunnable = new Runnable(){
    	public void run(){
    		System.out.println("Runnable running");
    	}
    }
    Thread thread = new Thread(myRunnable);
    thread.start();

    2.1.3、实现Callable接口

    实现call()方法,使用FutureTask类来包装Callable对象,FutureTask对象封装了该Callable对象的call()方法的返回值;使用 FutureTask 对象作为 Thread 对象的 target创建并启动新线程。执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果

    FutureTask<Integer> task = new FutureTask<>(new MyCallable());// FutureTask 也有闭锁的功能
    new Thread(task).start();

    其中FutureTask该类实现了接口:RunnableFuture,该接口又继承了两个接口Runnable, Future

    2.1.4、实现多线程的方式

    实现多线程的实质方式只有两种

    只能通过新建Thread类这一种方式来创建线程,但是类里面的run方法有两种方式来实现,第一种是重写run方法,第二种实现Runnable接口的run方法,然后再把该runnable实例传给Thread类。除此之外,从表面上看线程池、定时器等工具类也可以创建线程,但是它们的本质都逃不出刚才所说的范围,也就是实现Runnable接口和继承Thread类。

    2.3、创建 Thread 子类还是实现 Runnable 接口

    • 实现 Runnable 接口,线程池可以有效的管理实现了 Runnable 接口的线程如果多个线程正在运行中,如果某个线程调用 System.exit()指示终结程序,那么全部的线程都会结束执行;
    • Thread类实现了Runnable接口;
    • 如果同时实现Thread和Runnable,那么只会执行Thread里的run方法,因为虽然传入了Runnable对象,但是由于Thread的run方法被覆盖了,所以Runnable里传入的对象不会被执行;
      public static void main(String[] args) {
          new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("Runnable ......");
              }
          }){
              public void run(){
                  System.out.println("Thread ......");
              }
          }.start();
      }
      @Override
      public void run() {
          if (target != null) {
              target.run();
          }
      }

    2.4、start与run方法

    • 启动线程时调用run()方法而非start()方法

    • 一个线程两次调用start方法会出现什么情况:Java的线程是不允许启动两次的,第二次调用会出现 IllegalThreadStateException 异常,这是个运行时异常;

      public synchronized void start() {
          /**
           * This method is not invoked for the main method thread or "system"
           * group threads created/set up by the VM. Any new functionality added
           * to this method in the future may have to also be added to the VM.
           *
           * A zero status value corresponds to state "NEW".
           */
          if (threadStatus != 0)
      		throw new IllegalThreadStateException();
      	....
      }
    • start方法调用的是start0的本地方法,其调用的是JVM_StartThread方法

    • start方法一般会有两个线程,一个是启动线程,另外一个线程本身;start方法调用后,并不一定马上开始运行,只是其状态便成为可运行状态。

    • 如何给run方法传参

      • 构造函数传参
      • 成员变量传参
      • 回调函数传参
    • 如何实现处理线程的返回值

      • 主线程等待法
      • 使用Thread类的join方法阻塞当前线程以等待子线程完成
      • 使用Callable接口实现:通过FutureTask 或者线程池获取;

    2.5、线程名称

    // 当创建一个线程的时候,可以给线程起一个名字
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable, "New Thread");
    thread.start();
    System.out.println(thread.getName());
    // 也可以通过:
    Thread.currentThread()获取当前线程的引用

    2.6、Thread 的部分属性

    • ID:每个线程的独特标识,用于标识不同的线程
    • Name:线程的名称,方便定位问题
    • Priority:线程对象的优先级。优先级别在1-10之间,1是最低级,10是最高级。不建议改变它们的优先级。
    • Status:线程的状态。在Java中,线程只能有这6种中的一种状态:new、runnable、block、waiting、time waiting 和 terminated。
    • isDaemon:是否为守护线程;

    2.7、线程的中断、停止与暂停

    2.7.1、线程中断

    Java 提供中断机制来通知线程表明我们想要结束它。中断机制的特性是线程需要检查是否被中断,而且还可以决定是否响应结束的请求,不能强制停止线程,因为无法判断线程是否是本身运行的;

    2.7.2、线程停止

    • 线程停止的方法:Java并没有提供安全停止线程的方法

      • ①、使用volatile的退出标志,使线程正常退出,也就是当run方法完成后线程终止;该种方法存在的问题:在生产者消费者模式下,无法正确的停止,因为可能使用的是阻塞队列,而阻塞队列在push、take等操作的时候都是阻塞的;
      • ②、使用stop方法强行终止线程,但不推荐使用,因为stop和suspend及resume一样,是过期的方法,使用会造成不可预料的结果
      • ③、使用interrupt 方法中断线程;
    • interrup 终止线程:判断线程是否是停止状态

      • ①、interruputed:测试当前线程是否有已经中断,执行后具有将状态标志清除为 false 的功能如果连续调用两次该方法,第二次调用则返回 false
      • ②、isInterrupteed:测试线程Thread对象是否已经中断,但不清除状态标志;
        public static void main(String[] args) throws Exception{
            Thread thread = new Thread(() -> {
                for (;;){
        
                }
            });
        
            // 启动线程
            thread.start();
            //设置中断标志
            thread.interrupt();
            //获取中断标志
            System.out.println("isInterrupted: " + thread.isInterrupted());// true
            //获取中断标志并重置
            System.out.println("isInterrupted: " + thread.interrupted());// false
            //获取中断标志并重直
            System.out.println("isInterrupted: " + Thread.interrupted());// false
            //获取中断标志
            System.out.println("isInterrupted: " + thread.isInterrupted());// true
            thread.join();
            System.out.println("Main thread is over.");
        }
    • stop()方法-过期方法

      • 暴力停止线程的方法;
      • 调用 stop 方法时会抛出 java.lang.ThreadDeath 异常,通常情况下,该异常不需要显示捕获;
      • 强制停止线程,可能对锁定的对象进行了"解锁",导致数据得不到同步处理,出现数据不一致的情况
    • 线程停止在代码中增加异常处理

    • 使用 return 停止线程:将方法 interrupt() 和 return 结合使用

    2.7.3、暂停线程:可以使用 suspend()方法暂停线程,使用resume() 方法恢复线程的执行

    这两个方法的缺点:

    • 使用这两个方法时,如果使用不当极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象
    • 数据的不同步:容易出现因为线程的暂停而导致数据不同步的情况

    2.7.4、线程中断原理

    • Java线程中断机制是一种协作机制,也就是说通过中断并不能直接停止另一个线程,需要被中断的线程自己处理中断.

    • 中断模型:
      每个线程对象里都有一个boolean类型的标识,代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身),Thread提供了几个方法来操作中断状态:

      • ①、public static boolean interrupted():测试当前线程是否中断,线程的中断状态由该方法清除。如果连续两次调用该方法,则第二次调用将返回 false;注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象;
      • ②、public boolean isInterrupted():测试线程是否已经中断.线程的中断状态不受该方法的影响;
      • ③、public void interrupt():中断线程,唯一能将中断状态设置为true的方法,调用的是本地方法:private native void interrupt0();
      // 调用上述三个方法时不同的返回值,尤其注意 interrupted()静态方法
      public static void main(String[] args) throws Exception{
          Thread thread = new Thread(() -> {
              for (;;){
      
              }
          });
          // 启动线程
          thread.start();
          //设置中断标志
          thread.interrupt();
          //获取中断标志
          System.out.println("isInterrupted: " + thread.isInterrupted());// true
          //获取中断标志并重置,虽然使用的是 thread 来调用的,真正中断的线程是当前线程,也就是main方法
          System.out.println("isInterrupted: " + thread.interrupted());// false
          //获取中断标志并重直
          System.out.println("isInterrupted: " + Thread.interrupted());// false
          //获取中断标志
          System.out.println("isInterrupted: " + thread.isInterrupted());// true
          thread.join();
          System.out.println("Main thread is over.");
      }
    • 中断处理:被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理

      • ①、中断状态管理:一般来说当可能阻塞的方法声明中有抛出InterruptedException则暗示该方法是可中断的;如果程序捕获到这些可中断的阻塞方法抛出的InterruptedException或检测到中断后,可以按照如下原则处理:

        • 如果遇到的是可中断的阻塞方法抛出InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断,则可清除中断状态并抛出InterruptedException,使当前方法也成为一个可中断的方法
        • 若有时候不太方便在方法上抛出InterruptedException,比如要实现的某个接口中的方法签名上没有throws InterruptedException,这时就可以捕获可中断方法的InterruptedException并通过Thread.currentThread.interrupt()来重新设置中断状态。如果是检测并清除了中断状态,亦是如此。
      • ②、中断响应:根据实际情况而定,有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务;如做一些事务回滚操作,一些清理工作,一些补偿操作

        响应中断的方法:wait、sleep、join等

    • 中断的使用:

      • 点击某个桌面应用中的取消按钮时;
      • 某个操作超过了一定的执行时间限制需要中止时;
      • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
      • 一组线程中的一个或多个出现错误导致整组都无法继续时;
      • 当一个应用或服务需要停止时

    2.7.5、线程使用interrupt优雅退出例子

    • 直接中断,没有可响应的
      // 有线程调用了interrupt方法,那么对应线程需要调用 Thread.currentThread().isInterrupted() 一直检测是否有被中断;
      public static void main(String[] args) throws InterruptedException {
          Runnable run = () -> {
              int num = 0;
              // 调用 Thread.currentThread().isInterrupted() 一直检测是否有被中断;
              while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
                  if (num % 10000 == 0) {
                      System.out.println(num + " 是10000的倍数");
                  }
                  num++;
              }
          };
          Thread t = new Thread(run);
          t.start();
          Thread.sleep(2000);
          // 通知线程中断
          t.interrupt();
      }
    • 带有sleep中断线程
      public static void main(String[] args) throws InterruptedException {
          Runnable run = () -> {
              int num = 0;
              try {
                  while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                      if (num % 100 == 0) {
                          System.out.println(num + " 是100的倍数");
                      }
                      num++;
                  }
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          };
          Thread t = new Thread(run);
          t.start();
          Thread.sleep(500);
          t.interrupt();
      }
    • 在循环迭代中有响应中断的方法,比如sleep:
      public static void main(String[] args) throws InterruptedException {
          Runnable run = () -> {
              int num = 0;
              // 需要在循环中去检测异常
              while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                  if (num % 100 == 0) {
                      System.out.println(num + " 是100的倍数");
                  }
                  num++;
                  try {
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      // 捕获异常之后,不能吞掉异常,需要再次恢复中断
                      Thread.currentThread().interrupt();
                  }
              }
      
          };
          Thread t = new Thread(run);
          t.start();
          Thread.sleep(5000);
          t.interrupt();
      }

    2.8、线程的睡眠

    • Thread.sleep()
    • TimeUnit.SECONDS.sleep():传参有问题其不会抛出异常

    当一个执行中的线程调用了Thread的sleep方法会,调用线程会暂时让出指定时间的执行权利,但是该线程所有持有的锁不释放;在指定的时间会正常返回,线程处于就绪装填;

    如果在睡眠期间其他线程调用了该线程的interrupt方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常

    2.9、等待线程的终结

    Thread.join()	当前线程调用某个线程的这个方法时,它会暂停当前线程,直到被调用线程执行完成
    Thread.join(long miseconds)	这方法让调用线程等待特定的毫秒数。
    Thread.join(long milliseconds, long nanos)	第二个版本的join方法和第一个很像,只不过它接收一个毫秒数和一个纳秒数
    

    2.10、线程异常处理

    当一个非检查异常被抛出,默认的行为是在控制台写下stack trace并退出程序

    • 必须实现一个类来处理非检查异常,这个类必须实现 UncaughtExceptionHandler 接口并实现在接口内已声明的 uncaughtException() 方法:
    public class ExceptionHandler implements UncaughtExceptionHandler{
    	public void uncaughtException(Thread t, Throwable e){
    		System.out.printf("An exception has been captured\n");
    		System.out.printf("Thread: %s\n",t.getId());
    		System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
    		System.out.printf("Stack Trace: \n");
    		e.printStackTrace(System.out);
    		System.out.printf("Thread status: %s\n",t.getState());
    	}
    }
    Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
    • 当在一个线程里抛出一个异常,但是这个异常没有被捕获(这肯定是非检查异常):
      JVM 检查线程的相关方法是否有设置一个未捕捉异常的处理者。如果有,JVM 使用 Thread 对象和 Exception 作为参数调用此方法;如果没有定义,那么JVM会寻找默认非捕捉异常handle如果线程没有捕捉未捕获异常的处理者,那么JVM会把异常的stacktrace写入操控台并结束任务;
    • 主线程能够轻易发现异常,但是子线程却不行;
    • 异常处理器调用策略:
    • 使用异常处理器:
      Thread.setDefaultUncaughtExceptionHandler(new MyExceptionHandler());
      static class MyExceptionHandler implements Thread.UncaughtExceptionHandler {
          @Override
          public void uncaughtException(Thread t, Throwable e) {
              // do something
          }
      }

    2.11、本地线程变量

    ThreadLocal

    本地线程变量为每个使用这些变量的线程储存属性值。可以用 get() 方法读取值和使用 set() 方法改变值

    2.12、线程组可以把线程分组

    Java 提供 ThreadGroup 类来组织线程。ThreadGroup 对象可以由 Thread 对象组成和由另外的 ThreadGroup 对象组成,生成线程树结构,方便线程管理

    当线程运行过程中出现异常情况时,在某些情况下JVM会把线程的控制权交到线程关联的线程组对象上来进行处理

    不推荐使用线程组

    • 线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了;
    • 线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值

    2.13、用线程工厂创建线程

    线程对象工厂 ThreadFactory

    2.14、join()和yield()方法

    2.14.1、join()方法

    • 等待该线程终止,指的是主线程等待子线程的终止,子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行;让调用该方法的thread完成run方法的里面的东西后,在执行 join 方法后面的代码当前运行着的线程将阻塞直到这个线程实例完成了执行;join方法调用后,线程的状态是:WAITING

    • join 具有使线程排队运行的作用:

      join与synchronize 区别:join 内部是wait方法进行等待,而synchronized关键字使用的是"对象监视器"原理作为同步;

    • 在执行 join 的过程中,如果当前线程对象被中断,则当前线程出现异常;如果主线程被中断了,那么其需要将中断状态传递给子线程;

    • join(long) 内部是使用 wait(long)方法来实现的,所以join(long)方法具有释放锁的特点,而 sleep(long)不释放锁的;

    • 使用join方法之后,线程顺序执行;

    • join是代码实现

      public final synchronized void join(long millis) throws InterruptedException {
          long base = System.currentTimeMillis();
          long now = 0;
      
          if (millis < 0) {
              throw new IllegalArgumentException("timeout value is negative");
          }
      
          if (millis == 0) {
              while (isAlive()) {
                  wait(0);
              }
          } else {
              while (isAlive()) {
                  long delay = millis - now;
                  if (delay <= 0) {
                      break;
                  }
                  wait(delay);
                  now = System.currentTimeMillis() - base;
              }
          }
      }
    • join中有wait操作,那么其是如何被唤醒的:需要深入到native方法中

    示例:

    public class ThreadJoin {
        public static void main(String[] args) throws Exception{
            JoinThread yield = new JoinThread();
            Thread t1 = new Thread(yield, "t1");
            Thread t2 = new Thread(yield, "t2");
            Thread t3 = new Thread(yield, "t3");
            t1.start();
    		t1.join();
            t2.start();
    		t2.join();
            t3.start();
        }
        static class JoinThread implements Runnable {
            @Override
            public void run() {
                System.out.println("current thread name:" + Thread.currentThread().getName());
            }
        }
    }
    // 输出结果
    current thread name:t1
    current thread name:t2
    current thread name:t3

    2.14.2、yield()方法

    使当前线程从执行状态(运行状态)变为可执行状态(就绪状态),调用yield的时候锁并没有被释放,放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间,放弃的时间不确定,将CPU让给其他资源导致速度变慢,一般是把机会给到线程池拥有相同优先级的线程,其状态是:RUNNABLE

    使用yield之后,线程交替执行

    示例:

    public class ThreadYield {
        public static void main(String[] args) throws Exception{
            YieldThread yieldThread = new YieldThread();
            Thread t1 = new Thread(yieldThread, "FirstThread");
            Thread t2 = new Thread(yieldThread, "SecondThread");
            t1.start();
            t2.start();
        }
        static class YieldThread implements Runnable {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                    Thread.yield();
                }
            }
        }
    }
    // 输出结果
    FirstThread: 0
    SecondThread: 0
    FirstThread: 1
    SecondThread: 1
    FirstThread: 2
    SecondThread: 2
    FirstThread: 3
    SecondThread: 3
    FirstThread: 4
    SecondThread: 4

    2.14.3、两者的区别

    • join 是 final 的实例方法,yield是原生静态方法;
    • yield 只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,只能使同优先级或更高优先级的线程有执行的机会。
    • join 等待该线程终止,等待调用join方法的线程结束,再继续执行

    2.15、线程的优先级

    • 线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的;
    • 优先级具有规则性:线程的优先级与代码执行顺序无关,CPU 尽量将执行资源让给优先级比较高的线程;高优先级的线程总是大部分先执行完的,但不代表高优先级的线程全部不执行完;
    • 优先级具有随机性:也就是优先级较高的线程不一定每次都先执行完;不要把线程的优先级与运行结果的顺序作为衡量的标准,线程优先级与打印顺序无关;
    • 优先级不应该去使用:因为不同的系统对优先级的支持不一样,可能有的系统会无视优先级;

    2.16、守护线程

    守护线程优先级非常低,通常在程序里没有其他线程运行时才会执行;当守护线程是唯一在运行的线程时,JVM 会结束守护线程并终止程序;

    • 守护线程通常用于在同一程序里给普通线程(也叫使用者线程)提供服务,它们通常无限循环的等待服务请求或执行线程任务,JAVA 中最典型的这种类型代表就是垃圾回收器
    public class Daemon extends Thread{
    	public Daemon(){
    		setDaemon(true);//在这个构造函数,用setDaemon() 方法让此线程成为守护线程
    	}
    }
    • 只能在start() 方法之前可以调用 setDaemon() 方法。一旦线程运行了,就不能修改守护状态,否则会抛出一个IllegalThreadStateException异常。可以使用isDaemon()方法来检查线程是否是守护线程(方法返回 true)或者是使用者线程(方法返回 false);在Daemon中产生的新线程也是Daemon的;
    • 典型的守护线程是垃圾回收线程,当进程中没有非守护线程时,则垃圾回收线程也就没有存在的必要了守护线程的作用是为其他线程的运行提供便利服务,最典型的应用:GC,当只有守护线程运行时,JVM会自动退出;
    • 守护线程创建出来的线程也是守护线程;

    2.17、线程的生命周期

    1、通用的线程生命周期

    通用的线程生命周期可以用五态模型来描述:初始状态、可运行状态、运行状态、休眠状态、终止状态

    • 初始状态:指的是线程已经被创建,但是不允许分配CPU执行;这是编程语言层面的,在操作系统层面,真正的线程还没有创建;
    • 可运行状态:指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被成功创建,可以分配CPU执行;
    • 当有空闲CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态转换成了运行状态;
    • 运行状态的线程如果调用了一个阻塞API或者等待某个事件,那么线程就会转换到休眠状态,同时释放CPU使用权

    2、Java线程的生命周期

    Java语言中线程共有6种状态,其中BLOCKED、WAITING、TIME_WAITING 是一种状态即休眠状态,一般称为阻塞状态,分别是:

    • NEW(初始化状态)
    • RUNNABLE(可运行/运行状态):对应系统中的可运行、运行中;
    • BLOCKED(阻塞状态)
    • WAITING(无时限等待)
    • TIME_WAITING(有时限等待)
    • TERMINATED(终止状态)

    3、状态转换过程

    • (1)RUNNABLE 与 BLOCKED 状态转换

      只有一种常见会触发这种转换,就是现场等待synchronized的隐式锁。synchronized修饰的代码同一时刻只允许一个线程执行,其他线程只能等待,在这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED状态,而当等待的线程获得 synchronized 隐式锁时,又会从BLOCKED转换为RUNNABLE状态;

    • (2)RUNNABLE 与 WAITING 状态转换

      有三种场景会触发这种隐式转换:

      • 获得 synchronized 隐式锁的线程,调用无参的 Object.wait方法;
      • 调用无参数的Thread.join方法。等待的线程状态会从 RUNNABLE 转换到 WAITING。当线程执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE;
      • 调用 LockSupport.park() 方法。调用该方法,线程状态会从 RUNNABLR 转换到 WAITING。调用LockSupport.unpark(Thread thread)可以唤醒目标线程,目标线程的状态又会从 WAITING 转换到 RUNNABLE
    • (3)RUNNABLE 与 TIME_WAITING 状态转换

      有五种场景会触发这种转法:

      • 调用带超时参数的 Thread.sleep(long millis)方法;
      • 获得 synchronized 隐式锁的线程,调用到超时的 Object.waiting(long timeout)方法;
      • 调用带超时参数的 Thread.join(long miliis)方法;
      • 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline)方法;
      • 调用带超时参数的 LockSupport.parkUntil(long deadline)方法;

      TIME_WAITING 和 WAITING状态的区别仅仅是触发条件多了超时参数;

    • (4)从 NEW 到 RUNNABLE 状态

      Java刚创建出来的Thread对象就是NEW状态。NEW状态的线程不会被操作系统调度,因此不会被执行。从NEW状态转换到RUNNABLE状态,只需要调用线程对象的start()方法即可;

    • (5)从 RUNNABLE 与 TERMINATED 状态

      线程执行完了或者因异常退出了run()方法,该线程结束生命周期

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OKfR6S37-1580025046155)(image/thread-status.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0h9oWJvy-1580025046156)(image/线程生命周期.png)]

    2.18、线程管理器MXBean

    • (1)获取当前线程数方式

      • 使用线程管理器MXBean
      • 直接通过线程组的activeCount,这种需要不断向上寻找父线程数组,否则只能获取当前线程组

      如获取一个程序中开启了多少个线程:

      public static void main(String[] args) throws Exception {
          System.out.println("Hello World");
          ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
          ThreadInfo[] threadInfo = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds());
          for (ThreadInfo info : threadInfo) {
              System.out.println(info.getThreadName());
          }
      }
    • (2)线程管理器MXBean

    3、竞态条件与临界区

    • 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源:如果多个线程对这些相同资源进行了"写操作"才会引发线程安全问题;
    • 竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件
    • 临界区:致竞态条件发生的代码区称作临界区

    4、线程安全与共享资源

    允许被多个线程同时执行的代码称作线程安全的代码,线程安全的代码不包含竞态条件

    4.1、局部变量

    存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享,基础类型的局部变量是线程安全的

    4.2、局部的对象引用

    • 尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中
    • 如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的
    • 实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的:
      public void someMethod(){			  
      	LocalObject localObject = new LocalObject();
      	localObject.callMethod();
      	method2(localObject);
      }
      public void method2(LocalObject localObject){
      	localObject.setValue("value");
      }

    4.3、对象成员:(实例变量)

    • 对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的;
    • 案例:
      public class NotThreadSafe{
      	StringBuilder builder = new StringBuilder();			    
      	public add(String text){
      		this.builder.append(text);
      	}
      }
      // 如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题
      NotThreadSafe sharedInstance = new NotThreadSafe();
      new Thread(new MyRunnable(sharedInstance)).start(); // 两个MyRunnable共享了同一个NotThreadSafe对象
      new Thread(new MyRunnable(sharedInstance)).start();
      public class MyRunnable implements Runnable{
      	NotThreadSafe instance = null;			  
      	public MyRunnable(NotThreadSafe instance){
      	this.instance = instance;
      	}
      	public void run(){
      	this.instance.add("some text");
      	}
      }
      new Thread(new MyRunnable(new NotThreadSafe())).start();
      new Thread(new MyRunnable(new NotThreadSafe())).start();

    现在两个线程都有自己单独的NotThreadSafe对象,调用add()方法时就会互不干扰,再也不会有竞态条件问题了

    4.4、线程控制逃逸规则

    可以帮助你判断代码中对某些资源的访问是否是线程安全的**

    • 规则:如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的.
    • 即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接)整个应用也许就不再是线程安全的了;

    5、synchronized 关键字

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jSmbFr5g-1580025046156)(image/synchronized.png)]

    synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

    进入synchronized 块内使用到的变量从线程的工作内存中清除,主要在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取;退出synchronized块是在synchronized块内对共享变量的修改刷新到主内存;

    5.1、synchronized 方法与锁对象

    • 只有共享资源的读写访问才需要同步化实现;
    • A线程先持有object对象的Lock锁,B线程可以以异步化的方式调用object对象中的非synchronized类型的方法;
    • A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,即同步;
    • 普通同步方法,锁是当前实例对象;
    • 静态同步方法,锁是当前类的class对象;
    • 同步方法块,锁是括号里面的对象

    5.2、脏读

    读取实例变量时,此值已经被其他线程修改过的,脏读可以通过 synchronized 关键字解决

    • 当 A 线程调用 anyObject 对synchronized象加入 synchronized 关键字的 X 方法时, A 线程就获得了 X 方法锁,准确的来说是获得了对象的锁,所以其他线程必须等待 A 线程执行完毕后才可以调用 X 方法,但 B 线程可以随意调用其他非 synchronized 的方法;

    • 当 A 线程调用 anyObject 对象中对象加入 synchronized 关键字的 X 方法时, A 线程就获得了 X 方法 所在对象的锁,所以其他线程必须等待 A 线程执行完毕后才可以调用 X 方法,而 B 线程如果调用声明了synchronized 关键字的非 X 方法时,必须等待 A 线程将 X 方法执行完毕,也就释放对象锁后才可以调用

    5.3、synchronized 锁重入

    synchronized 关键字拥有锁重入功能,也就是在使用 synchronized 时,当一个线程得到一个对象的锁之后,再次请求此对象锁时可以再次得到该对象的锁。在一个 synchronized 方法/块内部调用本类的其他 synchronized 方法/块时,是永远可以得到锁的;

    可重入锁:自己可以再次获得自己的内部锁,也支持父子类继承的环境子类可以通过可重入锁调用父类的同步方法

    5.4、出现异常时,锁自动释放

    线程a调用 synchronized 方法时出现异常,会自动释放锁,线程b进入方法正常执行;

    5.5、同步不具有继承性

    当子类重写父类的同步方法时,如果子类的重写方法不加入同步标志的化,一样不具备同不性

    5.6、静态方法同步

    静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法

    5.7、synchronized 与 Lock 的区别

    • 区别:

      • synchronized 是Java的一个关键字,其是在JVM层面上实现的,如果线程执行时发生异常,JVM 会自动释放锁。因此不会导致死锁现象发生;Lock 是接口,通过代码实现的,在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock时需要在finally块中释放锁;
      • Lock 可以让等待锁的线程响应中断,而 synchronized 使用时等待的线程会一直等待下去,不能响应中断;
      • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 不行;
      • 在资源竞争不是很激烈的情况下, synchronized 的性能要优于 Lock,但是在资源竞争很激烈的情况下,synchronized性能会下降几十倍,但是 Lock 是保持常态的;
      • 在 JDK1.5 之后 synchronized 作了很多优化,在性能上已经有很大提升. 如:自旋锁、锁消除、锁粗化、轻量级锁、偏向锁
      • synchronized 和 ReentrantLock 都是可重入锁;
      • 公平锁:即尽量以请求锁的顺序来获取锁,synchronized 是非公平锁,无法保证等待的线程获取锁的顺序;ReentrantLock和ReentrantReadWriteLock,默认情况下是非公平锁,但是可以设置为 公平锁;
      • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
      • 机制:synchronized是操作Mark Word,Lock是调用Unsafe类的park方法。
    • Lock 适用场景:

      • 某个线程在等待一个锁的控制权的这段时间需要中断;
      • 需要分开处理一些wait-notify,ReentrantLock 里面的 Condition应用,能够控制notify哪个线程,锁可以绑定多个条件
      • 具有公平锁功能,每个到来的线程都将排队等候

    为什么有synchronized还会有Lock?

    • synchronized无法响应中断;支持中断的API:void lockInterruptibly() throws InterruptedException;
    • synchronized不支持超时;支持超时的API:boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    • synchronized 不支持非阻塞获取锁;非阻塞获取的API:boolean tryLock();

    5.8、原子性、可见性、有序性

    • 原子性:在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit,这两个字节码指令,在Java中对应的关键字就是synchronized,通过monitorentermonitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到;

    • 可见性:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值

    • 有序性:synchronized是无法禁止指令重排和处理器优化的,那synchronized如何保证有序性?

      Java中填入有序性:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

      • as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。也就是说- as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行。

      由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性

    6、synchronized 同步块

    synchronized 声明方法在某些情况下是有弊端的

    6.1、实例方法中的同步块

    不需要同步整个方法,而是同步方法中的一部分,在非同步的Java方法中的同步块的例子如下所示:

    public  void add(int value){
    	synchronized(this){
    		this.value = value;
    	}
    }

    注意Java同步块构造器用括号将对象括起来,在同步构造器中用括号括起来的对象叫做监视器对象

    静态方法中的同步块:这些方法同步在该方法所属的类对象上

    public class MyClass {
    	public static synchronized void log1(String msg1, String msg2){
    		log.writeln(msg1);
    		log.writeln(msg2);
    	}
    	public static void log2(String msg1, String msg2){
    		synchronized(MyClass.class){
    			log.writeln(msg1);
    			log.writeln(msg2);
    		}
    	}
    }

    同步代码块中,不在 synchronized 块中的是异步执行的,在 synchronized 块中就是同步执行的

    在使用synchronized(this)代码块时(即访问的是实例方法)需要注意:

    当一个线程访问object的一个synchronized(this)代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,说明synchronized使用的“对象监视器”是一个;和synchronized方法一样,synchronized(this) 代码块也是锁定当前对象的

    6.2、synchronized同步方法或同步代码块两种作用

    多个线程调用同一个对象中的不同名称的synchronized同步方法或同步代码块时,调用的效果就是按顺序执行,也是同步的、阻塞的;说明synchronized同步方法或同步代码块两种作用:

    • 对其他 synchronized 同步方法或同步代码块调用呈阻塞状态;

    • 同一个时间只要一个线程看执行 synchronized 同步方法或同步代码块中的代码

      • Java 支持任意对象作为对象监视器来实现同步的功能**

        锁定非 this 对象具有:如果一个类中有很多个 synchronized 方法,虽然实现了同步,但会收到阻塞,影响运行效率;但是如果使用同步代码块锁非this对象,则代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,提高运行效率;

      • 同步代码块放在非同步方法中声明,并不能保证调用方法的线程的执行是同步/顺序性的,也就是线程调用方法的顺序是无序的,
        虽然在同步块中执行的顺序是同步的,这样容易出现"脏读"问题**

    6.3、结论

    synchronized(非this对象的x) 是将x对象本身作为"对象监视器",这样得到以下3个结论:

    • 当多个线程同时执行 synchronized(x){}同步代码块时是同步的;
    • 当其他线程执行x对象中 synchronized 同步方法时是同步的;
    • 当其他线程执行x对象方法里面 synchronized(this)代码块是也呈现同步效果;

    但是需要注意的是:如果其他线程调用不加 synchronized 关键字的方法时,还是异步调用;

    6.4、静态同步synchronized方法与synchronized(Class.class)代码块

    • 如果 static 加在 synchronized 同步方法上,那么对当前的*.java文件对应的 Class 类进行加锁。将 synchronized(string) 同步块与 String 联合使用时,需要注意字符串常量池带来的一些意外情况;
    • 如果是 synchronized(Class.class)代码块,则效果是是一样的,也是对整个类加锁;

    7、synchronized底层实现及锁优化

    • 可以通过反编译字节码 --> javap -c SyncDemo.class 查看底层实现
    • synchronized 的优化借鉴了锁的CAS操作

    7.1、同步代码块的实现

    同步代码块是使用 monitorenter 和 monitorexit 指令来实现的。有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁

    7.1.1、monitorenter

    每个对象都有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权

    • 如果 monitor 的进入数为 0,则该线程进入monitor,然后将进入数设置为 1,该线程为 monitor的所有者。
    • 如果线程已经占用该monitor,只是重新进入,则monitor的进入数加 1;
    • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再尝试重新获取 monitor的所有权。

    Synchronized是对对象进行加锁,在Jvm中,对象在内存中分为三块区域:对象头、实例数据和对齐填充。在对象头中保存了锁标志位和指向Monitor对象的起始地址。当Monitor被某个线程持有后,就会处于锁定状态,Owner部分会指向持有Monitor对象的线程。另外Monitor中还有两个队列,用来存放进入及等待获取锁的线程

    7.1.2、monitorexit

    执行该指令的线程必须是objectref所对应的monitor的持有者,指令执行时,monitor的进入数减1,如果减1后为0,那么线程退出monitor,不再持有monitor。

    可重入锁每次退出都是 monitor 减1,当 monitor为0的时间,锁被完全释放;

    synchronized 代码块的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

    7.2、同步方法的实现

    • (1)基本实现:

      方法的同步并没有通过指令monitorenter和monitorexit来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM 就是根据该标示符来实现方法的同步:当方法调用时会检查方法的 ACC_SYNCHRONIZED 访问标示是否被设置,如果设置了,执行线程将先获取monitor,获取成功后,执行方法体。方法值完释放monitor,在方法执行期间,其他线程无法再获得同一个monitor对象。锁是放在对象头中的;

      值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

      加锁的本质就是在锁对象的对象头中写入当前线程ID

    • (2)同步方法与同步代码块的实现:

      无论是 ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,都是基于Java虚拟机(HotSpot)中,Monitor是基于C++实现的,有ObjectMonitor实现,该类中有enter、exit、wait、notify、notifyAll等。sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法,这是在JDK6之前synchronized的实现会直接调用ObjectMonitor,这种锁被称之为重量级锁。

    7.3、重量级锁

    synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖底层操作系统的Mutex Lock来实现的,而操作系统实现线程间的切换成本非常高,状态之间的转换需要相对较长的时间。依赖于底层操作系统的 Mutex Lock 所实现的锁我们称之为"重量级锁",主要是涉及到用户态到内核态的切换

    Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵

    7.4、轻量级锁

    7.4.1、锁的状态

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tYWtJI3F-1580025046157)(image/锁状态流程转换.png)]

    锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁

    随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(锁的升级是单向的,也就是说只能从低到高,不会出现锁的降级)。JDK6 中是默认开启偏向锁和轻量级锁的,也可以通过-XX:UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中。轻量级锁是相对于使用操作系统互斥量来实现的传统锁而言的。另外轻量级锁并不是用来替代重量级锁的。轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,会导致轻量级锁膨胀为重量级锁.

    • 注意:锁降级实际上是会发生的,当JVM进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。

      • 降级目的和过程:JVM会尝试在SWT的停顿中对处于“空闲(idle)”状态的重量级锁进行降级(deflate),降级过程:在STW时,所有的Java线程都会暂停在“安全点(SafePoint)”,此时VMThread通过对所有Monitor的遍历,或者通过对所有依赖于MonitorInUseLists值的当前正在“使用”中的Monitor子序列进行遍历,从而得到哪些未被使用的“Monitor”作为降级对象;

      • 可降级的Monitor对象:重量级锁的降级发生于STW阶段,降级对象就是那些仅仅能被VMThread访问而没有其他JavaThread访问的Monitor对象;

      • 但是:但是锁升降级效率较低,如果频繁升降级的话对JVM性能会造成影响

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NnT28pqN-1580025046157)(image/轻量级锁获取与释放过程.png)]

    7.4.2、轻量级锁的加锁过程

    • (1)在代码块进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为"01"状态,是否偏向锁为"0")虚拟机首先将在当前线程的栈桢建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝;
    • (2)拷贝对象头中的 Mark Word 复制到锁记录中;
    • (3)拷贝成功后,虚拟机将使用CAS操作尝试将对Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner针指向object mark word.如果执行成功,
      则执行步骤(4),如果失败则执行步骤(5);
    • (4)如果更新成功,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为"00",即表示对象处于轻量级锁定状;
    • (5)如果更新失败,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程;

    7.4.3、轻量级锁解锁过程

    • (1)通过CAS操作尝试把线程中复制的 Displaced Mark Word 对象替换当前的 Mark Word。
    • (2)如果替换成功,整个同步过程就完成了。
    • (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀)那就要在释放锁的同时,唤醒被挂起的线程.

    7.5、偏向锁

    偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
    引入偏向锁是为了在无多线程环境的情况下尽可能减少不必要的轻量级锁执行路径。因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令,偏向锁是在只有一个线程执行同步块时进一步提高性能。

    偏向锁在JDK 6及以后的JVM里是默认启用的,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态;不适合锁竞争激烈的情况

    偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令-XX:BiasedLockingStartupDelay=0来关闭延迟

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPvwtFsU-1580025046158)(image/偏向锁获取与释放过程.png)]

    7.5.1、偏向锁的获取过程

    • (1)访问 Mark Word 中偏向锁的标识是否设置为1,锁标志位是否为01–确认可偏向状态
    • (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3).
    • (3)如果线程ID并未指向当前线程,则通过 CAS 操作竞争锁.如果竞争成功,则将 Mark Word 中线程ID设置为当前线程ID,然后值(5);如果竞争失败,执行(4)
    • (4)如果 CAS 获取偏向锁失败,则表示有竞争,当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码;
    • (5)执行同步代码;

    7.5.2、偏向锁释放过程

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁;

    偏向锁的释放仅仅将栈中的最近一条lock record的obj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id

    偏向锁是可以重偏向的,但是STW期间不能

    7.6、其他优化

    注意:这些优化仅在Java虚拟机server模式下起作用

    7.6.1、适应性自旋(Adaptive Spinning)

    • 从轻量级锁的获取过程中知道,当前线程在获取轻量级锁的过程中执行 CAS 操作失败时,是要通过自旋来获取重量级锁。问题是:自旋是需要消耗CPU的,如果一直获取不到锁,那么该线程就一直处在自旋状态。解决该问题最简单的方法是指定自旋的次数。JDK 6中变为默认开启,但是JDK采用了更合适的方法-适应性自旋。简单来说就是如果自旋成功了,那么下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少;PreBlockingSpin

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6XgIgwq-1580025046158)(image/自旋锁.png)]

    • 自旋锁的实现:

    public class MyWaitNotify3{
    	MonitorObject myMonitorObject = new MonitorObject();
    	boolean wasSignalled = false;
    
    	public void doWait(){
    		synchronized(myMonitorObject){
    			while(!wasSignalled){
    				try{
    					myMonitorObject.wait();
    				} catch(InterruptedException e){...}
    			}
    			//clear signal and continue running.
    			wasSignalled = false;
    		}
    	}
    
    	public void doNotify(){
    		synchronized(myMonitorObject){
    			wasSignalled = true;
    			myMonitorObject.notify();
    		}
    	}
    }

    7.6.2、锁粗化(Lock Coarsening)

    是将多次连接在一起的加锁和解锁操作合并在一起,将多个连续的锁扩展成一个范围更大的锁。如:

    	public class StringBufferTest {
    		public void append(){
    			StringBuffer stringBuffer = new StringBuffer();
    			stringBuffer.append("a");
    			stringBuffer.append("b");
    		}
    	}

    里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁

    7.6.3、锁消除(Lock Elimination)

    锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。如:方法内的局部变量;

    public void hello() {
    	Object object = new Object();
    	synchronized(object) {
    		System.out.println(object);
    	}
    }
    // 上述代码在逃逸分析中会被优化成:
    public void hello(){
    	Object object = new Object();
    	System.out.println(object);
    }

    7.7、总结

    JDk 中采用轻量级锁和偏向锁等对 synchronized 的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。

    优点 缺点 适应场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距; 如果线程存在锁竞争,会带来额外的锁撤销的消耗; 适用于只有一个线程访问同步块的场景
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度; 如果始终得不到锁竞争的线程使用自旋会消耗CPU; 追求响应时间,同步执行速度非常块;线程交替执行
    重量级锁 线程竞争不使用自旋,不会消耗CPU; 线程阻塞,响应时间缓慢; 追求吞吐量,同步块执行速度较长。

    7.8、synchronized相关面试题

    • synchronized使用时需要注意:

      • 锁对象不能为空:因为锁的信息时保存在对象头中的,如果对象都没有,锁信息无法保存
      • 作用域不宜过大:synchronized包裹的范围,会导致性能下降;
      • 避免死锁:相互等待锁导致死锁
    • 如何选择Lock和synchronized

      • 尽可能避免使用者两者,可以使用java.util.concurrent包下的;
      • 如果可以使用synchronized,就使用,因为使用lock会增加代码复杂度;
    • 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪一个线程?

      • 随机不可控
    • 一句话介绍synchronized:JVM会自动通过monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可冲入和不可中断的性质;

    8、volatile

    volatile特性

    9、线程安全

    当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,这个对象时线程安全的;

    9.1、哪些线程安全问题

    • 运行结果错误:a++多线程下出现消失的结果;

    • 活跃性问题:死锁、活锁、饥饿;

    • 对象发布和初始化的时候的安全问题;

      逸出时的安全问题:

      • 方法返回一个private对象;
      • 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界:
        • 在构造函数中未初始化完毕就将this赋值;
        • 隐式逸出:注册监听事件;
        • 在构造函数中运行线程;
          public class ThisEscape {
              public ThisEscape(EventSource source) {
                  source.register(
                      new EventListener(){
                          public void onEvent(Event e){
                              // doSomething
                          }
                      }
                  );
              }
          }

      解决逸出时安全问题:

      • 返回副本
      • 工厂模式
        public class SafeListener {
            private finale EventListener listener;
            private SafeListener(){
                listener = new EventListener(){
                    public void onEvent(Event e){
                        // doSomething
                    }
                }
            }
            public static SafeListener newInstance(EventSource source) {
                SafeListener safe = new SafeListener();
                source.registerListener(sale.listener);
                return safe;
            }
        }

    需要考虑线程安全的问题:

    • 访问共享的变量或资源,比如对象的属性、静态变量、共享缓存、数据库等,都有并发危险;
    • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发;
    • 不同的数据之间存在捆绑关系的时候;
    • 使用其他类时,如果对象没有声明是线程安全的;

    9.2、多线程导致的问题

    多线程性能问题:

    • 调度:上下文切换,在抢锁、IO会导致密集的上下文切换;
    • 协作:内存同步

    10、线程通信

    目标是使线程间能够互相发送信号.另一方面,线程通信使线程能够等待其他线程的信号

    10.1、使用 sleep 和 while(true)来实现线程通信

    弊端:线程需要不停的通过 while 语句轮询机制来检测某一个条件,注意会浪费CPU资源,如果轮询的时间间隔很小,更浪费CPU资源;如果时间间隔很大,有可能取不到想要得到的数据

    10.2、等待/唤醒机制

    • 方法 wait 的作用是使当前线程执行的代码进行等待,wait 方法是 Object 类的方法,该方法用来将当前线程置入"预执行队列",并且在wait 所在的代码行处停止执行,直到接到通知或者被中断为止.在调用 wait 之前,线程必须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中调用wait 方法。在执行wait方法之后,当前线程释放锁。在从 wati 返回之前,线程与其他线程竞争重新获得锁.如果调用wait 时没有合适的锁,则抛出 IllegalMonitorStateException 异常.不需要 catch;

    • 方法 notify 也要在同步方法或者同步块中调用,即在调用前,线程必须获得该对象的对象级别锁.如果调用 notify 是没有适当的锁,也会抛出 llegalMonitorStateException.该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划其随机挑选一个呈wait状态的线程,对其发出notify通知,并使它等待获取该对象的对象锁。值的注意的是:在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,需要等到执行 notify 方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的wait线程执行完毕后,它会释放该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在 wait状态,直到这个对象发挥一个 notify 或者 notifyAll;

    wait使线程停止运行,而 notify 使停止的线程继续运行

    • 通知过早问题:如果在 调用 wait 方法之前就调用过 notify 方法,即通知过早问题,会打乱程序的正常逻辑

    • 等待wait的条件发生了变化,也容易造成程序逻辑混乱.最典型的为对集合的操作

    10.3、生产者/消费者模式

    生产者/消费者模式是等待/通知模式最经典的案例

    10.3.1.一生产与一消费:操作值

    1个生产者与1个消费者进行数据交互

    10.3.2、多生产与多消费:操作值,有可能“假死”

    • 假死:其实就是线程进入 WAITING 等待状态,如果全部线程都进入 WAITING 状态,则程序就不再执行任何业务功能了,整个项目呈停止状态;

    • 多个生产者与消费者为什么会产生"假死"?虽然在代码中使用来 wait/notify,但不保证 notify 唤醒的是异类,也许是同类,比如:“生产者”唤醒“生产者”,“消费者”唤醒“消费者”,按照这样情况运行的比率积少成多,就会导致所有的线程都不能继续运行下去,大家都在等待;

    • 解决"假死"现象:将 生产者和消费者中的 notify 改为 notifyAll,那么其不光通知同类线程,也包括异类线程

    10.3.3、一生产与一消费:操作栈

    生产者向堆栈 List 对象中放入数据,使消费者从 List 堆栈中取出数据

    10.3.4、生产者消费者的不同模式实现

    生产者消费者

    10.4、通过管道进行线程间通信:字节流

    一个线程发送数据到输出管道,另一个线程从输入管道中读取数据,实现不同线程间通信使用代码 inputStream.connect(outputStream)或者 outputStream.connect(inputStream) 的作用使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入

    10.5、wait()、notify()和notifyAll()

    上述方法是定义在java.lang.Object类

    • 一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁,也就是说线程必须在同步块里调用wait()或者notify()

    • 不管是等待线程还是唤醒线程都在同步块里调用wait()和notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出 IllegalMonitorStateException 异常;

    • 一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁.这将允许其他线程也可以调用wait()或者notify()

    • 一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块,换句话说被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁

    10.6、丢失的信号(Missed Signals)

    • notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态,通知信号过后便丢弃了,如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号

    • 解决方案:设置变量表示是否被通知过

    • LostNotify.java

    10.7、假唤醒

    • 由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)

    • 为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁;注意:这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大

    • EarlyNotify.java

    10.8、多个线程等待相同信号

    10.9、不要在字符串常量或全局对象中调用wait()

    在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

    10.10、等待/通知的典型范式

    可归纳出等待/通知的典型范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者):

    • 等待方遵循原则如下

      • 获取对象的锁
      • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件;
      • 条件满足则执行对应的逻辑

      对应伪代码:

      synchronized(对象){
      	while(条件不满足){
      		对象.wait();
      	}
      	// doSomething();
      }
    • 通知方遵循原则如下:

      • 获得对象的锁
      • 改变条件
      • 通知所以等待在对象上的线程

      对应伪代码:

      synchronized(对象){
      	改变条件
      	对象.notifyAll();
      }

    11、ThreadLocal类

    存放每个线程的共享变量,解决变量在不同线程间的隔离性,能让线程拥有了自己内部独享的变量;覆盖initialValue方法指定线程独享的值

    每一个线程都有一个私有变量,是ThreadLocalMap类型。当为线程添加ThreadLocal对象时,就是保存到这个map中,所以线程与线程间不会互相干扰

    11.1、创建ThreadLocal对象

    private ThreadLocal myThreadLocal = new ThreadLocal();
    每个线程仅需要实例化一次即可。虽然不同的线程执行同一段代码时,访问同一个ThreadLocal变量,但是每个线程只能看到私有的ThreadLocal实例

    11.2、访问ThreadLocal对象

    • 一旦创建了一个ThreadLocal对象,你就可以通过以下方式来存储此对象的值:myThreadLocal.set("a thread local value");
    • 可以直接读取一个 ThreadLocal 对象的值:String threadLocalValue = (String) myThreadLocal.get();

    11.3、ThreadLocal泛型

    可以创建一个泛型化的ThreadLocal对象,当你从此ThreadLocal实例中获取值的时候,就不必要做强制类型转换

    private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

    11.4、初始化 ThreadLocal

    由于ThreadLocal对象的set()方法设置的值只对当前线程可见,那有什么方法可以为ThreadLocal 对象设置的值对所有线程都可见?通过 ThreadLocal 子类的实现,并覆写initialValue()方法,就可以为 ThreadLocal 对象指定一个初始化值:

    private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    	@Override
    	protected String initialValue() {
    		return "This is the initial value";
    	}
    };

    11.5、完整的ThreadLocal 实例

    public class ThreadLocalExample {
    	public static class MyRunnable implements Runnable {
    		private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    		@Override
    		public void run() {
    			threadLocal.set( (int) (Math.random() * 100D) );
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    			}
    			System.out.println(threadLocal.get());
    		}
    	}
    	public static void main(String[] args) {
    		MyRunnable sharedRunnableInstance = new MyRunnable();
    		Thread thread1 = new Thread(sharedRunnableInstance);
    		Thread thread2 = new Thread(sharedRunnableInstance);
    		thread1.start();
    		thread2.start();
    
    		thread1.join(); //wait for thread 1 to terminate
    		thread2.join(); //wait for thread 2 to terminate
    	}
    
    }

    11.6、InheritableThreadLocal

    正常情况下,父线程中在ThreadLocal设置的变量在子线程中是获取不到的,那么子线程如何获取父线程中的值呢?使用InheritableThreadLocal

    InheritableThreadLocalThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有值,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值

    InheritableThreadLocal继承自ThreadLocal,代码如下:

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    	// 
        protected T childValue(T parentValue) {
            return parentValue;
        }
        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }

    InheritableThreadLocal通过重写getMapcreateMap方法,让本地变量保存到了具体的inheritableThreadLocals(Thread类的变量)变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面;

    示例:

    public class ParentThreadSharedDataWithSon {
        // 使用InheritableThreadLocal来构建ThreadLocal,子线程能看到父线程设置的变量
      	public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        // public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
        public static void main(String[] args) {
            threadLocal.set("Hello 子线程");
            Thread thread = new Thread(()->{
                System.out.println(threadLocal.get());
            });
            thread.start();
            System.out.println("Main Thread:" + threadLocal.get());
        }
    }	

    11.7、ThreadLocal内存泄露

    11.7.1、ThreadLocal 的实现

    每个 Thread 维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取 value。ThreadLocalMap 是使用ThreadLocal的弱引用作为Key 的,弱引用的对象在 GC 时会被回收.

    11.7.2、为什么会内存泄漏

    • ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value;如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,永远无法回收,造成内存泄漏。只有thead退出以后,value的强引用链条才会断掉

    • ThreadLocalMap的设计中已经考虑到这种情况,加上了一些防护措施:在ThreadLocal的get()、set()、rehash、remove的时候都会清除线程ThreadLocalMap里所有key为null的value

    • 但是这些被动的预防措施并不能保证不会内存泄漏:

      • 使用线程池的时候,这个线程执行任务结束,ThreadLocal 对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏;
      • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么这个期间就会发生内存泄漏

    11.7.3、为什么使用弱引用

    • key使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致 Entry 内存泄漏

    • key使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal 也会被回收,value在下一次ThreadLocalMap调用set,get的时候会被清除;

    • 对比上述情况可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除;

    • ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用;

    11.7.4、ThreadLocal最佳实践:如何避免内存泄漏

    每次使用完ThreadLocal,都调用它的remove()方法,清除数据。在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。

    11.8、ThreadLocal的应用场景

    使用场合主要解决多线程中数据数据因并发产生不一致问题

    • 最常见的ThreadLocal使用场景为用来解决数据库连接、Session 管理等

    • 参数传递

      场景:如果方法一层一层调用,调用了很多层,但是有个别参数只需要第一层方法和最后一层方式使用,如何传递?可以使用 ThreadLocal 来操作:

      public class ThreadLocalCache {
      	public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
      }

      在拦截器或者AOP 中设置需要传输的参数;注意:在请求结束后一定要调用remove方法,移出不必要的键值对,以免造成内存泄漏。

    • 父子线程数据传递:InheritableThreadLocal

      • 该类继承自 ThreadLocal

        public class InheritableThreadLocal<T> extends ThreadLocal<T> {
        	protected T childValue(T parentValue) {
        		return parentValue;
        	}
        	/**
        		* 重写Threadlocal类中的getMap方法,在原Threadlocal中是返回
        		*t.theadLocals,而在这么却是返回了inheritableThreadLocals,因为
        		* Thread类中也有一个要保存父子传递的变量
        		*/
        	ThreadLocalMap getMap(Thread t) {
        		return t.inheritableThreadLocals;
        	}
        	/**
        		* 同理,在创建ThreadLocalMap的时候不是给t.threadlocal赋值
        		*而是给inheritableThreadLocals变量赋值
        		*
        		*/
        	void createMap(Thread t, T firstValue) {
        		t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        	}
        }
      • InheritableThreadLocal是如何实现在子线程中能拿到当前父线程中的值的呢?创建线程时,init(…)方法里有如下代码:

      if (parent.inheritableThreadLocals != null)
      	//这句话的意思大致不就是,copy父线程parent的map,创建一个新的map赋值给当前线程的inheritableThreadLocals。
      	this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

      拷贝 parentMap 的数据,在copy过程中是浅拷贝,key和value都是原来的引用地址

      • 在创建 InheritableThreadLocal 对象的时候赋值给线程的 t.inheritableThreadLocals 变量

      • 在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为 null,如果不为null则copy一份ThradLocalMap 到子线程的t.inheritableThreadLocals成员变量中去

      • 因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值;

      • InheritableThreadLocal问题:在线程池中,会缓存之前使用过的线程

        • ①、问题场景:

          有两个线程A,B, 如果A线程先执行后,将InheritableThreadLocal中的value已经被更新了,由于B是获取到缓存线程,直接从t.InheritableThreadLocal 中获得值,而此时获取的值是 A 线程修改过后的值,而不是原来父线程的值。
        • ②、造成问题的原因:线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用现在的localMap
        • ③、解决方案:
          在使用完这个线程的时候清除所有的localMap,在submit新任务的时候在重新重父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值;阿里巴巴有一套解决方案:transmittable-thread-local

    ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static 修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享 此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只 要是这个线程内定义的)都可以操控这个变量

    11.9、FastThreadLocal

    1、用法:

    FastThreadLocal用法上兼容ThreadLocal。使用FastThreadLocal不用像ThreadLocal那样先try 之后finally进行threadLocal对象.remove();

    由于构造FastThreadLocalThread的时候,通过FastThreadLocalRunnable对Runnable对象进行了包装:

    FastThreadLocalRunnable.wrap(target); // 从而构造了FastThreadLocalRunnable对象

    FastThreadLocalRunnable在执行完之后都会调用FastThreadLocal.removeAll();

    2、最佳实践:

    FastThreadLocal并不是什么情况都快。 使用FastThreadLocalThread线程才会快,如果是普通线程还更慢! 注意:使用FastThreadLocalThread线程才会快,如果是普通线程还更慢!

    Netty中有两个基准测试类:FastThreadLocalFastPathBenchmark、FastThreadLocalSlowPathBenchmark

    • FastThreadLocal操作元素的时候,使用常量下标在数组中进行定位元素来替代ThreadLocal通过哈希和哈希表,这个改动特别在频繁使用的时候,效果更加显著!
    • 想要利用上面的特征,线程必须是FastThreadLocalThread或者其子类,默认DefaultThreadFactory都是使用FastThreadLocalThread的
    • 只用在FastThreadLocalThread或者子类的线程使用FastThreadLocal才会更快,因为FastThreadLocalThread 定义了属性threadLocalMap类型是InternalThreadLocalMap。如果普通线程会借助ThreadLocal

    3、伪共享问题:

    FastThreadLocal利用字节填充来解决伪共享问题

    所以FastThreadLocal比ThreadLocal快,并不是空间换时间,FastThreadLocal并没有浪费空间!!!

    12、深入理解ThreadLocal

    12.1、理解ThreadLocal

    ThreadLocal 在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能;

    12.2、深入解析ThreadLocal 类

    • public T get():用来获取ThreadLocal在当前线程中保存的变量副本

    • public void set(T value):用来设置当前线程中变量的副本

    • public void remove():用来移除当前线程中变量的副本

    • protected T initialValue():是一个 protected 方法,一般是用来在使用时进行重写的,它是一个延迟加载方法

    • ThreadLocal 是如何为每个线程创建变量的副本的:

      • ①、在每个线程 Thread 内部有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量);
      • ②、初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals;
      • ③、为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
    • 在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法;

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
    	protected Long initialValue() {
    		return Thread.currentThread().getId();
    	};
    };

    12.3、变量

    private final int threadLocalHashCode = nextHashCode();
    // 即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
    private static final int HASH_INCREMENT = 0x61c88647;
    • 哈希策略

      所有 ThreadLocal 对象共享一个AtomicInteger对象nextHashCode用于计算hashcode,一个新对象产生时它的hashcode就确定了,算法是从0开始,以HASH_INCREMENT = 0x61c88647 为间隔递增。这是ThreadLocal唯一需要同步的地方

    • 0x61c88647这个魔数是怎么确定的呢?

      • ThreadLocalMap 的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,上述算法中使用 0x61c88647 可以让hash的结果在2的n次方内尽可能均匀分布,减少冲突的概率。
      • 这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527
      • (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647

    12.4、ThreadLocalMap

    ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制

    其定义在 ThreadLocal 类内部的私有类,它是采用"开放定址法"解决冲突的hashmap。key是ThreadLocal对象(ThreadLocal中存放的是ThreadLocal的弱引用)。当调用某个ThreadLocal对象的get或put方法时,首先会从当前线程中取出ThreadLocalMap,然后查找对应的value。ThreadLocalMap 实例是作为 java.lang.Thread 的成员变量存储的,每个线程有唯一的一个 threadLocalMap

    public T get() {
    	Thread t = Thread.currentThread();
    	ThreadLocalMap map = getMap(t);     //拿到当前线程的ThreadLocalMap
    	if (map != null) {
    		ThreadLocalMap.Entry e = map.getEntry(this);    // 以该ThreadLocal对象为key取value
    		if (e != null)
    			return (T)e.value;
    	}
    	return setInitialValue();
    }
    ThreadLocalMap getMap(Thread t) {
    	return t.threadLocals;
    }
    • 将 ThreadLocalMap 作为 Thread 类的成员变量的好处是:

      当线程死亡时,threadLocalMap被回收的同时,保存的"线程局部变量"如果不存在其它引用也可以同时被回收。同一个线程下,可以有多个treadLocal实例,保存多个"线程局部变量"。同一个threadLocal实例,可以有多个线程使用,保存多个线程的"线程局部变量"。
    • ThreadLocalMap类内部有个静态内部类:Entry,其继承自WeakReference
      static class Entry extends WeakReference<ThreadLocal<?>> {
      	/** The value associated with this ThreadLocal. */
      	Object value;
      
      	Entry(ThreadLocal<?> k, Object v) {
      		super(k);
      		value = v;
      	}
      }
    • ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值

    12.5、碰撞解决与神奇的 0x61c88647

    既然ThreadLocal用map就避免不了冲突的产生

    • 碰撞的类型:

      • 只有一个ThreadLocal实例的时候,当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法,且是线性探测(linear-probe)
      • 多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率,同时利用开放定址法处理碰撞
    • 神奇的0x61c88647:注意 0x61c88647 的利用主要是为了多个ThreadLocal实例的情况下用的

      private final int threadLocalHashCode = nextHashCode();

      每当创建ThreadLocal实例时这个值都会累加 0x61c88647。为了让哈希码能均匀的分布在2的N次方的数组里,即Entry[] table

      ==> threadLocalHashCode 的使用:

      private void set(ThreadLocal<?> key, Object value) {
      	Entry[] tab = table;
      	int len = tab.length;
      	int i = key.threadLocalHashCode & (len-1);
      	...		           
      }
      key.threadLocalHashCode & (len-1)

      ThreadLocalMap中Entry[] table的大小必须是2的N次方呀(len = 2^N), 那len-1的二进制表示就是低位连续的N个1,那key.threadLocalHashCode & (len-1)的值就是threadLocalHashCode 的低N位.产生的哈希码分布真的是很均匀,而且没有任何冲突啊;可以使用python验证:

      	import sys
      	HASH_INCREMENT = 0x61c88647
      	def magic_hash(n):
      		for i in range(n):
      			nextHashCode = i * HASH_INCREMENT + HASH_INCREMENT;
      			result = nextHashCode & (n-1)
      			sys.stdout.write(str(result))
      			sys.stdout.write("\t")
      		print()
      	magic_hash(16)
      	magic_hash(64)

    12.6、ThreadLocal和synchronized

    • ThreadLocal 和 synchronized 都用于解决多线程并发访问
    • ThreadLocal 与 synchronized 有本质的区别:
      • synchronized 是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问.
      • ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享
    • synchronized 用于线程间的数据共享,而 ThreadLocal 则用于线程间的数据隔离

    12.7、ThreadLocal与线程池

    在线程池中,由于大部分线程都是重用而不是重新创建,那么对应线程中的ThreadLocal没有清理,造成数据篡写;所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。

    13、死锁

    两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁;

    13.1、死锁产生的必要条件

    • 互斥条件:所谓互斥就是进程在某一时间内独占资源
    • 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    上述四个条件同时满足才会发生死锁;

    线程死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况

    13.2、死锁程序

    public class DeadLock{
    	public void method1(){
    		synchronized(Integer.class){
    			System.out.println("Aquired lock on Integer.class object");
    			synchronized(String.class){
    				System.out.println("Aquired lock on String.class object");
    			}
    		}
    	}
    	public void method2(){
    		synchronized(String.class){
    			System.out.println("Aquired lock on String.class object");
    			synchronized(Integer.class){
    				System.out.println("Aquired lock on Integer.class object");
    			}
    		}
    	}
    }

    还有一种典型的死锁场景:类加载过程中的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是应用本身的代码库,jstack不见得能显示全部锁信息。具体可参看JDK官方文档

    13.3、避免死锁

    • 加锁顺序:如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(并对这些锁做适当的排序),但总有些时候是无法预知的

    • 加锁时限:

      • ①、在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
      • ②、这种机制存在一个问题,在Java中不能对 synchronized 同步块设置超时时间你需要创建一个自定义锁,或使用Java5中java.util.concurrent 包下的工具
    • 死锁检测:是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景

      • ①、如何检测:
        每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中.当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生
      • ②、当检测出死锁时,这些线程该做些什么呢?
        • 一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁
        • 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁;
    • 给锁线程增加有意义的名字;

    • 专锁专用

    13.4、死锁检测

    • 使用jstack命令来进行检测,jstack <pid>
      "Thread-1":
      	at com.learning.example.thread.base.Test.run(DeadLock.java:29)
      	- waiting to lock <0x000000076b022c38> (a java.lang.Object)
      	- locked <0x000000076b022c48> (a java.lang.Object)
      	at java.lang.Thread.run(Thread.java:748)
      "Thread-0":
      	at com.learning.example.thread.base.Test.run(DeadLock.java:22)
      	- waiting to lock <0x000000076b022c48> (a java.lang.Object)
      	- locked <0x000000076b022c38> (a java.lang.Object)
      	at java.lang.Thread.run(Thread.java:748)
      
      Found 1 deadlock.
      
    • 实际应用中,类死锁情况可能不会有上面如此清晰的输出,总体上可以理解为:区分线程状态 -> 查看等待目标 -> 对比Monitor等持有状态;
    • Java提供了标准的管理API,ThreadMXBean,其直接提供了 findDeadlockedThreads()方法用于定位

    13.5、死循环导致线程等待如何诊断

    如果是死循环导致线程等待,会导致CPU飙升,可以使用top命令配合grep java查找到CPU使用率高的java进程,再通过top -Hp <pid>查看该java进程下CPU使用率较高的线程,然后在使用jstack命令查看线程的具体情况

    13.6、分布式环境下死锁

    分布式事务下死锁问题

    13.7、发生死锁后修复策略

    • 保存案发现场,然后重启服务器;
    • 后续对日志进行分析;

    修复策略:

    • 避免策略

    13.8、哲学家就餐问题

    哲学家就餐问题是在计算机科学中的一个经典问题,用来演示在并行计算中多线程同步(Synchronization)时产生的问题;

    哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子;

    哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽;

    解决哲学家就餐问题:

    • 服务员检查(避免策略);
    • 改变一个哲学家拿叉子的顺序(避免策略);
    • 餐票(避免策略);
    • 领导调节(检测与恢复策略):进程终止、资源抢占;

    14、活锁、饥饿和公平

    14.1、活锁

    • 线程未发生阻塞,但仍然存在无法执行的情况,称为活锁。线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行

    • 解决活锁:尝试等待一个随机的时间

    14.2、饥饿

    如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”,解决饥饿的方案被称之为“公平性”即所有线程均能公平地获得运行机会

    14.3、Java中导致饥饿的原因

    • 高优先级线程吞噬所有的低优先级线程的CPU时间:优先级越高的线程获得的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台,对大多数应用来说,你最好是不要改变其优先级值;
    • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问;
    • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态;

    14.4、在Java中实现公平性方案

    • 使用锁,而不是同步块:为了提高等待线程的公平性,我们使用锁方式来替代同步块
    • 公平锁。
    • 注意性能方面

    15、伪共享问题

    伪共享问题涉及到CPU缓存问题

    15.1、CPU缓存

    CPU 缓存(Cache Memory)是位于 CPU 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多;

    高速缓存的出现主要是为了解决 CPU 运算速度与内存读写速度不匹配的矛盾,因为 CPU 运算速度要比内存读写速度快很多,这样会使 CPU 花费很长时间等待数据到来或把数据写入内存。

    在缓存中的数据是内存中的一小部分,但这一小部分是短时间内 CPU 即将访问的,当 CPU 调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度

    CPU 缓存可以分为一级缓存,二级缓存,部分高端 CPU 还具有三级缓存;每一级缓存中所储存的全部数据都是下一级缓存的一部分,越靠近 CPU 的缓存越快也越小。当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,然后是 L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长;为了支撑更多的热点数据,同时追求最高的性价比,多级缓存架构应运而生

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qbC3SOL-1580025046160)(image/CPU-Cache层次结构图.png)]

    15.2、缓存行

    缓存行

    15.3、什么是伪共享

    多个线程同时读写同一个缓存行的不同变量时导致的 CPU 缓存失效;尽管这些变量之间没有任何关系,但由于在主内存中邻近,存在于同一个缓存行之中,它们的相互覆盖会导致频繁的缓存未命中,引发性能下降

    15.4、为什么会发生伪共享

    如果多个线程的变量共享了同一个 CacheLine,任意一方的修改操作都会使得整个 CacheLine 失效(因为 CacheLine 是 CPU 缓存的最小单位),也就意味着,频繁的多线程操作,CPU 缓存将会彻底失效,降级为 CPU core 和主内存的直接交互;

    比如在 JDK 的 LinkedBlockingQueue 中,存在指向队列头的引用 head 和指向队列尾的引用 tail 。而这种队列经常在异步编程中使有,这两个引用的值经常的被不同的线程修改,但它们却很可能在同一个缓存行,于是就产生了伪共享

    15.5、伪共享解决方法

    • 字节填充(缓存行填充)

      一条缓存行有 64 字节,而 Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享

      public class PaddingObject{
      	public volatile long value = 0L;    // 实际数据
      	public long p1, p2, p3, p4, p5, p6; // 填充
      }

      在 Java7 之后,一个 JVM 的优化给字节填充造成了一些影响,上面的代码片段 public long p1, p2, p3, p4, p5, p6; 会被认为是无效代码被优化掉,有回归到了伪共享的窘境之中,可以使用继承的方式来填充;

    • Contended注解方式:

      在JDK1.8中,新增了一种注解@sun.misc.Contended,来使各个变量在Cache line中分隔开。注意,jvm需要添加参数-XX:-RestrictContended才能开启此功能,填充的默认宽度是128,要自定义宽度可以设置 -XX:ContendedPaddingWidth参数

      @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.FIELD, ElementType.TYPE})
      public @interface Contended {
      	String value() default "";
      }

    15.6、伪共享最佳实践

    • 在Thread类中生成随机数是和线程有着关联,多线程下产生随机数的操作是很常见的,JDK为了确保产生随机数的操作不会产生false sharing ,把产生随机数的三个相关值设为独占cache line:

      // The following three initially uninitialized fields are exclusively
      // managed by class java.util.concurrent.ThreadLocalRandom. These
      // fields are used to build the high-performance PRNGs in the
      // concurrent code, and we can not risk accidental false sharing.
      // Hence, the fields are isolated with @Contended.
      /** The current seed for a ThreadLocalRandom */
      @sun.misc.Contended("tlr")
      long threadLocalRandomSeed;
      
      /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
      @sun.misc.Contended("tlr")
      int threadLocalRandomProbe;
      
      /** Secondary seed isolated from public ThreadLocalRandom sequence */
      @sun.misc.Contended("tlr")
      int threadLocalRandomSecondarySeed;
    • ConcurrentHashMap中的内部类CounterCell,另外还包括并发容器 Exchanger 也有相同的操作

      @sun.misc.Contended static final class CounterCell {
          volatile long value;
          CounterCell(long x) { value = x; }
      }
    • RingBuffer,优秀的开源框架 Disruptor 中的一个数据结构

      abstract class RingBufferPad{
      	protected long p1, p2, p3, p4, p5, p6, p7;
      }

    16、ThreadLocalRandom

    17、线程分类

    在JVM中除了应用线程,还有其他一些线程用于支持JVM的运行,这些线程可以被划分为以下几类:

    • VM Thread:负责JVM在安全点内的各种操作,这些操作(诸如自动内存管理、取消偏向锁、线程dump、线程挂起等等)在执行过程中需要JVM处于这样一个状态——堆的内容不会被改变,这种状态在JVM里叫做安全点(safe-point)。
    • Periodic task thread:这个线程负责响应定时触发的事件(例如:中断),用来执行一些定时操作。
    • GC thread:这些线程负责JVM里的垃圾收集活动;
    • Compiler threads:这些线程负责在运行时将字节码编译为本地代码;
    • Singal dispatcher thread:这些线程负责响应外部发给当前JVM进程的信号,并通过调用JVM内的其他线程。

    可以通过jstack命令获取dump文件

    18、JVM如何实现线程

    Thread类中start方法,用来启动一个线程,会调用对应的run方法。一个线程退出后是再次start是非法的,会抛出异常。

    start方法调用了start0方法,这是一个JNI接口,在Java中通过JNI接口可以实现Java调用本地方法,start0方法的实现在 jdk/src/share/native/java/lang/Thread.c中定义

    19、线程调度算法

    线程调度是指按照特定机制为多个线程分配CPU的使用权;

    Java 虚拟机如何进行线程调度:有两种调度模型:分时调度模型和抢占式调度模型

    • 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解;
    • Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU;

    放弃CPU原因:一个线程会因为以下原因而放弃 CPU

    • Java 虚拟机让当前线程暂时放弃 CPU,转到就绪状态,使其它线程获得运行机会;
    • 当前线程因为某些原因而进入阻塞状态;
    • 线程结束运行

    Java 的线程调度是不分时的,同时启动多个线程后,不能保证各个线程轮流获得均等的 CPU 时间片。不是所有的线程切换都需要进入内核模式

    20、CPU超线程技术

    • 超线程是英特尔公司的私有技术,其能够帮助提升物理处理器的资源使用效率,保证处理器一直处于繁忙状态,以便完成更多工作

    三、JUC(java.util.concurrent)包

    从整体来看,concurrent包的实现示意图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnZyQXnK-1580025046160)(image/concurrent包的实现示意图.png)]

    1、JUC原子类

    目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的,原子类都是在java.util.concurrent.atomic

    1.1、原子类概览

    原子都是在

    • 基本类型:AtomicInteger、AtomicLong、AtomicBoolean;JDK8 新增原子化累加器:DoubleAdder、DoubleAccumulator、LongAdder、LongAccumulator,这四个类仅仅用来支持累加操作;
    • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;
    • 引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
    • 对象的属性修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,对普通变量可以进行升级操作,使用这三个原子类需要注意以下几点:
      • 对象必须是volatile类型的,只有这样才能保证可见性。否则会抛出IllegalArgumentException异常;
      • 不能将变量声明为static的;
      • 不能是私有变量,即private修饰的;
    • Adder累加器:LongAdder、DoubleAdder;本质上是空间换时间;
    • Accumulator累加器:LongAccumulator、DoubleAccumulator,适合大量和并行计算

    1.2、各类原子类使用

    • AtomicLong 是作用是对长整形进行原子操作。

      在32位操作系统中,64位的long和double变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性。而使用AtomicLong能让long的操作保持原子型。

      long foo = 65465498L; 非原子操作,Java 会分两步写入 long 变量,先写32位,再写后32位,就非线程安全的。
      private volatile long foo; ==> 原子性操作

    • AtomicIntegerFieldUpdater用法:

      static Student tom;
      static Student peter;
      private static AtomicIntegerFieldUpdater<Student> updater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "score");
      @Override
      public void run() {
          for (int i = 0; i < 10000; i++) {
              peter.score ++;
              updater.getAndIncrement(tom);
          }
      }
      static class Student {
         volatile int score;
      }

      可以将Student对象里的score用过AtomicIntegerFieldUpdater变为原子操作;

    • Adder累加器:高并发下LongAdder比AtomicLong效率高,其本质是空间换时间,竞争激烈的情况下,LongAdder把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,是利用多段锁的理念;AtomicLong由于竞争激烈,每一次加法,都要flush和refresh,即与主内存通信;而LongAdder每个线程都会有自己的计数器,仅在自己线程内计数;最后再通过sum方法来汇总这些操作,而sum方法是同步的;

      使用场景的区别:在竞争激烈的情况下,LongAdder的预期吞吐量要高,但是会消耗更多的空间;LongAdder适合的场景是统计求和和计数的场景,LongAdder基本只提供了add方法,而AtomicLong提供了CAS方法

    2、锁的相关概念

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jP6coGwc-1580025046161)(image/Java锁小结.png)]

    2.1、同步锁:通过synchronized关键字来进行同步

    同步锁的原理是,对于每一个对象,有且仅有一个同步锁:不同的线程能共同访问该同步锁。但是,在同一个时间点,该同步锁能且只能被一个线程获取到;

    2.2、JUC包中的锁

    包括:Lock接口、ReadWriteLock接口、LockSupport阻塞、Condition条件 和 AbstractOwnableSynchronizer、AbstractQueuedSynchronizer、AbstractQueuedLongSynchronizer 三个抽象类,ReentrantLock独占锁、ReentrantReadWriteLock读写锁。由于CountDownLatch、CyclicBarrier 和 Semaphore 也是通过AQS来实现的

    2.3、可重入锁

    synchronized和ReentrantLock都是可重入锁,锁基于线程的分配,而不是基于方法调用的分配。

    线程可以进入任何一个它已经拥有的锁所同步着的代码块。可重入锁是用来最大的作用是用来解决死锁的;

    2.4、AQS-AbstractQueuedSynchronizer类

    是java中管理"锁"的抽象类,锁的许多公共方法都是在这个类中实现。AQS 是独占锁(例如:ReentrantLock)和共享锁(例如:Semaphore)的公共父类。

    AQS锁的分类:

    • 独占锁:锁在一个时间点只能被一个线程锁占有.根据锁的获取机制,它又划分为"公平锁"和"非公平锁"。

      • 公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁;
      • 非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁;

      独占锁的典型实例子是 ReentrantLock,此外,ReentrantReadWriteLock.WriteLock 也是独占锁

    • 共享锁:能被多个线程同时拥有,能被共享的锁。JUC 包中的 ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch 和 Semaphore 都是共享锁。

    2.5、CLH队列-Craig Landin and Hagersten lock queue

    CLH 队列是 AQS 中"等待锁"的线程队列.在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,常常需要通过锁来保护这些资源在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待.CLH 就是管理这些"等待锁"的线程的队列。CLH 是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。

    2.6、CAS:Compare And Swap

    是比较并交换函数,它是原子操作函数;即通过CAS操作的数据都是以原子方式进行的。

    synchronized、Lock 都采用了悲观锁的机制,而 CAS 是一种乐观锁的实现

    2.7、对象锁、类锁、私有锁

    • 对象锁:使用 synchronized 修饰非静态的方法以及 synchronized(this) 同步代码块使用的锁是对象锁;
    • 类锁:使用 synchronized 修饰静态的方法以及 synchronized(class) 同步代码块使用的锁是类锁;
    • 私有锁:在类内部声明一个私有属性如private Object lock,在需要加锁的同步块使用 synchronized(lock)

    其特性:

    • 对象锁具有可重入性。
    • 当一个线程获得了某个对象的对象锁,则该线程仍然可以调用其他任何需要该对象锁的 synchronized 方法或 synchronized(this) 同步代码块。
    • 当一个线程访问某个对象的一个 synchronized(this) 同步代码块时,其他线程对该对象中所有其它 synchronized(this) 同步代码块的访问将被阻塞,因为访问的是同一个对象锁。
    • 每个类只有一个类锁,但是类可以实例化成对象,因此每一个对象对应一个对象锁。
    • 类锁和对象锁不会产生竞争。
    • 私有锁和对象锁也不会产生竞争。
    • 使用私有锁可以减小锁的细粒度,减少由锁产生的开销。

    3、独占锁

    3.1、ReentrantLock

    ReentantLock可重入锁,又是独占锁

    4、共享锁-ReentrantReadWriteLock-读写锁

    4.1、ReadWriteLock

    读写锁,维护了一对锁:读取锁和写入锁;

    • 读取锁:只用于读取数据操作,是"共享锁",能被多个线程同时获取;
    • 写入锁:用于写入操作,是"独占锁",只能被一个线程获取。
    • 不能同时存在读取锁和写入锁

    ReadWriteLock 是一个接口,ReentrantReadWriteLock 是它的实现类。ReentrantReadWriteLock 包括内部类 ReadLock 和 WriteLock

    适合读多写少的情况

    4.2、如何获取读写锁

    // 返回用于读取操作的锁。
    ReentrantReadWriteLock.ReadLock readLock()
    // 返回用于写入操作的锁。
    ReentrantReadWriteLock.WriteLock writeLock()

    ReentrantReadWriteLock与ReentrantLock一样,其锁主体依然是Sync,它的读锁、写锁都是依靠Sync来实现的

    读写锁ReentrantReadWriteLock内部维护着两个一对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,将 state 这个 32 位的 int 值分为高 16 位和低 16位,分别用于共享模式和独占模式;分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过为运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。代码如下:

    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    • state 的高 16 位代表读锁的获取次数,包括重入次数,获取到读锁一次加 1,释放掉读锁一次减 1
    • state 的低 16 位代表写锁的获取次数,因为写锁是独占锁,同时只能被一个线程获得,所以它代表重入次数;
    • 每个线程都需要维护自己的 HoldCounter,记录该线程获取的读锁次数,这样才能知道到底是不是读锁重入,用 ThreadLocal 属性 readHolds 维护

    4.3、写锁:支持可重入的排他锁

    写锁的获取最终会调用tryAcquire(int arg),该方法在内部类Sync中实现;

    写锁的释放最终还是会调用AQS的模板方法release(int arg)方法,该方法首先调用tryRelease(int arg)方法尝试释放锁,tryRelease(int arg)方法为读写锁内部类Sync中定义了

    非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作;

    Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁;

    4.4、读锁:可重入的共享锁

    它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是或获取成功;

    读锁的获取过程:

    • 因为存在锁降级情况,如果存在写锁且锁的持有者不是当前线程则直接返回失败,否则继续;但是需要遵循先获取写锁、获取读锁在释放写锁的次序;

      在获取读锁的方法tryAcquireShared(int unused)中,有一段代码就是来判读锁降级的:

      int c = getState();
      // exclusiveCount(c)计算写锁,如果存在写锁,且锁的持有者不是当前线程,直接返回-1
      if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
      	return -1;

      为什么锁降级中读锁的获取释放?

      假如当前线程A不获取读锁而是直接释放了写锁,这个时候另外一个线程B获取了写锁,那么这个线程B对数据的修改是不会对当前线程A可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程A释放读锁后,线程B才会获取写锁成功

    • 依据公平性原则,判断读锁是否需要阻塞,读锁持有线程数小于最大值(65535),且设置锁状态成功,执行以下代码(对于HoldCounter下面再阐述),并返回1。如果不满足改条件,执行fullTryAcquireShared();fullTryAcquireShared(Thread current)会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1

    • 读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程

    • 为何要引入firstRead、firstReaderHoldCount。这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds

    • 公平情况下,对于读锁,不允许插队,避免写锁无法获取到;

    • 非公平情况下,写锁可以随时插队,读锁仅在等待队列头结不是想获取写锁的线程时候可以插队;

      static final class NonfairSync extends Sync {
          private static final long serialVersionUID = -8159625535654395037L;
          final boolean writerShouldBlock() {
              return false; // writers can always barge
          }
          final boolean readerShouldBlock() {
              return apparentlyFirstQueuedIsExclusive();
          }
      }

    4.5、StampedLock:比读写锁性能更高的锁

    其有写锁、悲观读锁、乐观读

    5、共享锁-闭锁:CountDownLatch

    CountDownLatch原理和示例

    • 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。允许1或xN个线程等待其他线程完成执行;
    • 数据结构:CountDownLatch 包含了sync对象,sync是 Sync 类型;CountDownLatch的Sync是实例类,它继承于AQS通过"共享锁"实现;
    • CountDownLatch 中3个核心函数: CountDownLatch(int count), await(), countDown()
    CountDownLatch(int count):
    public CountDownLatch(int count) {
    	if (count < 0) throw new IllegalArgumentException("count < 0");
    	this.sync = new Sync(count);
    }
    Sync(int count) {
    	setState(count);
    }
    protected final void setState(long newState) {
    	state = newState;
    }

    在AQS中,state是一个private volatile long类型的对象。对于CountDownLatch而言,state表示的“锁计数器”;CountDownLatch中的getCount()最终是调用AQS中的getState(),返回的state对象,即”锁计数器“

    • 使用场景:并行计算

    • 实现原理:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p59svVWR-1580025046161)(image/CountdownLatch.png)]

      • CountDownLatch是通过“共享锁”实现的.
      • 在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”最多能被count给线程同时获取.
      • 当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行。
      • 而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;
      • 通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!
    • 使用例子

    private final static int threadCount = 200;
    public static void main(String[] args)throws Exception {
    	final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
    	ExecutorService exec = Executors.newCachedThreadPool();
    	for (int i = 1; i <= threadCount; i++) {
    		final int count = i;
    		exec.execute(() -> {
    			try{
    				test(count);
    			} catch (Exception e){
    				log.error("exception", e);
    			} finally {
    				countDownLatch.countDown();
    			}
    		});
    	}
    	// 等待线程池中所有线程执行完毕后,main方法线程才继续执行
    	countDownLatch.await();
    	// 可以设置等待时长,即等待多少时间后执行main方法线程
    //        countDownLatch.await(10, TimeUnit.MILLISECONDS);
    	log.info("~~~~~~~~main method finish {}", Thread.currentThread().getName());
    	exec.shutdown();
    }
    private static void test(int count) throws Exception {
    	Thread.sleep(100);
    	log.info("{}, {}", count, Thread.currentThread().getName());
    }

    6、栅栏:CyclicBarrier

    • 6.1、是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的barrier;CyclicBarrier 是包含了"ReentrantLock对象lock"和"Condition对象",它是通过独占锁实现的;

      CyclicBarrier 的原理不是 AQS 的共享模式,是 AQS Condition 和 ReentrantLock 的结合使用

      下图应该从下往上看才正确

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Z4waPOi-1580025046161)(image/CyclicBarrier.png)]

    • 6.2、主要方法:

      • CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。

      • CyclicBarrier(int parties, Runnable barrierAction):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

      • int await():在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。如果该线程不是到达的最后一个线程,则他会一直处于等待状态,除非发生以下情况:

        • 最后一个线程到达,即index == 0;
        • 超出了指定时间(超时等待);
        • 其他的某个线程中断当前线程;
        • 其他的某个线程中断另一个等待的线程;
        • 其他的某个线程在等待barrier超时;
        • 其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态

        如果一个线程处于等待状态时,如果其他线程调用reset(),或者调用的barrier原本就是被损坏的,则抛出BrokenBarrierException异常。同时,任何线程在等待时被中断了,则其他所有线程都将抛出BrokenBarrierException异常,并将barrier置于损坏状态

        在CyclicBarrier中,同一批线程属于同一代。当有parties个线程到达barrier,generation就会被更新换代。其中broken标识该当前CyclicBarrier是否已经处于中断状态;

        当barrier损坏了或者有一个线程中断了,则通过breakBarrier()来终止所有的线程;在breakBarrier()中除了将broken设置为true,还会调用signalAll将在CyclicBarrier处于等待状态的线程全部唤醒。

      • int await(long timeout, TimeUnit unit):在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。

      • int getNumberWaiting():返回当前在屏障处等待的参与者数目。

      • int getParties():返回要求启动此 barrier 的参与者数目。

      • boolean isBroken():查询此屏障是否处于损坏状态。

      • void reset():将屏障重置为其初始状态。

    • 6.3、使用场景:并行计算等。当一组线程(任务)并发的执行一件工作的时候,必须等待所有的线程(任务)都完成时才能进行下一个步骤

    • 6.4、CountDownLatch 与 CyclicBarrier 两者的区别:

      • CountDownLatch 的作用是允许1或N个线程等待其他线程完成执行;CyclicBarrier 则是允许N个线程相互等待;
      • CountDownLatch 的计数器无法被重置;CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的barrier;
      • CoundDownLatch操作的是事件,而CyclicBarrier侧重点是线程,而不是调用事件
    • 6.5、例子:

    @Slf4j
    public class CyclicBarrierDemo {
        static CyclicBarrier barrier = new CyclicBarrier(5);
        // 到达屏障后执行某个回调
        static CyclicBarrier barrier = new CyclicBarrier(5, ()->{
            log.info("sdasdasdasdasdas");
        });
        public static void main(String[] args)throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
    
            for (int i = 0; i < 10; i++) {
                final int count = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
                    try {
                        race(count);
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            executorService.shutdown();
        }
        private static void race(int count) throws Exception{
            Thread.sleep(1000);
            log.info("{} is ready", count);
            barrier.await();
            log.info("{} continue",count);
        }
    }
    • 6.6、在并行任务计算中,如果单个线程完成了任务还需要等待其他线程完成,假如有10个线程正在跑任务,有9个线程已经完成了任务,那么这9个线程就得都等待该线程,这可能是很大的资源浪费,使用CountDownLatch也有这个问题。

    7、共享锁-信号量:Semaphore

    • 是一个计数信号量,它的本质是一个"共享锁",它的作用是限制某段代码块的并发数

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0mRMB3IK-1580025046161)(image/Semaphore.png)]

    • 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可

    • Semaphore 包含了sync对象,sync是 Sync 类型;而且,Sync 也是一个继承于 AQS 的抽象类。Sync包括两个子类:"公平信号量"FairSync和"非公平信号量"NonfairSync。默认情况下,sync是NonfairSync(即,默认是非公平信号量)。

    • “公平信号量”和“非公平信号量”的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在CLH队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在CLH队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的tryAcquireShared()函数的实现不同

    • 使用场景:Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目,比如数据库连接池的连接数;可以用来实现限流器

    • 如果Semaphore的数值初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁类似,;但区别也必将明显,比如互斥锁是有持有者的;

    • Semaphore内部包含公平锁(FairSync)和非公平锁(NonfairSync),继承内部类Sync,其中Sync继承AQS;Semaphore默认选择非公平锁

    • 例子:

    @Slf4j
    public class SemaphoreDemo {
        private final static int threadCount = 20;
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 1; i <= threadCount; i++) {
                final int count = i;
                executorService.execute(() -> {
                    try{
                        // 写法1: 获取许可,执行方法,释放许可
    //                    semaphore.acquire();
    //                    test(count);
    //                    semaphore.release();
    
                        // 写法2:尝试获取许可,获取成功则执行方法;如果没有获取成功,则不丢弃;
                        // 尝试获取可以设置超时时间:tryAcquire(long timeout, TimeUnit unit)
                        if (semaphore.tryAcquire()){
                            test(count);
                            semaphore.release();
                        }
                    }catch (Exception e){
                        log.error("exception", e);
                    }
                });
            }
            executorService.shutdown();
        }
        private static void test(int count) throws Exception {
            Thread.sleep(1000);
            log.info("{}, {}", count, Thread.currentThread().getName());
            log.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        }
    }

    8、Condition

    • 在使用 notify 和 notifyAll 方法进行通知时,被通知的线程是由JVM随机选择的.但是 ReentrantLock 集合Condition 类就实现选择性通知。线程可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知

    • synchronized 就相当于整个 Lock 对象中只有一个单一的 Condition 对象,所有的线程都注册在它的一个对象上,线程开始 notifyAll 时,需要通知所有的 waitin线程,没有选择权;Object 中的 wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的。而Condition是需要与"互斥锁"/"共享锁"捆绑使用的

    • 函数列表:

      // 造成当前线程在接到信号或被中断之前一直处于等待状态。
      void await()
      // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
      boolean await(long time, TimeUnit unit)
      // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
      long awaitNanos(long nanosTimeout)
      // 造成当前线程在接到信号之前一直处于等待状态。
      void awaitUninterruptibly()
      // 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
      boolean awaitUntil(Date deadline)
      // 唤醒一个等待线程。
      void signal()
      // 唤醒所有等待线程。
      void signalAll()

    9、LockSupport

    是用来创建锁和其他同步类的基本线程阻塞原语。park()unpark()的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到Thread.suspend 和 Thread.resume所可能引发的死锁问题。因为park()unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。

    每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞

    函数列表:

    // 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
    static Object getBlocker(Thread t)
    // 为了线程调度,禁用当前线程,除非许可可用。
    static void park()
    // 为了线程调度,在许可可用之前禁用当前线程。
    static void park(Object blocker)
    // 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。
    static void parkNanos(long nanos)
    // 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
    static void parkNanos(Object blocker, long nanos)
    // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
    static void parkUntil(long deadline)
    // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
    static void parkUntil(Object blocker, long deadline)
    // 如果给定线程的许可尚不可用,则使其可用。
    static void unpark(Thread thread)

    LockSupport 是通过调用 Unsafe 函数中的接口实现阻塞和解除阻塞的

    park和wait的区别:wait让线程阻塞前,必须通过synchronized获取同步锁; park 面向对象不同; 实现机制不一样,因此两者没有交集;

    10、Callable、Future

    Callable 是类似于 Runnable 的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务;Callable 和 Runnable 有几点不同:

    • Callable规定的方法是call(),而Runnable规定的方法是run();
    • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    • call()方法可抛出异常,而run()方法是不能抛出异常的。
    • 运行 Callable 任务可拿到一个 Future 对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果.

    如果需要获取线程的执行结果,需要使用到Future,Callable用于产生结果,Future用于获取结果,Callabl接口使用泛型来定义结果的返回值类型,在线程池提交Callable任务后返回了一个Future对象

    Future的方法

    // 该方法是非阻塞的
    // 如果任务运行之前调用了该方法,那么任务就不会被运行;
    // 如果任务已经完成或者已经被取消,那么该方法方法不起作用;
    // 如果任务正在运行,并且 cancel 传入参数为 true,那么便会去终止与 Future 关联的任务
    // cancel(false) 与 cancel(true)的区别在于,cancel(false) 只 取消已经提交但还没有被运行的任务(即任务就不会被安排运行);而 cancel(true) 会取消所有已经提交的任务,包括 正在等待的 和 正在运行的 任务
    boolean cancel(boolean mayInterruptIfRunning);
    // 该方法是非阻塞的。在任务结束之前,如果任务被取消了,该方法返回 true,否则返回 false;如果任务已经完成,该方法则一直返回 false
    boolean isCancelled();
    // 该方法同样是非阻塞的。如果任务已经结束(正常结束,或者被取消,或者执行出错),返回 true,否则返回 false
    boolean isDone();
    // 获取任务结果,get方法是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get方法的线程会阻塞,直到任务执行完才会唤醒
    V get() throws InterruptedException, ExecutionException;
    // 获取任务结果,支持超时
    V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;

    11、FutureTask

    public class FutureTask<V> implements RunnableFuture<V>{};
    
    public interface RunnableFuture<V> extends Runnable, Future<V>{};
    • 可用于异步获取执行结果或取消执行任务的场景:通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果。FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等;

    • FutureTask执行多任务计算:利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果

    public class FutureTaskDemo {
        public static void main(String[] args) {
            FutureTaskDemo task = new FutureTaskDemo();
            List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            for (int i =0;i<10;i++){
                FutureTask<Integer> f = new FutureTask<Integer>(task.new ComputeTask(i, "" + i));
                taskList.add(f);
                executorService.submit(f);
            }
            System.out.println("所有计算任务提交完毕,主线程做其他事情");
            Integer total = 0;
            for (FutureTask<Integer> t : taskList){
                try {
                    total = total + t.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
            executorService.shutdown();
            System.out.println("计算任务执行完毕,执行结果:" + total);
    
        }
        private class ComputeTask implements Callable<Integer> {
            private Integer result = 0;
            private String taskName = "";
            public ComputeTask(Integer result, String taskName) {
                this.result = result;
                this.taskName = taskName;
            }
            public String getTaskName() {
                return taskName;
            }
            public Integer call() throws Exception {
                for (int i = 0; i < 100; i++) {
                    result = +i;
                }
                Thread.sleep(5000);
                System.out.println("子线程任务:" + taskName + " 计算完毕");
                return result;
            }
        }
    }
    • 高并发环境下,能够确保任务只执行一次:下面代码保证了在高并发环境下不会多次创建连接或者多次锁的出现
    private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool
                = new ConcurrentHashMap<String, FutureTask<Connection>>();
    public Connection getConnection(String key) throws Exception{
    	FutureTask<Connection> connectionTask = connectionPool.get(key);
    	if (connectionTask != null){
    		return connectionTask.get();
    	}
    	Callable<Connection> callable = new Callable<Connection>() {
    		public Connection call() throws Exception {
    			return createConnection();
    		}
    	};
    	FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
    	connectionTask = connectionPool.putIfAbsent(key, newTask);
    	if (connectionTask == null){
    		connectionTask = newTask;
    		connectionTask.run();
    	}
    	return connectionTask.get();
    }

    12、Fork/Join框架

    12.1、概述

    Fork/Join框架是JDK 1.7提供的一个用于并行执行任务的框架,其核心理念是把一个大任务分割成若干个小任务进行窃取执行,然后最终汇总每个小任务结果后得到大任务结果的并发框架;Fork就是把一个大任务切分为若干子任务进行并行执行,Join就是合并这些子任务的最终执行结果得到这个大任务的结果;

    JDK用来执行Fork/Join任务的工作线程池大小等于CPU核心数;在一个4核CPU上,最多可以同时执行4个子任务

    12.2、算法

    Fork/Join 框架采用了工作窃取(work-stealing)算法来实现,其算法核心是指某个线程从其他队列里窃取任务来执行

    通过这种算法就可以充分利用线程进行并行操作,同时减少了线程间的竞争。但缺点就是在某些情况下还是存在竞争(双端队列里只有一个任务时)且消耗了更多的系统资源(创建多个线程和多个双端队列),可以说是一种空间换时间的优化

    12.3、相关类

    • ForkJoinTask:如果需要使用ForkJoin框架,必须首先创建一个ForkJoin任务,其提供在任务中执行fork()和join()操作机制;通常情况下,不需要直接继承ForkJoinTask,而只需要继承其子类即可。Fork/Join框架提供了以下两个子类:

      • RecursiveTask:用于有返回结果的任务;
      • RecusiveAction:用于没有返回结果的任务

      注意:ForkJoinTask是实现自接口Future的,其为一个抽象类

    • ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部;当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务;比起传统的线程池类ThreadPoolExecutor,ForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率;

      ForkJoin提供了三个方法:

      • execute:异步执行指定任务
      • invoke和invokeAll:执行指定的任务,等待完成返回结果;
      • submit:异步执行指定的任务并立即返回一个Future对象

    12.4、使用

    使用Fork/Join框架基本步骤:

    • 分割任务:首先需要创建一个ForkJoin任务,执行该类的fork方法可以对任务不断切割,直到分割的子任务足够小;
    • 合并任务执行结果:子任务执行的结果同一放在一个队列中,通过启动一个线程从队列中取执行结果。

    使用案例:计算大List的数据之和

    12.5、异常处理

    ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常

    if(task.isCompletedAbnormally()){
        System.out.println(task.getException());
    }

    getException方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null

    12.6、应用场景

    Fork/Join框架适合能够进行拆分再合并的计算密集型(CPU密集型)任务。Fork/Join框架是一个并行框架,因此要求服务器拥有多CPU、多核,用以提高计算能力;

    如果是单核、单CPU,不建议使用该框架,会带来额外的性能开销,反而比单线程的执行效率低。当然不是因为并行的任务会进行频繁的线程切换,因为Fork/Join框架在进行线程池初始化的时候默认线程数量为Runtime.getRuntime().availableProcessors(),单CPU单核的情况下只会产生一个线程,并不会造成线程切换,而是会增加Fork/Join框架的一些队列、池化的开销;

    12.7、注意

    • 除了fork() 和 join()方法外,线程不得使用其他的同步工具。线程最好也不要sleep();
    • 线程不得进行I/O操作;
    • 线程不得抛出checked exception

    13、Exchanger

    是一个用于线程间协作的工具类,用于两个线程之间能够交换。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据;

    具体交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据;

    13.1、主要方法

    //当一个线程执行该方法的时候,会等待另一个线程也执行该方法,因此两个线程就都达到了同步点
    //将数据交换给另一个线程,同时返回获取的数据
    V exchange(V x) throws InterruptedException
    
    //同上一个方法功能基本一样,只不过这个方法同步等待的时候,增加了超时时间
    V exchange(V x, long timeout, TimeUnit unit)throws InterruptedException, TimeoutException 

    13.2、使用例子

    public class ExchangerDemo {
        public static void main(String[] args) {
            List<String> buffer1 = new ArrayList<String>();
            List<String> buffer2 = new ArrayList<String>();
            Exchanger<List<String>> exchanger = new Exchanger<List<String>>();
            Thread producerThread = new Thread(new Producer(buffer1,exchanger));
            Thread consumerThread = new Thread(new Consumer(buffer2,exchanger));
            producerThread.start();
            consumerThread.start();
        }
        static class Producer implements Runnable {
            // 生产者消费者交换的数据结构
            private List<String> buffer;
            // 生产者和消费者的交换对象
            private Exchanger<List<String>> exchanger;
            public Producer(List<String> buffer, Exchanger<List<String>> exchanger) {
                this.buffer = buffer;
                this.exchanger = exchanger;
            }
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("生产者第" + i + "次提供");
                    for (int j = 1; j <= 3; j++) {
                        System.out.println("生产者装入" + i + "--" + j);
                        buffer.add("buffer:" + i + "--" + j);
                    }
                    System.out.println("生产者装满,等待与消费者交换...");
                    try {
                        exchanger.exchange(buffer);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        static class Consumer implements Runnable {
            private List<String> buffer;
            private final Exchanger<List<String>> exchanger;
            public Consumer(List<String> buffer, Exchanger<List<String>> exchanger) {
                this.buffer = buffer;
                this.exchanger = exchanger;
            }
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    //调用exchange()与消费者进行数据交换
                    try {
                        buffer = exchanger.exchange(buffer);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消费者第" + i + "次提取");
                    for (int j = 1; j <= 3; j++) {
                        System.out.println("消费者 : " + buffer.get(0));
                        buffer.remove(0);
                    }
                }
            }
        }
    }

    在Exchanger中,如果一个线程已经到达了exchanger节点时,对于它的伙伴节点的情况有三种:

    • 如果它的伙伴节点在该线程到达之前已经调用了exchanger方法,则它会唤醒它的伙伴然后进行数据交换,得到各自数据返回。
    • 如果它的伙伴节点还没有到达交换点,则该线程将会被挂起,等待它的伙伴节点到达被唤醒,完成数据交换。
    • 如果当前线程被中断了则抛出异常,或者等待超时了,则抛出超时异常。

    13.3、实现分析

    Exchanger算法的核心是通过一个可交换数据的slot,以及一个可以带有数据item的参与者,其文档注释中描述代码如下:

     for (;;) {
    	if (slot is empty) {                       // offer
    		place item in a Node;
    		if (can CAS slot from empty to node) {
    		wait for release;
    		return matching item in node;
    		}
    	}
    	else if (can CAS slot from node to empty) { // release
    		get the item in node;
    		set matching item in node;
    		release waiting thread;
    	}
    	// else retry on CAS failure
    }
    • 成员变量
    private final Participant participant;
    private volatile Node[] arena;
    private volatile Node slot;

    通过数组arena来安排不同的线程使用不同的slot来降低竞争问题,并且可以保证最终一定会成对交换数据。但是Exchanger不是一来就会生成arena数组来降低竞争,只有当产生竞争是才会生成arena数组

    14、Phaser

    15、CompletableFuture

    JDK1.8新增的,任务之间有聚合或者关系,可以使用CompletableFuture来解决。

    16、CompletionService

    批量的并行任务

    四、并发容器

    1、并发容器类

    • List Set:CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentSkipListSet;
    • Map:ConcurrentHashMap、ConcurrentSkipListMap;
    • Queue:ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、ConcurrentLinkedQueue、ConcurrentLinkedDeque;
    • 同步容器与并发容器:同步容器将所有对容器状态的访问都串行化,以实现他们的安全性.这种方法严重降低了并发性。

    2、CopyOnWriteArrayList

    2.1、特性

    相当于线程安全的 ArrayList,和 ArrayList 一样,是个可变数组;不同的是,具有以下几个特性

    • 最适合于应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突;
    • 线程安全的;
    • 通过 锁 + 数组拷贝 + volatile关键字保证线程安全;
    • 因为通常要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大;
    • 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作;即迭代器是只读的;
    • 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照;如果在迭代过程中修改了数据,正在迭代的过程中的数据还是老数据;
    • 元素可以为null;

    2.2、签名

    public class CopyOnWriteArrayList<E>  implements List<E>, RandomAccess, Cloneable, Serializable{}
    • 包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问
    • CopyOnWriteArrayList 本质上通过数组实现的

    2.3、实现原理

    • 动态数组:内部存在一个 volatile 数组来保存数据。在添加/删除/修改数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给 volatile数组。由于它在添加/修改/删除数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高;

    • 线程安全:是通过volatile和互斥锁来(ReentrantLock)实现的

      • CopyOnWriteArrayList 是通过volatile数组来保存数据的;一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入。通过volatile提供了读取到的数据总是最新的这个机制的保证。
      • 通过互斥锁来保护数据。在添加/修改/删除数据时,会先获取互斥锁,再修改完毕之后,先将数据更新到volatile数组中,然后再释放互斥锁

    基本操作步骤时:

    • 加锁
    • 从原数组中拷贝出一份新数组;
    • 在新数组中操作,并把新数组赋值给数组容器;
    • 解锁

    2.4、ArrayList的线程安全集合

    Vector 和 SynchronizedList 都是线程安全的类,Vector是每个方法都有synchronized关键字;SynchronizedList是在方法内部加了synchronized关键字,他们在并发环境下不一定是线程安全的,看如下代码:

    // 得到Vector最后一个元素
    public static Object getLast(Vector list) {
    	int lastIndex = list.size() - 1;
    	return list.get(lastIndex);
    }
    
    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
    	int lastIndex = list.size() - 1;
    	list.remove(lastIndex);
    }

    在一个方法中同时调用上述两个方法,可能发生数组越界异常

    CopyOnWriteArrayList 则不存在这个问题

    2.5、CopyOnWriteList缺点

    • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的。因为每次add()、set()、remove()这些增删改操作都要复制一个数组出来;
    • 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性

    3、CopyOnWriteArraySet:(HashSet)

    • 线程安全的无序的集合,可以将它理解成线程安全的HashSet.CopyOnWriteArraySet 和 HashSet 虽然都继承于共同的父类 AbstractSet;但是HashSet是通过 HashMap 来实现的,而 CopyOnWriteArraySet 是通过 CopyOnWriteArrayList来实现的。特性同 CopyOnWriteArrayList
    • 实现:
      • CopyOnWriteArraySet 继承于 AbstractSet,这就意味着它是一个集合;
      • CopyOnWriteArraySet 包含 CopyOnWriteArrayList 对象,它是通过 CopyOnWriteArrayList 实现的,而CopyOnWriteArrayList中允许有重复的元素,但是,CopyOnWriteArraySet 是一个集合不能有重复元素。CopyOnWriteArrayList 额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作

    4、ConcurrentHashMap:

    ConcurrentHashMap

    5、ConcurrentSkipListMap:(TreeMap)

    5.1、基本描述

    线程安全的有序的哈希表

    • ConcurrentSkipListMap 和 TreeMap,它们虽然都是有序的哈希表;但是 ConcurrentSkipListMap 是线程安全的,TreeMap 是线程不安全的;另外 ConcurrentSkipListMap 是通过跳表来实现的,而 TreeMap 是通过红黑树实现的。
      • 跳表:平衡树的一种替代的数据结构,和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的.
    • TreeMap是基于红黑树实现的,要实现高效的并发是非常困难的,为了保证效率,当我们插入或者删除节点的时,会移动节点进行平衡操作,这导致在高并发场景中难以进行合理粒度的同步;

    5.2、SkipList

    Skip List,称之为跳表,它是一种可以替代平衡树的数据结构,其数据元素默认按照key值升序,天然有序。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率;

    SkipList的特性:

    • 由很多层结构组成,level是通过一定的概率随机产生的;
    • 每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法;
    • 最底层(Level 1)的链表包含所有元素;
    • 如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现
    • 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素;

    其插入和查找的效率O(logn),其效率不低于红黑树,但是其原理和实现的复杂度要比红黑树简单多了;

    5.3、ConcurrentSkipListMap

    ConcurrentSkipListMap其内部采用SkipLis数据结构实现。为了实现SkipList,ConcurrentSkipListMap提供了三个内部类来构建这样的链表结构:Node、Index、HeadIndex。其中Node表示最底层的单链表有序节点、Index表示为基于Node的索引层,HeadIndex用来维护索引层次;

    ConcurrentSkipListMap是通过HeadIndex维护索引层次,通过Index从最上层开始往下层查找,一步一步缩小查询范围,最后到达最底层Node时,就只需要比较很小一部分数据了

    6、ConcurrentSkipListSet: (TreeSet)

    • http://www.cnblogs.com/skywang12345/p/3498556.html

    7、阻塞队列

    7.1、什么是阻塞队列

    是一个在队列基础上又支持了两个附加操作的队列。2个附加操作:

    • 支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满时;
    • 支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空;

    BlockingQueue 是一个接口,继承自 Queue

    7.2、应用场景

    • 常用于生产者与消费者:生产者是向队列中添加元素的线程,消费者是从队列中取元素的线程。简而言之:阻塞队列是生产者用来存放元素、消费者获取元素的容器;

    • 如何使用阻塞队列来实现生产者消费者模型:通知模式-就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用;

    • 为什么BlockingQueue适合解决生产者消费者问题?

      任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题.Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程创建资源,另一个消费资源)。BlockingQueue是一种数据结构,支持一个线程往里存资源,另一个线程从里取资源;

    • BlockingQueue 不接受 null 值的插入,相应的方法在碰到 null 的插入时会抛出 NullPointerException 异常;

    • BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll, retainAll 和 removeAll 不一定是原子操作;

    • BlockingQueue 不支持 close 或 shutdown 等关闭操作

    7.3、几个方法

    方法处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
    插入方法 add(e) offer(e) put(e) offer(e,time,unit)
    移除方法 remove poll() take() poll(time,unit)
    检查方法 element() peek() 不可用 不可用

    这四类方法分别对应的是:

    • ThrowsException:如果操作不能马上进行,则抛出异常
    • SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
    • Blocks:如果操作不能马上进行,操作会被阻塞
    • TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false

    7.4、Java的阻塞队列

    7.4.1、ArrayBlockingQueue

    • ArrayBlockingQueue是典型的有界队列,一个由数组结构组成的有界阻塞队列,内部是final数组保存数据,数组的大小就是队列的边界

    • 此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的,其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作;

    • 看到ArrayBlockingQueue继承AbstractQueue,实现BlockingQueue接口;AbstractQueue在Queue接口中扮演着非常重要的作用,该类提供了对queue操作的骨干实现;BlockingQueue继承java.util.Queue为阻塞队列的核心接口,提供了在多线程环境下的出列、入列操作,作为使用者,则不需要关心队列在什么时候阻塞线程,什么时候唤醒线程,所有一切均由BlockingQueue来完成

    • 其内部是采用一个 ReentrantLock 和相应的两个 Condition 来实现:

      // 用于存放元素的数组
      final Object[] items;
      // 下一次读取操作的位置
      int takeIndex;
      // 下一次写入操作的位置
      int putIndex;
      // 队列中的元素数量
      int count;
      // 以下几个就是控制并发用的同步器
      final ReentrantLock lock;
      private final Condition notEmpty;
      private final Condition notFull;
    • ArrayBlockingQueue 实现并发同步的原理就是,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程

    7.4.2、LinkedBlockingQueue

    • LinkedBlockingQueue其行为和内部实现是基于有界的逻辑实现的,如果在创建的时候没有指定容量,其容量自动设置为Integer.MAX_VALUE,成为了无界队列

      public LinkedBlockingQueue() {
      	this(Integer.MAX_VALUE);
      }
      public LinkedBlockingQueue(int capacity) {
      	if (capacity <= 0) throw new IllegalArgumentException();
      	this.capacity = capacity;
      	last = head = new Node<E>(null);
      }
    • 此队列按照先出先进的原则对元素进行排序

    • 其不同于ArrayBlockingQueue的是,其对于头尾操作时基于不同的锁的;

    • LinkedBlockingQueue在实现“多线程对竞争资源的互斥访问”时,对于“插入”和“取出(删除)”操作分别使用了不同的锁。对于插入操作,通过“插入锁putLock”进行同步;对于取出操作,通过“取出锁takeLock”进行同步。此外,插入锁putLock和“非满条件notFull”相关联,取出锁takeLock和“非空条件notEmpty”相关联。通过notFull和notEmpty更细腻的控制锁

      private final ReentrantLock takeLock = new ReentrantLock(); // 取出锁
      private final Condition notEmpty = takeLock.newCondition(); // 非空条件
      private final ReentrantLock putLock = new ReentrantLock(); // 插入锁
      private final Condition notFull = putLock.newCondition(); // 未满条件

    7.4.3、PriorityBlockingQueue

    • 带排序的 BlockingQueue 实现,其并发控制采用的是 ReentrantLock,支持优先级的无界阻塞队列;PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容;
    • 简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞);
    • 默认情况下元素采用自然顺序升序排序,此类实现了 Collection 和 Iterator 接口中的所有接口方法,对其对象进行迭代并遍历时,不能保证有序性。如果你想要实现有序遍历,建议采用 Arrays.sort(queue.toArray()) 进行处理
    • PriorityBlockingQueue 使用了基于数组的二叉堆来存放元素,所有的 public 方法采用同一个 lock 进行并发控制

    7.4.4、DelayQueue

    • 支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素;

    • DelayQueue主要用于两个方面:(1)缓存:清掉缓存中超时的缓存数据;(2)任务超时处理

    • 在内部用一个PriorityQueue保存所有的Delayed对象,Delayed接口中只有一个方法long getDelay(TimeUnit unit);,返回该任务的deadline距离当前时间还有多久,堆顶保存了最快到期的任务;以支持优先级无界队列的PriorityQueue作为一个容器,容器里面的元素都应该实现Delayed接口,在每次往优先级队列中添加元素时以元素的过期时间作为排序条件,最先过期的元素放在优先级最高

    7.4.5、SynchronousQueue

    SynchronousQueue

    • 不存储元素的阻塞队列,该队列的容量为0,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列
    • SynchronousQueue分为公平和非公平,默认情况下采用非公平性访问策略,当然也可以通过构造函数来设置为公平性访问策略
    • 在JDK6之后,用CAS替换了原本基于锁的逻辑,同步开销比较小;
    • 是Executors.newCachedThreadPool()的默认队列;
    • 不能在 SynchronousQueue 中使用 peek 方法(在这里这个方法直接返回 null),peek 方法的语义是只读取不移除;
    • SynchronousQueue 也不能被迭代,因为根本就没有元素可以拿来迭代的;
    • Transferer 有两个内部实现类,是因为构造 SynchronousQueue 的时候,可以指定公平策略。公平模式意味着,所有的读写线程都遵守先来后到,FIFO 嘛,对应 TransferQueue。而非公平模式则对应 TransferStack;
    • SynchronousQueue非常适合做交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务

    7.4.6、LinkedBlockingDeque

    • 链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争;支持FIFO、FILO两种操作方式
    • LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
    • LinkedBlockingDeque 继承AbstractQueue,实现接口BlockingDeque,而BlockingDeque又继承接口BlockingQueue,BlockingDeque是支持两个附加操作的 Queue,这两个操作是:获取元素时等待双端队列变为非空;存储元素时等待双端队列中的空间变得可用;
    • 通过互斥锁ReentrantLock 来实现,notEmpty 、notFull 两个Condition做协调生产者、消费者问题

    7.5、非阻塞队列

    7.5.1、LinkedTransferQueue

    • 由链表结构组成的FIFO无界阻塞队列TransferQueue队列,相对于其他阻塞队列,多了tryTransfer和transfer方法;
    • LinkedTransferQueue采用一种预占模式:有就直接拿走,没有就占着这个位置直到拿到或者超时或者中断。即消费者线程到队列中取元素时,如果发现队列为空,则会生成一个null节点,然后park住等待生产者。后面如果生产者线程入队时发现有一个null元素节点,这时生产者就不会入列了,直接将元素填充到该节点上,唤醒该节点的线程,被唤醒的消费者线程拿东西走人

    7.5.2、ConcurrentLinkedQueue

    public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializa{
    
    }
    • 是一个基于链接节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用“wait-free”算法(即CAS算法)来实现的
    • 特性:
      • 在入队的最后一个元素的next为null;
      • 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到;
      • 对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点);
      • 允许head和tail更新滞后。即head、tail不总是指向第一个元素和最后一个元素;
    • head的不变性和可变性:
      • 不变性:
        • 所有未删除的节点都可以通过head节点遍历到;
        • head不能为null;
        • head节点的next不能指向自身;
      • 可变性:
        • head的item可能为null,也可能不为null;
        • 允许tail滞后head,也就是说调用succc()方法,从head不可达tail;
    • tail的不变性和可变性:
      • 不变性:tail不能为null
      • 可变性:
        • tail的item可能为null,也可能不为null;
        • tail节点的next域可以指向自身;
        • 允许tail滞后head,也就是说调用succc()方法,从head不可达tail
    • 内部类Node:Node是个单向链表节点,next用于指向下一个Node,item用于存储数据。Node中操作节点数据的API,都是通过Unsafe机制的CAS函数实现的;
    • ConcurrentLinkedQueue就是通过volatile来实现多线程对竞争资源的互斥访问的

    7.5.3、ConcurrentLinkedDeque

    7.6、如何选择队列

    • 考虑应用场景对队列边界的需求:ArrayBlockingQueue是有明确容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue则不缓存任何元素;
    • 从空间利用角度:数组结构ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点;但是ArrayBlockingQueue其初始分配阶段需要一段连续的空间,所以其初始内存需求更大;
    • 通用场景中,LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为其实现了更加细粒度的锁操作;
    • ArrayBlockingQueue实现简单,性能更好预测,稳定;
    • 如果需要实现两个线程之间的接力性,SynchronousQueue是完美符合该场景的,而且线程间协调和数据传输统一起来,代码更加规范;
    • 在元素队列较小的场景下,SynchronousQueue有优异的性能;

    8、三类并发容器比较

    • Concurrent类型是基于lock-free的,一般可以提供高吞吐量;Concurrent没有类似copyonwrite之类容器严重的修改开销;
    • 但是Concurrent往往有较低的遍历一致性,就是所谓的弱一致性,可能发生fail-fast
    • LinkedBlcokingQueue内部是基于锁的,并提供了BlockingQueue的等待方法;

    五、JUC包核心与算法

    1、AQS:AbstractQueuedSynchronizer-抽象队列同步器

    AbstractQueuedSynchronizer

    2、CAS:Compare and Swap-比较与交换

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S1vN4WxX-1580025046162)(image/CAS.png)]

    2.1、什么是CAS

    cpu指令,在大多数处理器架构,包括 IA32,Space 中采用的都是 CAS 指令.

    • CAS 语义:CAS 有3个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false;

    • CAS 是乐观锁技术:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 有3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做.

    • CAS 操作是基于共享数据不会被修改的假设。

    2.2、Java中CAS 的实现

    伪代码:

    do{   
    	备份旧数据;  
    	基于旧数据构造新数据;  
    }while(!CAS(内存地址,备份的旧数据,新数据 ))  

    JDK1.5 之前,需要编写明确的代码来执行CAS操作。在JDK1.5 之后,引入了底层的支持。并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU 不支持CAS指令,那么JVM将使用自旋锁;

    2.2.1、CAS实现的核心类:Unsafe

    • Java 无法直接访问底层操作系统,而是通过本地 native 方法来访问。不过 JVM 还是开了个后门,JDK 中有一个类 Unsafe,它提供了硬件级别的原子操作对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例
    • 对 CAS 的实现:valueOffset为变量值在内存中的偏移地址,unsafe就是通过偏移地址来得到数据的原值的
    /*		
    compareAndSwap方法的参数含义:
    第一个参数:要修改的参数
    第二个参数:对象中要修改变量的偏移量
    第三个参数:修改之前的值
    第四个参数:预想修改后的值
    */
    public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
    // 对象、对象的地址、预期值、修改值
    public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
    public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
    • 可以查看原子类的实现,比如:AtomicInteger#addAndGet 方法的实现:
      // JDK7:在addAndGet作一部分操作,然后调用compareAndSet,由该方法调用 Unsafe#getAndAddInt
      public final int addAndGet(int delta) {
      	for (;;) {
      		int current = get();
      		int next = current + delta;
      		if (compareAndSet(current, next))
      			return next;
      	}
      }
      public final boolean compareAndSet(int expect, int update) {
      	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
      }
      // JDK8:直接调用 Unsafe#getAndAddInt
      public final int addAndGet(int delta) {
      	return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
      }

    2.2.2、Unsafe 方法实现:使用 C++ 来实现的

    注意:对应于windows操作系统,X86 处理器,sun.misc.Unsafe 类的compareAndSwapInt()方法的源代码:
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);;该方法是本地方法,这个本地方法在openjdk中依次调用的c++代码主要有三个文件:openjdk/openjdk/hotspot/src/share/vm/prims/unsafe.cppopenjdk/openjdk/hotspot/src/share/vm/runtime/atomic.cppopenjdk/openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp

    对应部分源码片段:

    inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
    // alternative for InterlockedCompareExchange
    	int mp = os::is_MP();
    		__asm {
    		mov edx, dest
    		mov ecx, exchange_value
    		mov eax, compare_value
    		LOCK_IF_MP(mp)
    		cmpxchg dword ptr [edx], ecx
    	}
    }
    
    

    CPU提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁

    • 如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)
    • lock前缀说明:
      • Ⅰ、确保对内存的读-改-写操作原子执行
      • Ⅱ、禁止该指令与之前和之后的读和写指令重排序
      • Ⅲ、把写缓冲区中的所有数据刷新到内存中

    2.3、CAS 使用场景

    • 原子类的实现
    • AbstractQueuedSynchronizer(AQS)

    2.4、CAS 缺点

    2.4.1、ABA 问题

    • 问题:在运用CAS做Lock-Free 操作中有一个经典的ABA问题。

      线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题;

    • 解决思路是:每次变量更新的时候把变量的版本号加 1,那么 A-B-A 就会变成 A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化.

      //	可以参考:AtomicStampedReference#compareAndSet 方法:AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp
      public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
      	Pair<V> current = pair;
      	return expectedReference == current.reference && expectedStamp == current.stamp &&
      			((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp)));
      }

      该类检查了当前引用与当前标志是否与预期相同,如果全部相等,才会以原子方式将该引用和该标志的值设为新的更新值

    2.4.2、CPU 开销较大

    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力;主要是自旋CAS操作如果长时间不成功,会给CPU带来非常大的执行开销。

    2.4.3、不能保证代码块的原子性

    CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新.就不得不使用 synchronized;

    2.5、CAS 与 synchronized 的区别

    • synchronized 关键字会让没有得到锁资源的线程进入 BLOCKED 状态,而后在争夺到锁资源后恢复为 RUNNABLE 状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高;尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

    • 从锁的分类来看,CAS 属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新;而 synchronized 属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守;

    • 两者没有绝对的好坏,关键看使用场景.在1.6版本后,synchronized 变为重量级锁之前也是采用 CAS 机制;

    • 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用

    2.6、JDK8对CAS的优化

    在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,没有此方法

    • 1.8在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add;
    • 如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap;

    六、线程池

    1、概要

    1.1、为什么使用线程池

    • 在多线程技术中,线程的创建和销毁很消耗时间,因为创建线程需要获取内存资源或者其他更多的资源。提高效率就是减少线程的创建和销毁次数。可以利用已有线程来解决这个问题,这就是池化技术产生的原因.就如同数据库连接池一样;
    • 线程池真正关注的点:如何缩短创建线程的时间和销毁线程的时间;
    • 可有效的控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞;
    • 提供定时执行\定期执行\单线程\并发控制数等功能;

    1.2、什么是线程池

    线程池是一种多线程处理方法,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。

    1.3、应用范围

    • 需要大量线程来完成的任务,且完成任务时间较短,如web服务完成网页请求这样的任务.但是对于长时间的任务,比如一个ftp连接请求;
    • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求;
    • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用;

    1.4、如何设计一个线程池

    • 基本组成部分

      • 线程池管理器:用于创建并管理线程池,包括创建线程池、销毁线程池、添加新任务等功能;
      • 工作线程:线程池中的线程;
      • 任务接口:每个任务必须实现的接口,以供工作线程调度任务执行;
      • 任务队列:用于存放没有处理的任务,提供一种缓存机制;

    1.5、线程池原理

    预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭.如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已。如此反复。线程池的实现类是 ThreadPoolExecutor 类;

    核心工作线程值在初始的时候被创建,当新任务来到的时候被启动,但是我们可以通过重写 prestartCoreThread 或 prestartCoreThreads 方法来改变这种行为。通常场景我们可以在应用启动的时候来 WarmUp 核心线程,从而达到任务过来能够立马执行的结果,使得初始任务处理的时间得到一定优化

    2、JDK线程池结构

    Executor框架结构:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-enfwe4Lp-1580025046163)(image/Executor.png)]

    • Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,其只有一个方法:
      void execute(Runnable command);
    • ExecutorService不仅提供Service管理功能,如shutdown等方法,也提供了更加全面的提交任务机制,如返回Future,其解决了Runnable无法返回结果的困扰;
    • AbstractExecutorService,其实现了非常有用的一些方法供子类直接使用
    • Java提供了几种基础实现:ThreadPoolExecutor、ScheduleThreadPoolExecutor、ForkJoinPool;
    • Executors提供了各种方便的静态工厂方法;
    • 由于线程池支持获取线程执行的结果,所以,引入了Future接口,RunnableFuture继承自此接口,其主要实现类是FutureTask;在线程池的使用过程中,我们是往线程池提交任务(task),使用过线程池的都知道,我们提交的每个任务是实现了 Runnable 接口的,其实就是先将 Runnable 的任务包装成 FutureTask,然后再提交到线程池

    2.1、Executor

    public interface Executor {
        void execute(Runnable command);
    }

    如果我们希望线程池同步执行每一个任务,我们可以这么实现这个接口:

    class DirectExecutor implements Executor {
        public void execute(Runnable r) {
            r.run();// 这里不是用的new Thread(r).start(),也就是说没有启动任何一个新的线程。
        }
    }

    我们希望每个任务提交进来后,直接启动一个新的线程来执行这个任务,我们可以这么实现:

    class ThreadPerTaskExecutor implements Executor {
        public void execute(Runnable r) {
            new Thread(r).start();  // 每个任务都用一个新的线程来执行
        }
    }

    Executor是基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者;

    2.2、ExecutorService-真正的线程池接口

    这个接口继承自Executor,主要是添加了一些线程池生命周期的管理方法;

    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)throws InterruptedException;

    ExecutorService的生命周期有三种状态:运行、关闭、终止。

    • ExecutorService在创建时处于运行状态;
    • shutdown方法将执行平滑的关闭过程:不再接受新的任务,同时等待已久提交的任务执行完成,包括那些还未开始执行的任务。
    • shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务;

    在ExecutorService关闭后提交的任务将由拒绝策略来进行处理;

    2.3、ThreadPoolExecutor

    ExecutorService的默认实现,线程池中最核心的一个类

    2.4、ScheduledThreadPoolExecutor

    Timer/TimerTask存在问题:

    • Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor只支持基于相对时间的调度;
    • Timer在执行所有定时任务时只会创建一个线程。如果某个任务执行时间过长,那么将破坏其他TimerTask的定时精确性;
    • Timer创建的线程没有处理异常,因此一旦抛出非受检异常,该线程会立即终止,也不会恢复线程的运行;

    ScheduledExecutorService:和Timer/TimerTask类似,解决那些需要任务重复执行的问题

    • 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现;提供了“延迟”“周期执行”功能的ThreadPoolExecutor;
    • 一旦启用已延迟的任务就执行它,但是有关何时启用,启用后何时执行则没有任何实时保证。按照提交的先进先出 (FIFO) 顺序来启用那些被安排在同一执行时间的任务;
    • 一般通过Executors.newScheduledThreadPool(int);来构造一个ScheduledThreadPoolExecutor对象;
    • 它所使用的阻塞队列变成了DelayedWorkQueue,而不是ThreadLocalhExecutor的LinkedBlockingQueue;
    • DelayedWorkQueue为ScheduledThreadPoolExecutor中的内部类,它其实和阻塞队列DelayQueue有点儿类似。DelayQueue是可以提供延迟的阻塞队列,它只有在延迟期满时才能从中提取元素,其列头是延迟期满后保存时间最长的Delayed元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null

    2.5、Executors创建线程池

    Executors 提供了5种不同的线程池创建方式

    • newCachedThreadPool():用来处理大量短时间工作任务的线程池,其内部使用 SynchronousQueue作为工作队列具有以下几个特点:

      • 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
      • 如果线程限制的时间超过60秒,则会被终止并移出缓存;
      • 长时间闲置时,这种线程池,不会消耗什么资源;
    • newFixedThreadPool(int nThreads):重用指定数目的线程,其背后使用的是无界工作队列,任何时候最大只有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nthreads;

    • newSingleThreadExecutor():其她点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务都是被顺序执行的,最大会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;

    • newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;

    • newWorkStealingPool(),Java8加入的方法,其内部会构建ForkJoinPool,利用work-stealing算法,并行的处理任务,不保证处理顺序

    为什么不建议使用JDK自身提供的构建线程池的方式?

    使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出)。因为这些工程方法中都没有指定阻塞队列的容量,没有指定的话默认容量是Integer.MAX_VALUE,那么阻塞队列就是个无界队列,而创建这么多线程,必然会导致OOM;

    • newFixedThreadPool
      public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
      	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
      }
      Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
      	at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
      	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
      	at com.learning.example.thread.pool.ExecutorsDemo.main(ExecutorsDemo.java:17)
      

    真正的导致OOM的其实是LinkedBlockingQueue.offer

    • ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
    • LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE

    除了调用ThreadPoolExecutor构造器之外,还可以使用Apache和Guava来使用线程池,可以使用guava的ThreadFactoryBuilder

    3、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;
    }

    3.1、核心参数

    • corePoolSize:核心线程数大小,当线程数 < corePoolSize,会创建线程执行runnable;如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁;
    • maximumPoolSize:最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中;largestPoolSize:记录了曾经出现的最大线程个数;如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中;如果maximumPoolSize=corePoolSize,即是固定大小线程池;
    • keepAliveTime:保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime才起作用。但是当 ThreadPoolExecutor的 allowCoreThreadTimeOut=true时,核心线程超时后也会被回收。
    • unit:时间单位
    • workQueue:保存任务的阻塞队列;当请求的线程数大于 corePoolSize 时,线程进入 BlockingQueue。后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性;两个锁分别控制元素的添加和获取,是一个生产消费模型队列;
    • threadFactory:创建线程的工厂;线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的
    • handler:拒绝策略,默认有四种拒绝策略;当超过参数 workQueue的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护
    • workers:保持工作线程的集合,线程的工作线程被抽象为静态内部类,是基于AQS实现的,线程池底层的存储结构其实就是一个HashSet

    3.2、参数关系

    • corePoolSize 与 maximumPoolSize:

      • 如果线程池中的实际线程数 < corePoolSize, 新增一个线程处理新的任务;
      • 如果线程池中的实际线程数 >= corePoolSize, 新任务会放到workQueue中;
      • 如果阻塞队列达到上限,且当前线程池的实际线程数 < maximumPoolSize,新增线程来处理任务;
      • 如果阻塞队列满了,且这时线程池的实际线程数 >= maximumPoolSize,那么线程池已经达到极限,会根据拒绝策略RejectedExecutionHandler拒绝新的任务。
    • 如果线程池阻塞队列达到极限时,在运行一段时间后,阻塞队列中的任务执行完成了,线程池会将超过核心线程数的线程在一段时间内自动回收,在秒杀的业务场景中会有这样的情况发生。

    3.3、线程池状态

    采用一个 32 位的整数来存放线程池的状态和当前池中的线程数,其中高 3 位用于存放线程池状态,低 29 位表示线程数

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    // 这里 COUNT_BITS 设置为 29(32-3),意味着前三位用于存放线程状态,后29位用于存放线程数
    // 很多初学者很喜欢在自己的代码中写很多 29 这种数字,或者某个特殊的字符串,然后分布在各个地方,这是非常糟糕的
    private static final int COUNT_BITS = Integer.SIZE - 3;
    
    // 000 11111111111111111111111111111
    // 这里得到的是 29 个 1,也就是说线程池的最大线程数是 2^29-1=536870911
    // 以我们现在计算机的实际情况,这个数量还是够用的
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
    // 我们说了,线程池的状态存放在高 3 位中
    // 运算结果为 111跟29个0:111 00000000000000000000000000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    // 000 00000000000000000000000000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    // 001 00000000000000000000000000000
    private static final int STOP       =  1 << COUNT_BITS;
    // 010 00000000000000000000000000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    // 011 00000000000000000000000000000
    private static final int TERMINATED =  3 << COUNT_BITS;
    
    // 将整数 c 的低 29 位修改为 0,就得到了线程池的状态
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    // 将整数 c 的高 3 为修改为 0,就得到了线程池中的线程数
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     */
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }
    • RUNNING:这个没什么好说的,这是最正常的状态:接受新的任务,处理等待队列中的任务
    • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
    • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
    • TIDYING:所有的任务都销毁了,workCount 为 0。线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()
    • TERMINATED:terminated() 方法结束后,线程池的状态就会变成这个

    RUNNING 定义为 -1,SHUTDOWN 定义为 0,其他的都比 0 大,所以等于 0 的时候不能提交任务,大于 0 的话,连正在执行的任务也需要中断

    3.4、任务执行顺序

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3Ato65C-1580025046164)(image/线程池主要处理流程.png)]

    详细流程:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZTGPdeFl-1580025046164)(image/ThreadPool-execute.png)]

    execute方法执行机制:

    • 一个任务提交,如果线程池大小没达到corePoolSize,则每次都启动一个worker也就是一个线程来立即执行;(执行这个步骤时需要获取全局锁)
    • 如果来不及执行,则把多余的线程放到workQueue,等待已启动的worker来循环执行;
    • 如果队列workQueue都放满了还没有执行,则在maximumPoolSize下面启动新的worker来循环执行workQueue;
    • 如果启动到maximumPoolSize还有任务进来,线程池已达到满负载,此时就执行任务拒绝RejectedExecutionHandler;
    • 线程池核心代码:
      public void execute(Runnable command) {
      	if (command == null)
      		throw new NullPointerException();
      	int c = ctl.get();
      	// 判断当前线程数是否小于 corePoolSize,如果是,使用入参任务通过 addWork方法创建一个新的线程.
      	// 如果能完成新线程的创建execute方法结束,成果提交任务.
      	if (workerCountOf(c) < corePoolSize) {
      		if (addWorker(command, true))// true表示会再次检查workCount是否小于corePoolSize
      			return;
      		c = ctl.get();
      	}
      	// 如果上面没有完成任务提交;状态为运行并且能发成功加入任务到工作队列后,在进行一次check,如果状态在任务
      	// 加入了任务队列后变为非运行(可能线程池被关闭了),非运行状态下当然需要reject;
      	// 然后在判断当前线程数是否为0,如果是,新增一个线程;
      	if (isRunning(c) && workQueue.offer(command)) {
      		int recheck = ctl.get();
      		if (! isRunning(recheck) && remove(command))
      			reject(command);
      		else if (workerCountOf(recheck) == 0)
      			addWorker(null, false);
      	}
      	// 如果任务不能加入到工作队列,将尝试使用任务增加一个线程,如果失败,则是线程池已经shutdown或者线程池已经
      	// 达到饱和状态,所以reject这个任务.
      	else if (!addWorker(command, false))
      		reject(command);
      }
      
      // ctl变量有双重角色,通过高低位的不同,既表示线程池状态,又表示工厂线程数目
      private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    submit()方法执行机制:

    public Future<?> submit(Runnable task) {
    	if (task == null) throw new NullPointerException();
    	RunnableFuture<Void> ftask = newTaskFor(task, null);
    	execute(ftask);
    	return ftask;
    }
    • submit 返回一个 Future 对象,我们可以调用其 get 方法获取任务执行的结果;就是将 Runnable 包装成 FutureTask 而已。可以看到,最终还是调用 Execute 方法

    3.5、新任务添加到队列

    线程池使用 addWorker 方法新建线程,第一个参数代表要执行的任务,线程会将这个任务执行完毕后再从队列取任务执行。第二参数是核心线程的标志,它并不是 Worker 本身的属性,在这里只用来判断工作线程数量是否超标;

    第一部分进行一些前置判断,并使用循环 CAS 结构将线程数量加1。代码如下

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry: //这个语法不常用,用于给外层 for 循环命名。方便嵌套 for 循环中,break 和 continue 指定是外层还是内层循环
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            
            // firstTask 不为空代表这个方法用于添加任务,为空代表新建线程。SHUTDOWN 状态下不接受新任务,但处理队列中的任务。这就是第二个判断的逻辑。
            if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
            
            // 使用循环 CAS 自旋,增加线程数量直到成功为止
            for (;;) {
            int wc = workerCountOf(c);
            //判断是否超过线程容量
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //使用 CAS 将线程数量加1
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //修改不成功说明线程数量有变化
            //重新判断线程池状态,有变化时跳到外层循环重新获取线程池状态
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            //到这里说明状态没有变化,重新尝试增加线程数量
            }
        }
        ... ...
    }

    第二部分负责新建并启动线程,并将 Worker 添加至 Hashset 中。代码很简单,没什么好注释的,用了 ReentrantLock 确保线程安全

    下面主要是不同队列策略表现:

    • 直接递交:一种比较好的默认选择是使用 SynchronousQueue,这种策略会将提交的任务直接传送给工作线程,而不持有。如果当前没有工作线程来处理,即任务放入队列失败,则根据线程池的实现,会引发新的工作线程创建,因此新提交的任务会被处理。这种策略在当提交的一批任务之间有依赖关系的时候避免了锁竞争消耗。值得一提的是,这种策略最好是配合 unbounded 线程数来使用,从而避免任务被拒绝。同时我们必须要考虑到一种场景,当任务到来的速度大于任务处理的速度,将会引起无限制的线程数不断的增加。

    • 无界队列:使用无界队列如 LinkedBlockingQueue 没有指定最大容量的时候,将会引起当核心线程都在忙的时候,新的任务被放在队列上,因此,永远不会有大于 corePoolSize 的线程被创建,因此 maximumPoolSize 参数将失效。这种策略比较适合所有的任务都不相互依赖,独立执行。举个例子,如网页服务器中,每个线程独立处理请求。但是当任务处理速度小于任务进入速度的时候会引起队列的无限膨胀。

    • 有界队列:有界队列如 ArrayBlockingQueue 帮助限制资源的消耗,但是不容易控制。队列长度和 maximumPoolSize 这两个值会相互影响,使用大的队列和小 maximumPoolSize 会减少 CPU 的使用、操作系统资源、上下文切换的消耗,但是会降低吞吐量,如果任务被频繁的阻塞如IO线程,系统其实可以调度更多的线程。使用小的队列通常需要大 maximumPoolSize,从而使得 CPU更忙一些,但是又会增加降低吞吐量的线程调度的消耗。总结一下是 IO 密集型可以考虑多些线程来平衡 CPU 的使用,CPU 密集型可以考虑少些线程减少线程调度的消耗

    3.6、worker

    Worker 本身并不区分核心线程和非核心线程,核心线程只是概念模型上的叫法,特性是依靠对线程数量的判断来实现的

    • 继承自 AQS,本身实现了一个最简单的不公平的不可重入锁
    • 构造方法传入 Runnable,代表第一个执行的任务,可以为空。构造方法中新建一个线程;构造函数主要是做三件事:
      • 设置同步状态state为-1,同步状态大于0表示就已经获取了锁;
      • 设置将当前任务task设置为firstTask;
      • 利用Worker本身对象this和ThreadFactory创建线程对象。
    • 实现了 Runnable 接口,在新建线程时传入 this。因此线程启动时,会执行 Worker 本身的 run 方法;
    • run 方法调用了 ThreadPoolExecutor 的 runWorker 方法,负责实际执行任务

    3.7、拒绝策略

    public interface RejectedExecutionHandler {
        void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }

    3.7.1、JDK实现的拒绝策略

    四种策略都是静态内部类,在默认情况下,ThreadPoolExecutor使用抛弃策略:private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

    • CallerRunsPolicy:调用者运行策略

      在线程池没有关闭(调用shut Down)的情况下,直接由调用线程来执行该任务。当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理

      使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了

    • AbortPolicy:当触发拒绝策略时,直接抛出拒绝执行的异常RejectedExecutionException,中止策略的意思也就是打断当前执行流程。Tomcat中的拒绝策略也是类似的

    • DiscardPolicy:直接丢弃该任务,什么也不做。

    • DiscardOldestPolicy:在线程池没有关闭(调用shutDown)的情况下,丢弃线程池任务队列中等待最久-即队列首部的任务,并尝试直接执行该触发饱和策略的任务;

    3.7.2、第三方拒绝策略

    1、Dubbo中实现的拒绝策略:

    // 继承自线程池默认的拒绝策略
    public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
        protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
        private final String threadName;
        private final URL url;
        private static volatile long lastPrintTime = 0;
        private static Semaphore guard = new Semaphore(1);
        public AbortPolicyWithReport(String threadName, URL url) {
            this.threadName = threadName;
            this.url = url;
        }
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            String msg = String.format("Thread pool is EXHAUSTED!" +
                            " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                            " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
                    threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                    e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                    url.getProtocol(), url.getIp(), url.getPort());
            logger.warn(msg);
            dumpJStack();
            throw new RejectedExecutionException(msg);
        }
        private void dumpJStack() {
           //省略实现
        }
    }

    当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因

    • 输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在
    • 输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
    • 继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性;

    2、Netty中的线程池拒绝策略:

    private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        NewThreadRunsPolicy() {
            super();
        }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException(
                        "Failed to start a new thread", e);
            }
        }
    }

    Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常;

    3、ActiveMq中的线程池拒绝策略:

    new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
            try {
                executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
            }
    
            throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
        }
    });

    ActiveMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常

    4、pinpoint中的线程池拒绝策略:

    public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
        private final RejectedExecutionHandler[] handlerChain;
        public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
            Objects.requireNonNull(chain, "handlerChain must not be null");
            RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]);
            return new RejectedExecutionHandlerChain(handlerChain);
        }
        private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
            this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");
        }
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) {
                rejectedExecutionHandler.rejectedExecution(r, executor);
            }
        }
    }

    pinpoint的拒绝策略实现很有特点,其定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍

    3.8、设置线程池线程的名称

    线程池中的线程是通过线程工厂来创建的即ThreadFactory,那么设置线程名称也是通过其实现的,ThreadPoolExecutor默认使用的线程工厂是Executors.DefaultThreadFactory

    static class DefaultThreadFactory implements ThreadFactory {
    	// poolNumber是static的原子变量用来记录当前线程池的编号是应用级别的,所有线程池公用一个,比如创建第一个线程池时候线程池编号为1,创建第二个线程池时候线程池的编号为2,这里pool-1-thread-1里面的pool-1中的1就是这个值
    	private static final AtomicInteger poolNumber = new AtomicInteger(1);
    	private final ThreadGroup group;
    	// threadNumber是线程池级别的,每个线程池有一个该变量用来记录该线程池中线程的编号,这里pool-1-thread-1里面的thread-1中的1就是这个值
    	private final AtomicInteger threadNumber = new AtomicInteger(1);
    	// namePrefix是线程池中线程的前缀,默认固定为pool
    	private final String namePrefix;
    
    	DefaultThreadFactory() {
    		SecurityManager s = System.getSecurityManager();
    		group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
    		namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    	}
    
    	public Thread newThread(Runnable r) {
    		// 具体创建线程,可知线程的名称使用namePrefix + threadNumber.getAndIncrement()拼接的
    		Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(),0);
    		if (t.isDaemon())
    			t.setDaemon(false);
    		if (t.getPriority() != Thread.NORM_PRIORITY)
    			t.setPriority(Thread.NORM_PRIORITY);
    		return t;
    	}
    }

    那么,如果需要自定义名称的话,可以手动实现一个ThreadFactory

    // 命名线程工厂
    static class NamedThreadFactory implements ThreadFactory {
    	private static final AtomicInteger poolNumber = new AtomicInteger(1);
    	private final ThreadGroup group;
    	private final AtomicInteger threadNumber = new AtomicInteger(1);
    	private final String namePrefix;
    
    	NamedThreadFactory(String name) {
    		SecurityManager s = System.getSecurityManager();
    		group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
    		if (null == name || name.isEmpty()) {
    			name = "pool";
    		}
    		namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
    	}
    
    	public Thread newThread(Runnable r) {
    		Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
    		if (t.isDaemon())
    			t.setDaemon(false);
    		if (t.getPriority() != Thread.NORM_PRIORITY)
    			t.setPriority(Thread.NORM_PRIORITY);
    		return t;
    	}
    }
    // 创建线程池的时候可以使用如下
    static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-ACCEPT-POOL"));
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-PROCESS-POOL"));

    3.9、空闲线程回收

    如果当前池子中的工作线程数大于 corePoolSize,如果超过这个数字的线程处于空闲的时间大于 keepAliveTime,则这些线程将会被终止,这是一种减少不必要资源消耗的策略。这个参数可以在运行时被改变,我们同样可以将这种策略应用给核心线程,我们可以通过调用 allowCoreThreadTimeout 来实现

    3.10、线程池异常处理

    1、使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理

    利用Future.get得到任务抛出的异常的缺点在于,我们需要显式的遍历Future,调用get方法获取每个任务执行抛出的异常,然后处理

    2、重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常:

    protected void afterExecute(Runnable r, Throwable t) { }

    3、为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常

    当一个线程因为未捕获的异常而退出时,JVM会把这个事件报告给应用提供的UncaughtExceptionHandler异常处理器,如果没有提供任何的异常处理器,那么默认的行为就是将堆栈信息输送到System.err;

    注意,这个方案不适用于使用submit方式提交任务的情况,原因是:FutureTask的run方法捕获异常后保存,不再重新抛出,意味着runWorker方法并不会捕获到抛出的异常,线程也就不会退出,也不会执行我们设置的UncaughtExceptionHandler,只能在execute.execute()使用

    如何为工作者线程设置UncaughtExceptionHandler呢?ThreadPoolExecutor的构造函数提供一个ThreadFactory,可以在其中设置我们自定义的UncaughtExceptionHandler,这里不再赘述。

    4、在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常

    这种方法比较简单,也有他的局限性,不够灵活,我们的处理被局限在了线程代码边界之内

    3.11、获取线程执行结果

    // 其参数Runnable,该接口的run方法是没有返回值的,所以这个方法的返回的Future仅可以用来断言任务已经结束;
    public Future<?> submit(Runnable task) {
    	if (task == null) throw new NullPointerException();
    	RunnableFuture<Void> ftask = newTaskFor(task, null);
    	execute(ftask);
    	return ftask;
    }
    // 假设这个方法返回的Future对象是f,f.get的返回值就是传给submit方法的参数result
    public <T> Future<T> submit(Runnable task, T result) {
    	if (task == null) throw new NullPointerException();
    	RunnableFuture<T> ftask = newTaskFor(task, result);
    	execute(ftask);
    	return ftask;
    }
    // Callable接口的call方法有返回值,可以通过Future的get获取任务的执行结果
    public <T> Future<T> submit(Callable<T> task) {
    	if (task == null) throw new NullPointerException();
    	RunnableFuture<T> ftask = newTaskFor(task);
    	execute(ftask);
    	return ftask;
    }

    3.12、线程池任务取消

    通过Future取消线程池中的任务

    // 该方法是非阻塞的
    // 如果任务运行之前调用了该方法,那么任务就不会被运行;
    // 如果任务已经完成或者已经被取消,那么该方法方法不起作用;
    // 如果任务正在运行,并且 cancel 传入参数为 true,那么便会去终止与 Future 关联的任务
    // cancel(false) 与 cancel(true)的区别在于,cancel(false) 只 取消已经提交但还没有被运行的任务(即任务就不会被安排运行);而 cancel(true) 会取消所有已经提交的任务,包括 正在等待的 和 正在运行的 任务
    boolean cancel(boolean mayInterruptIfRunning);
    // 该方法是非阻塞的。在任务结束之前,如果任务被取消了,该方法返回 true,否则返回 false;如果任务已经完成,该方法则一直返回 false
    boolean isCancelled();
    // 该方法同样是非阻塞的。如果任务已经结束(正常结束,或者被取消,或者执行出错),返回 true,否则返回 false
    boolean isDone();

    当任务被取消时,Future 的 get 方法抛出了 CancellationException 异常,并且成功的取消了任务;

    虽然取消了任务,Future 的 get 方法也对我们的取消做出了响应(即抛出 CancellationException 异常),但是任务并没有停止,而是直到任务运行完毕了,程序才结束;
    FutureTask中cancel的实现中:cancel(true) 方法的原理是向正在运行任务的线程发送中断指令 —— 即调用运行任务的 Thread 的 interrupt() 方法。 如果一个任务是可取消的,那么它应该可以对 Thread 的 interrupt() 方法做出被取消时的响应。

    public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
                UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                    mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

    如果要通过 Future 的 cancel 方法取消正在运行的任务,那么该任务必定是可以对线程中断做出响应 的任务。通过 Thread.currentThread().isInterrupted() 方法,我们可以判断任务是否被取消,从而做出相应的取消任务的响应

    3.13、线程池关闭

    • shutdown:

      public void shutdown() {
          final ReentrantLock mainLock = this.mainLock;
          mainLock.lock();
          try {
              checkShutdownAccess();
              advanceRunState(SHUTDOWN); // 原子性的修改线程池的状态为SHUTDOWN状态
              interruptIdleWorkers();// 中断空闲的线程
              onShutdown(); // hook for ScheduledThreadPoolExecutor
          } finally {
              mainLock.unlock();
          }
          tryTerminate();
      }
      • 如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。
      • 如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出;
      • 可以通过isShutdown方法判断当前线程是否停止了;
      • isTerminated 是否线程池中断;
      • awaitTermination 等待中断
    • shutdownNow执行逻辑:将线程池状态修改为STOP,然后调用线程池里的所有线程的interrupt方法

      public List<Runnable> shutdownNow() {
          List<Runnable> tasks;
          final ReentrantLock mainLock = this.mainLock;
          mainLock.lock();
          try {
              checkShutdownAccess();
              advanceRunState(STOP);// 原子性的修改线程池的状态为STOP状态
              interruptWorkers();// 遍历线程池里的所有工作线程,然后调用线程的interrupt方法
              tasks = drainQueue();// 将队列里还没有执行的任务放到列表里,返回给调用方
          } finally {
              mainLock.unlock();
          }
          tryTerminate();
          return tasks;
      }

      当我们调用线程池的shutdownNow时:

      • 如果线程正在getTask方法中执行,则会通过for循环进入到if语句,于是getTask返回null。从而线程退出。不管线程池里是否有未完成的任务。
      • 如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错(如果任务里没有捕获InterruptedException异常),否则线程会执行完当前任务,然后通过getTask方法返回为null来退出;
      • 该方法会返回被中断的线程

    3.14、Hook

    ThreadPoolExecutor 提供了 protected 类型可以被覆盖的钩子方法,允许用户在任务执行之前会执行之后做一些事情。我们可以通过它来实现比如初始化 ThreadLocal、收集统计信息、如记录日志等操作。这类 Hook 如 beforeExecute 和 afterExecute。另外还有一个 Hook 可以用来在任务被执行完的时候让用户插入逻辑,如 rerminated。如果 hook 方法执行失败,则内部的工作线程的执行将会失败或被中断;另外还可以通过hook来暂停线程或恢复线程

    3.15、总结

    • 所谓线程池本质是一个hashSet。多余的任务会放在阻塞队列中
    • 线程池提供了两个钩子(beforeExecute,afterExecute)给我们,我们继承线程池,在执行任务前后做一些事情
    • 线程池原理关键技术:锁(lock,cas)、阻塞队列、hashSet(资源池)
    • 使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题

    4、线程池配置

    4.1、不同业务场景如何配置线程池参数

    • CPU密集型任务:需要尽量压榨CPU,参考值可以设为NCPU + 1;
    • IO密集型任务:参考值可以设置为 2*NCPU;

    4.2、科学设置线程池

    • 如果需要达到某个QPS,使用如下计算公式:

      设置的线程数 = 目标QPS / (1 / 任务实际处理时间)

      假设目标QPS=100, 任务的实际处理时间 0.2s, 100 * 0.2 = 20个线程,这里的20个线程必须对应物理的20个CPU核心,

      否则不能达到预估的QPS目标。

    • 如果IO任务较多,使用阿姆达尔定律来计算:

      设置的线程数 = CPU 核数 * (1 + io/computing)

      假设4核 CPU,每个任务中的 IO 任务占总任务的80%, 4 * (1 + 4) = 20个线程,这里的20个线程对应的是4核心的 CPU。

    • 线程队列大小的设置:按照目标响应时间计算队列大小

      队列大小 = 线程数 * (目标相应时间/任务实际处理时间)

      假设目标相应时间为0.4s,计算阻塞队列的长度为20 * (0.4 / 0.2) = 40。

    5、线程池最佳实践

    • 线程池的使用要考虑线程最大数量和最小数最小数量,避免任务堆积
    • 对于单部的服务,线程的最大数量应该等于线程的最小数量,而混布的服务,适当的拉开最大最小数量的差距,能够整体调整CPU内核的利用率.
    • 线程队列大小一定要设置有界队列,否则压力过大就会拖垮整个服务,避免过度扩展线程池
    • 必要时才使用线程池,须进行设计性能评估和压测.
    • 须考虑线程池的失败策略,失败后的补偿.
    • 后台批处理服务须与线上面向用户的服务进行分离.
    • 避免在线程池中使用ThreadLocal,因为可能存在造成数据混乱的情况

    七、多线程并发最佳实践

    • 1、使用本地变量;
    • 2、使用不可变类;
    • 3、最小化锁的作用域范围:S = 1 / (1 - a + a/n)
    • 4、使用线程池,而不是直接new Thread执行;
    • 5、宁可使用同步也不要使用线程的wait和notify(可以使用CountDownLatch);
    • 6、使用BlockingQueue实现生产-消费模式;
    • 7、使用并发集合而不是加了锁的同步集合;
    • 8、使用Semaphore创建有界的访问;
    • 9、宁可使用同步代码块也不要使用同步方法(synchronized);
    • 10、避免使用静态变量,如果一定要用静态变量,可以声明为 final;

    八、生产者与消费者

    1、问题描述

    在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题

    • 生产者消费者模式
      在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯;

    • 解决生产者\消费者问题的方法可以分为两类:

      • 采用某种机制保护生产者与消费者的同步;
      • 在生产者和消费者之间建立管道;

      第一种方式有较高的效率,并且易于实现,代码控制性好,属于常用实现模式;

      第二种管道缓冲区不易控制,被传输的数据不易封装.

    • 生产者\消费者经典的实现是ThreadPoolExecutor与工作队列的关系

    2、实现

    2.1、wait()/notify()方法

    wait()/nofity()[notifyAll()]方法是基类Object的两个方法:

    wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
    notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态

    public class WaitAndNotify {
        private static Integer count = 0;
        private static final Integer FULL = 10;
        private static Object LOCK = new Object();
        public static void main(String[] args) {
            WaitAndNotify w = new WaitAndNotify();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
        }
        class Producer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK) {
                        while (count == FULL) {
                            try {
                                LOCK.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        count++;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~生产者生产, 目前总共有:" + count);
                        LOCK.notifyAll();
                    }
                }
            }
        }
        class Consumer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK) {
                        while (count == 0) {
                            try {
                                LOCK.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~消费者消费, 目前总共有:" + count);
                        LOCK.notifyAll();
                    }
                }
            }
        }
    }
    

    2.2、ReentrantLock实现

    使用到 Condition的 await和signal()/singnalAll() 来实现线程的通信

    public class LockExample {
        private static Integer count = 0;
        private static final Integer FULL = 10;
        private Lock lock = new ReentrantLock();
        private final Condition notFull = lock.newCondition();
        private final Condition notEmpty = lock.newCondition();
        public static void main(String[] args) {
            LockExample w = new LockExample();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
        }
        class Producer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.lock();
                    try {
                        while (count == FULL){
                            try {
                                notFull.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        count++;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~生产者生产, 目前总共有:" + count);
                        notEmpty.signal();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
        class Consumer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.lock();
                    try {
                        while (count == 0){
                            try {
                                notEmpty.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~消费者消费, 目前总共有:" + count);
                        notFull.signal();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    }
    

    2.3、阻塞队列BlockingQueue的实现

    被阻塞的情况主要有如下两种:

    • 当队列满了的时候进行入队列操作
    • 当队列空了的时候进行出队列操作

    使用take()和put()方法,这里生产者和生产者,消费者和消费者之间不存在同步,所以会出现连续生成和连续消费的现象。

    public class BlockingQueueExample {
        private static Integer count = 0;
        final BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10);
        public static void main(String[] args) {
            BlockingQueueExample w = new BlockingQueueExample();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
        }
        class Producer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        blockingQueue.put(1);
                        count++;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~生产者生产, 目前总共有:" + count);
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
        class Consumer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        blockingQueue.take();
                        count--;
                        System.out.println(Thread.currentThread().getName() + " ^^^^^^消费者消费, 目前总共有:" + count);
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    2.4、信号量Semaphore的实现

    Java中的Semaphore维护了一个许可集,一开始先设定这个许可集的数量,可以使用acquire()方法获得一个许可,当许可不足时会被阻塞,release()添加一个许可。加入了另外一个mutex信号量,维护生产者消费者之间的同步关系,保证生产者和消费者之间的交替进行

    public class SemaphoreExample {
        private static Integer count = 0;
        final Semaphore notFull = new Semaphore(10);
        final Semaphore notEmpty = new Semaphore(0);
        final Semaphore mutex = new Semaphore(1);
        public static void main(String[] args) {
            SemaphoreExample w = new SemaphoreExample();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Producer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
            new Thread(w.new Consumer()).start();
        }
        class Producer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        notFull.acquire();
                        mutex.acquire();
                        count++;
                        System.out.println(Thread.currentThread().getName() + " ~~~~~生产者生产, 目前总共有:" + count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        mutex.release();
                        notEmpty.release();
                    }
                }
            }
        }
        class Consumer implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        notEmpty.acquire(); 
                        mutex.acquire();
                        count--;
                        System.out.println(Thread.currentThread().getName() + " ^^^^^^消费者消费, 目前总共有:" + count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        mutex.release();
                        notFull.release();
                    }
                }
            }
        }
    }

    2.5、管道输入输出流实现

    PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流.它们的作用是让多线程可以通过管道进行线程间的通讯.在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用。

    使用方法:

    先创建一个管道输入流和管道输出流,然后将输入流和输出流进行连接,用生产者线程往管道输出流中写入数据,消费者在管道输入流中读取数据,这样就可以实现了不同线程间的相互通讯。

    但是这种方式在生产者和生产者、消费者和消费者之间不能保证同步,也就是说在一个生产者和一个消费者的情况下是可以生产者和消费者之间交替运行的,多个生成者和多个消费者者之间则不行

    public class PipedExample {
        final PipedInputStream pis = new PipedInputStream();
        final PipedOutputStream pos = new PipedOutputStream();
        {
            try {
                pis.connect(pos);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args) {
            PipedExample p = new PipedExample();
            new Thread(p.new Producer()).start();
            new Thread(p.new Consumer()).start();
        }
        class Producer implements Runnable {
            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(1000);
                        int num = (int) (Math.random() * 255);
                        System.out.println(Thread.currentThread().getName() + "生产者生产了一个数字,该数字为: " + num);
                        pos.write(num);
                        pos.flush();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        pos.close();
                        pis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        class Consumer implements Runnable {
            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(1000);
                        int num = pis.read();
                        System.out.println("消费者消费了一个数字,该数字为:" + num);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        pos.close();
                        pis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    2.6、Exchanger

    public class ExchangerDemo {
        public static void main(String[] args) {
            List<String> buffer1 = new ArrayList<String>();
            List<String> buffer2 = new ArrayList<String>();
            Exchanger<List<String>> exchanger = new Exchanger<List<String>>();
            Thread producerThread = new Thread(new Producer(buffer1,exchanger));
            Thread consumerThread = new Thread(new Consumer(buffer2,exchanger));
            producerThread.start();
            consumerThread.start();
        }
        static class Producer implements Runnable {
            // 生产者消费者交换的数据结构
            private List<String> buffer;
            // 生产者和消费者的交换对象
            private Exchanger<List<String>> exchanger;
            public Producer(List<String> buffer, Exchanger<List<String>> exchanger) {
                this.buffer = buffer;
                this.exchanger = exchanger;
            }
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("生产者第" + i + "次提供");
                    for (int j = 1; j <= 3; j++) {
                        System.out.println("生产者装入" + i + "--" + j);
                        buffer.add("buffer:" + i + "--" + j);
                    }
                    System.out.println("生产者装满,等待与消费者交换...");
                    try {
                        exchanger.exchange(buffer);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        static class Consumer implements Runnable {
            private List<String> buffer;
            private final Exchanger<List<String>> exchanger;
            public Consumer(List<String> buffer, Exchanger<List<String>> exchanger) {
                this.buffer = buffer;
                this.exchanger = exchanger;
            }
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    //调用exchange()与消费者进行数据交换
                    try {
                        buffer = exchanger.exchange(buffer);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消费者第" + i + "次提取");
                    for (int j = 1; j <= 3; j++) {
                        System.out.println("消费者 : " + buffer.get(0));
                        buffer.remove(0);
                    }
                }
            }
        }
    }

    九、Disruptor

    1、LMAX架构

    LMAX是一种新型零售金融交易平台,它能够以很低的延迟(latency)产生大量交易(吞吐量)。

    这个系统是建立在JVM平台上,核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处理器完全是运行在内存中(in-memory),使用事件源驱动方式(event sourcing). 业务逻辑处理器的核心是Disruptors,这是一个并发组件,能够在无锁的情况下实现网络的Queue并发操作;

    2、Disruptor介绍

    3、Disruptor快速入门

    • (1)构架一个Event

      @Data
      public class OrderEvent {
          private Long value;
      }
    • (2)构建一个EventFactory

      public class OrderEventFactory implements EventFactory<OrderEvent> {
          @Override
          public OrderEvent newInstance() {
              return new OrderEvent();
          }
      }
    • (3)构建一个EventHandler

      public class OrderEventHandler implements EventHandler<OrderEvent> {
          @Override
          public void onEvent(OrderEvent event, long sequence, boolean endOfBatch) throws Exception {
              System.out.println("消费者:" + event.getValue());
          }
      }
    • (4)构建一个Disruptor

      /**
       * 1 eventFactory: 消息(event)工厂对象
       * 2 ringBufferSize: 容器的长度
       * 3 executor: 线程池(建议使用自定义线程池) RejectedExecutionHandler
       * 4 ProducerType: 单生产者 还是 多生产者
       * 5 waitStrategy: 等待策略
       */
      //1. 实例化disruptor对象
      Disruptor<OrderEvent> disruptor = new Disruptor<OrderEvent>(orderEventFactory,
              ringBufferSize,
              executor,
              ProducerType.SINGLE,
              new BlockingWaitStrategy());
      //2. 添加消费者的监听 (构建disruptor 与 消费者的一个关联关系)
      disruptor.handleEventsWith(new OrderEventHandler());
      //3. 启动disruptor
      disruptor.start();

      关于RingBuffer的使用

      //1 在生产者发送消息的时候, 首先 需要从我们的ringBuffer里面 获取一个可用的序号
      long sequence = ringBuffer.next();	//0	
      try {
          //2 根据这个序号, 找到具体的 "OrderEvent" 元素 注意:此时获取的OrderEvent对象是一个没有被赋值的"空对象"
          OrderEvent event = ringBuffer.get(sequence);
          //3 进行实际的赋值处理
          event.setValue(data.getLong(0));			
      } finally {
          //4 提交发布操作
          ringBuffer.publish(sequence);			
      }

    4、Disruptor核心组件

    4.1、RingBuffer

    基于数组的缓存实现,也是创建sequence与定义WaitStrategy的入口

    4.2、Disruptor

    4.3、Sequence

    • 通过顺序递增的序号来编号,管理进行交互的数据;
    • 对数据的处理过程总是沿着序号逐个递增处理;
    • 一个sequence用于跟踪标识某个特定的事件处理者(RingBuffer、Producer、Consumer)的处理进度
    • Sequence可以看成是一个AtomicLong用于标识进度;
    • 还有另外一个目的是防止不同的Sequence之间CPU缓存伪共享的问题

    4.4、Sequencer

    • Disruptor真正的核心
    • 此接口有两个实现类:MultiProducerSequencer、SingleProducerSequencer
    • 主要实现生产者和消费者之间快速、正确的传递数据的并发算法

    4.5、SequenceBarrier

    • 用于保持对RingBuffer的Mian Published Sequence和Consumer之间的平衡关系;SequenceBarrier还定义了决定Cosnumer是否还有可处理的事件的逻辑;

    4.6、WaitStrategy

    • 决定一个消费者将如何等待生产者将Event置入Disruptor

    • BlockingWaitStrategy:是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现;

    • SleepingWaitStrategy:其性能跟BlockingWaitStrategy差不多,对CPU的消耗类似,但其对生产者线程的影响最小,适合用于异步日志类似的场景;

    • YieldingWaitStrategy:其性能最好,适合用于低延迟的系统。在要求极高性能且事件处理数小于CPU逻辑核心数的场景中,推荐使用此策略;比如CPU开启超线程的特性;

    4.7、Event

    • 从生产者到消费者过程中所处理的数据单元

    4.8、EventProcessor

    • 主要事件循环,处理Disruptor的Event,拥有消费者的Sequence
    • 它有个实现类是BatchEventProcessor,包含了event loop有效的实现,并且将回调到一个EventHandler接口的实现对象;

    4.9、EventHandler

    由用户实现并且代表了Disruptor中的一个消费者的接口,也就是消费者的处理逻辑都需要写在这里;

    4.10、WorkProcessor

    确保每个sequence只能被一个processor消费,在同一个workpool中处理多个WorkProcessor不会被消费统一的sequence

    4.11、Disruptor编程模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7Nqr23j-1580025046166)(image/Disruptor图解.png)]

    5、Disruptor高级应用

    5.1、核心链路应用场景

    如京东中下单:用户添加到购物车 -> 下单 —> 支付 —> 配送 -> 收货

    核心链路的代码实现,业务逻辑非常复杂;核心链路特点:至关重要且业务复杂

    主要实现方式:

    • 传统的完全解耦模式
    • 模板模式

    上述方式不适合,解决手段:

    • 领域模型的高度抽象
    • 寻找更好的框架帮助进行编码:
      • 有限状态机框架,比如:spring-statemachine
      • 使用Disruptor

    5.2、并行计算

    • 串行操作:使用链式调用的方式:disruptor.handleEventsWith(new Handler1()).handleEventsWith(new Handler2()).handleEventsWith(new Handler3());
    • 并行操作:使用单独调用的方式:
      • 单独调用:
        disruptor.handleEventsWith(new Handler1());
        disruptor.handleEventsWith(new Handler2());
        disruptor.handleEventsWith(new Handler3());
      • 传入多个元素:disruptor.handleEventsWith(new Handler1(), new Handler2(), new Handler3());

    5.3、多边形高端操作

    Disruptor可以实现串并行同时编码

    • 菱形操作
      disruptor.handleEventsWith(new Handler1(), new Handler2())
              .handleEventsWith(new Handler3());
              
      // 或者 通过handlerEventsWith方法的返回参数:EventHandlerGroup 来处理
      EventHandlerGroup<Trade> ehGroup = disruptor.handleEventsWith(new Handler1(), new Handler2());
      ehGroup.then(new Handler3());
    • 六边形操作
      // 执行顺序:h1和h4是并行操作,h2在h1执行完成后执行;h5在h4执行完成后执行;在h2和h5都执行成功之后,在执行h3
      Handler1 h1 = new Handler1();
      Handler2 h2 = new Handler2();
      Handler3 h3 = new Handler3();
      Handler4 h4 = new Handler4();
      Handler5 h5 = new Handler5();
      disruptor.handleEventsWith(h1, h4);
      disruptor.after(h1).handleEventsWith(h2);
      disruptor.after(h4).handleEventsWith(h5);
      disruptor.after(h2, h5).handleEventsWith(h3);
      注意:单消费者模式下,上述代码执行跟构造Disruptor时传入的线程池有关,因为上述代码有5个EventHandler,所以,线程池中线程数量必须至少有5个;这是因为单消费者下是由BatchEventProcessor来操作的,而BatchEventProcessor就代表一个线程,其实现了Runnable接口;对于多消费者是不存在该问题的;

    5.4、多生产者与多消费者

    使用WorkPool来处理

    6、源码分析

    6.1、Disruptor底层性能特点

    • 数据结构方面:是否环形结构、数组、内存预加载;
    • 使用单线程写方式、内存屏障
    • 消除伪共享(填充缓存行)
    • 序号栅栏和序号配合使用来消除锁和CAS;

    6.2、数据结构与内存预加载机制

    • RingBuffer使用数组Object[] entries作为存储元素

    6.3、使用单线程写

    Disruptor的RingBuffer,做到完全无锁是因为单线程写;注入Redis、Netty等高性能技术框架的设计都是这个核心思想

    其是基于系统内核

    6.4、内存优化:内存屏障

    要正确的实现无锁,还需另外一个关键技术:内存屏障

    6.5、系统缓存优化:消除伪共享

    6.6、算法优化:序号栅栏机制

    • Disruptor3.0中,序号栅栏SequenceBarrier和序号Sequence搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用;
    • Disruptor3.0中,每个消费者和生产者都持有自己的序号,这些序号的变化必须满足如下基本条件:
      • 消费者序号数值必须小于生产者序号数值;
      • 消费者序号必须小于其前置(依赖关系)消费者的序号数值;
      • 生产者序号数值不能大于消费者中最小的序号数值,以避免生产者速度过快,将还未来得及消费的消息覆盖;

    6.7、WaitStrategy等待策略

    对于YieldingWaitStrategy实现类,尝试去修改源代码,降低高性能对CPU和内存资源的损耗

    6.8、EventProcessor核心机制

    十、线程与并发相关面试题

    1、为什么线程池的底层数据接口采用HashSet来实现

    2、使用模拟真正的并发请求

    使用CountDownLatch

    模拟超过5W的并发用户

    3、可重入锁为什么可以防止死锁

    public class Widget {
        public synchronized void doSomething(){
            // do something
        }
    }
    public class LoggingWidget extends Widget {
        public synchronized void doSomething() {
            super.doSomething();
        }
    }

    如果synchronized 不是可重入锁,那么LoggingWidget 的super.dosomething();无法获得Widget对象的锁,因为会死锁

    这里涉及到Java的重写;子类LoggingWidget 的doSomething方法,重写了父类Widget 的doSomething方法,但是子类对象如果要调用父类的doSomething方法,那么就需要用到super关键字了。因为实例方法的调用是Java虚拟机在运行时动态绑定的,子类LoggingWidget 的对象调用doSomething方法,一定是绑定到子类自身的doSomething方法,必须用super关键字告诉虚拟机,这里要调用的是父类的doSomething方法;

    super关键字并没有新建一个父类的对象,比如说widget,然后再去调用widget.doSomething方法,实际上调用父类doSomething方法的还是我们的子类对象;

    如果一个线程有子类对象的引用loggingWidget,然后调用loggingWidget.doSomething方法的时候,会请求子类对象loggingWidget 的对象锁;又因为loggingWidget 的doSomething方法中调用的父类的doSomething方法,实际上还是要请求子类对象loggingWidget 的对象锁,那么如果synchronized 关键字不是个可重入锁的话,就会在子类对象持有的父类doSomething方法上产生死锁了。正因为synchronized 关键字的可重入锁,当前线程因为已经持有了子类对象loggingWidget 的对象锁,后面再遇到请求loggingWidget 的对象锁就可以畅通无阻地执行同步方法了

    参考文章

    展开全文
  • Java并发与多线程(1):多线程一、并发与并行并发并行二、线程与进程进程线程线程调度:三、创建线程类 一、并发与并行 并发 指两个或多个事件在同一个时间段内发生。 并行 指两个或多个事件在同一时刻发生(一起进行...

    一、并发与并行

    1.并发
    指两个或多个事件在同一个时间段内发生。
    2.并行
    指两个或多个事件在同一时刻发生(一起进行)
    在这里插入图片描述
    在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每
    一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

    而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,
    即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核
    CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

    注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同
    理,线程也是一样的
    。从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个
    线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为
    线程调度。
    在这里插入图片描述

    二、线程与进程

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

    在这里插入图片描述
    2.线程
    线程是进程中的一个执行单元,负责当前进程中程序的执行(路),一个进程中至少有一个线程。一个进程
    中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    在这里插入图片描述

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

    线程调度

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

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

    设置线程的优先级
    在这里插入图片描述
    抢占式调度详解
    大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我
    们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。

    此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

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

    在这里插入图片描述

    三、创建线程类

    Java使用java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是
    完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
    Java中通过继承Thread类来创建并启动多线程的步骤如下:

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
      run()方法称为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象
    3. 调用线程对象的start()方法来启动该线程

    在这里插入图片描述

    代码如下:

    /**
     * 自定义一个线程类extends Thread
     * @author Mango
     */
    public class MainThread extends Thread{
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(getName() + ":正在执行!"+ i);
            }
        }
    
    }
    
    /**
     * 测试类
     * @author Mango
     */
    public class Dome1 {
    
        public static void main(String[] args) {
            MainThread mainThread = new MainThread();
    
            mainThread.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("main线程" + i);
            }
        }
    }
    
    展开全文
  • python 多进程并发与多线程并发总结

    万次阅读 2015-05-16 19:33:18
    本文对python支持的几种并发方式——多进程并发与多线程并发进行简单的总结。

    本文对python支持的几种并发方式进行简单的总结。

    Python支持的并发分为多线程并发与多进程并发(异步IO本文不涉及)。概念上来说,多进程并发即运行多个独立的程序,优势在于并发处理的任务都由操作系统管理,不足之处在于程序与各进程之间的通信和数据共享不方便;多线程并发则由程序员管理并发处理的任务,这种并发方式可以方便地在线程间共享数据(前提是不能互斥)。Python对多线程和多进程的支持都比一般编程语言更高级,最小化了需要我们完成的工作。

    一.多进程并发

    Mark Summerfield指出,对于计算密集型程序,多进程并发优于多线程并发。计算密集型程序指的程序的运行时间大部分消耗在CPU的运算处理过程,而硬盘和内存的读写消耗的时间很短;相对地,IO密集型程序指的则是程序的运行时间大部分消耗在硬盘和内存的读写上,CPU的运算时间很短。

    对于多进程并发,python支持两种实现方式,一种是采用进程安全的数据结构:multiprocessing.JoinableQueue,这种数据结构自己管理“加锁”的过程,程序员无需担心“死锁”的问题;python还提供了一种更为优雅而高级的实现方式:采用进程池。下面一一介绍。

    1.队列实现——使用multiprocessing.JoinableQueue

    multiprocessing是python标准库中支持多进程并发的模块,我们这里采用multiprocessing中的数据结构:JoinableQueue,它本质上仍是一个FIFO的队列,它与一般队列(如queue中的Queue)的区别在于它是多进程安全的,这意味着我们不用担心它的互斥和死锁问题。JoinableQueue主要可以用来存放执行的任务和收集任务的执行结果。举例来看(以下皆省去导入包的过程):

    def read(q):
        while True:
            try:
                value = q.get()
                print('Get %s from queue.' % value)
                time.sleep(random.random())
            finally:
                q.task_done()
    
    def main():
        q = multiprocessing.JoinableQueue()
        pw1 = multiprocessing.Process(target=read, args=(q,))
        pw2 = multiprocessing.Process(target=read, args=(q,))
        pw1.daemon = True
        pw2.daemon = True
        pw1.start()
        pw2.start()
        for c in [chr(ord('A')+i) for i in range(26)]:
            q.put(c)
        try:
            q.join()
        except KeyboardInterrupt:
            print("stopped by hand")
    
    if __name__ == '__main__':
        main()

    对于windows系统的多进程并发,程序文件里必须含有“入口函数”(如main函数),且结尾处必须调用入口点。例如以if __name__ == '__main__': main()结尾。

    在这个最简单的多进程并发例子里,我们用多进程实现将26个字母打印出来。首先定义一个存放任务的JoinableQueue对象,然后实例化两个Process对象(每个对象对应一个子进程),实例化Process对象需要传送target和args参数,target是实现每个任务工作中的具体函数,args是target函数的参数。

    pw1.daemon = True
    pw2.daemon = True

    这两句话将子进程设置为守护进程——主进程结束后随之结束。

    pw1.start()
    pw2.start()

    一旦运行到这两句话,子进程就开始独立于父进程运行了,它会在单独的进程里调用target引用的函数——在这里即read函数,它是一个死循环,将参数q中的数一一读取并打印出来。

    value = q.get()

    这是多进程并发的要点,q是一个JoinableQueue对象,支持get方法读取第一个元素,如果q中没有元素,进程就会阻塞,直至q中被存入新元素。

    因此执行完pw1.start() pw2.start()这两句话后,子进程虽然开始运行了,但很快就堵塞住。

    for c in [chr(ord('A')+i) for i in range(26)]:
            q.put(c)

    将26个字母依次放入JoinableQueue对象中,这时候两个子进程不再阻塞,开始真正地执行任务。两个子进程都用value = q.get()来读取数据,它们都在修改q对象,而我们并不用担心同步问题,这就是multiProcessing.Joinable数据结构的优势所在——它是多进程安全的,它会自动处理“加锁”的过程。

    try:
            q.join()

    q.join()方法会查询q中的数据是否已读完——这里指的就是任务是否执行完,如果没有,程序会阻塞住等待q中数据读完才开始继续执行(可以用Ctrl+C强制停止)。

    对Windows系统,调用任务管理器应该可以看到有多个子进程在运行。

    2.进程池实现——使用concurrent.futures.ProcessPoolExecutor

    Python还支持一种更为优雅的多进程并发方式,直接看例子:

    def read(q):
            print('Get %s from queue.' % q)
            time.sleep(random.random())
    
    def main():
        futures = set()
        with concurrent.futures.ProcessPoolExecutor() as executor:
            for q in (chr(ord('A')+i) for i in range(26)):
                future = executor.submit(read, q)
                futures.add(future)
        try:
            for future in concurrent.futures.as_completed(futures):
                err = future.exception()
                if err is not None:
                    raise err
        except KeyboardInterrupt:
            print("stopped by hand")
    
    if __name__ == '__main__':
        main()

    这里我们采用concurrent.futures.ProcessPoolExecutor对象,可以把它想象成一个进程池,子进程往里“填”。我们通过submit方法实例一个Future对象,然后把这里Future对象都填到池——futures里,这里futures是一个set对象。只要进程池里有future,就会开始执行任务。这里的read函数更为简单——只是把一个字符打印并休眠一会而已。

    try:
            for future in concurrent.futures.as_completed(futures):

    这是等待所有子进程都执行完毕。子进程执行过程中可能抛出异常,err = future.exception()可以收集这些异常,便于后期处理。

    可以看出用Future对象处理多进程并发更为简洁,无论是target函数的编写、子进程的启动等等,future对象还可以向使用者汇报其状态,也可以汇报执行结果或执行时的异常。

    二.多线程并发

    对于IO密集型程序,多线程并发可能要优于多进程并发。因为对于网络通信等IO密集型任务来说,决定程序效率的主要是网络延迟,这时候是使用进程还是线程就没有太大关系了。

    1.队列实现——使用queue.Queue

    程序与多进程基本一致,只是这里我们不必使用multiProcessing.JoinableQueue对象了,一般的队列(来自queue.Queue)就可以满足要求:

    def read(q):
        while True:
            try:
                value = q.get()
                print('Get %s from queue.' % value)
                time.sleep(random.random())
            finally:
                q.task_done()
    
    def main():
        q = queue.Queue()
        pw1 = threading.Thread(target=read, args=(q,))
        pw2 = threading.Thread(target=read, args=(q,))
        pw1.daemon = True
        pw2.daemon = True
        pw1.start()
        pw2.start()
        for c in [chr(ord('A')+i) for i in range(26)]:
            q.put(c)
        try:
            q.join()
        except KeyboardInterrupt:
            print("stopped by hand")
    
    if __name__ == '__main__':
        main()

    并且这里我们实例化的是Thread对象,而不是Process对象,程序的其余部分看起来与多进程并没有什么两样。

    2. 线程池实现——使用concurrent.futures.ThreadPoolExecutor

    直接看例子:

    def read(q):
            print('Get %s from queue.' % q)
            time.sleep(random.random())
    
    def main():
        futures = set()
        with concurrent.futures.ThreadPoolExecutor(multiprocessing.cpu_count()*4) as executor:
            for q in (chr(ord('A')+i) for i in range(26)):
                future = executor.submit(read, q)
                futures.add(future)
        try:
            for future in concurrent.futures.as_completed(futures):
                err = future.exception()
                if err is not None:
                    raise err
        except KeyboardInterrupt:
            print("stopped by hand")
    
    if __name__ == '__main__':
        main()

    用ThreadPoolExecutor与用ProcessPoolExecutor看起来没什么区别,只是改了一下签名而已。

    不难看出,不管是使用队列还是使用进/线程池,从多进程转化到多线程是十分容易的——仅仅是修改了几个签名而已。当然内部机制完全不同,只是python的封装非常好,使我们可以不用关心这些细节,这正是python优雅之处。

    参考文献:
    [1]. Summerfield M. Python 3 程序开发指南[J]. 2011.
    [2]. Summerfield M, Python编程实战[J]. 2013.

    展开全文
  • Java高并发与多线程

    2019-10-22 13:32:19
    文章目录高并发与多线程线程synchronized关键字(**悲观锁**)那什么时候用自旋,什么时候用重量级锁?volatile关键字CAS (无锁优化 **乐观锁、自旋锁**)ReentrantLock 重入锁synchronized和ReentrantLock有什么不同?...
  • java并发与多线程总结

    千次阅读 2017-03-13 23:12:13
    Java并发与多线程总结1.多线程的优点 资源利用率更好 程序在某些情况下更简单 程序响应更快
  • 今天我们就来聊聊Java大数据开发当中,分布式、高并发与多线程的概念异同。 对于新手小白们来说,学习Java的阶段,分布式、多线程、高并发,是常常听到,却又容易混淆的概念,甚至有人会认为分布式=高并发=多线程...
  • 3.1 Java并发与多线程概念

    千次阅读 2021-01-22 00:11:39
    首先需要了解到为什么Java会支持多线程,这一切都是芯片技术的发展,多年前cpu还是单核运行模式,可以说是因为有了多核心处理器才有了并发,有了多核心多线程,才有了多线程运行的程序。     &...
  • 并发与并行的目标:尽肯能快的执行完所有任务。 并发特点: 并发程序之间有相互制约的关系。 (1)直接制约:一个程序需要另一个程序的结果。 (2)间接制约:程序间竞争共享资源。处理器、缓冲区、磁盘IO等 ...
  • 并发与多线程 一、并发基本概念及其实现,进程、线程基本概念。 1.并发、进程、线程的基本概念和综述 (1.1)并发:两个或者更多的任务(独立的活动)同时进行;一个程序同时执行多个任务;以往计算机,单核CPU,某...
  • 并发多线程总是被一起提起,给人感觉两者好像差不多,实则高并发多线程多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。下面详解:什么是高并发?高...
  • 并发与多线程的关系与区别

    万次阅读 多人点赞 2019-03-09 21:30:41
    一、什么是高并发并发(High Concurrency)是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该...
  • C++——多进程并发与多线程并发

    千次阅读 2018-02-02 17:39:50
    1. 进程并发 ...操作系统在进程间提供的附加保护操作和更高级别的通信机制,意味着可以比线程更容易地编写安全的并发代码。 使用独立的进程实现并发,可以在网络连接的不同的机器上运行独立的进程,虽然这
  • 并发与多线程区别

    2018-12-06 01:00:45
    1、高并发 ...这就需要对接口进行优化,而多线程是处理高并发的一种手段。 2、多线程  是一种异步处理的一种方式,在同一时刻最大限度的利用计算机资源。   开发十年,只剩下这套Java开发体系了...
  • 1. 并发与并行的区别,何为并发编程,并发编程的优势在哪 2. 多线程、多任务、多进程机制概述 3. 多线程、多任务、多进程机制与编程思想的关系

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,647
精华内容 9,858
关键字:

并发与多线程