Java并发编程实战笔记2.0

1.用锁的最佳实践

1.永远只在更新对象的成员变量时加锁    
2.永远只在访问可变的成员变量时加锁    
3.永远不在调用其他对象的方法时加锁

2.信号量模型

信号量模型可以简单概括为:一个计数器,一个等待队列,三个方法。

init():设置计数器的初始值。
down():计数器的值减 1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行。
up():计数器的值加 1;如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。


class Semaphore{
  // 计数器
  int count;
  // 等待队列
  Queue queue;
  // 初始化操作
  Semaphore(int c){
    this.count=c;
  }
  // 
  void down(){
    this.count--;
    if(this.count<0){
      //将当前线程插入等待队列
      //阻塞当前线程
    }
  }
  void up(){
    this.count++;
    if(this.count<=0) {
      //移除等待队列中的某个线程T
      //唤醒线程T
    }
  }
}

3.读写锁

所有的读写锁都遵守以下三条基本原则:
    1.允许多个线程同时读共享变量;
    2.只允许一个线程写共享变量;
    3.如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
    读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。

读写锁的降级


class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 读锁  
  final Lock r = rwl.readLock();
  //写锁
  final Lock w = rwl.writeLock();
  
  void processCachedData() {
    // 获取读锁
    r.lock();
    if (!cacheValid) {
      // 释放读锁,因为不允许读锁的升级
      r.unlock();
      // 获取写锁
      w.lock();
      try {
        // 再次检查状态  
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 释放写锁前,降级为读锁
        // 降级是可以的
        r.lock();} finally {
        // 释放写锁
        w.unlock(); 
      }
    }
    // 此处仍然持有读锁
    try {use(data);} 
    finally {r.unlock();}
  }
}

4.CountDownLatch和CyclicBarrier

CountDownLatch 主要用来解决一个线程等待多个线程的场景;而CyclicBarrier是一组线程之间互相等待。除此之外 CountDownLatch的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但CyclicBarrier的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。

5.线程池

1.SingleThreadExecutor的意义

SingleThreadExecutor内部会创建一个Thread,这个Thread的工作就是从一个队列中取出用户提交的任务进行执行,
如果执行过程中发生未受检的异常,singleThreadExecutor会自动重新启动一个线程再继续工作,这一点比自己创建
一个线程自己管理轻松很多,也不需要再去维护一个任务队列。

线程池管理的线程的几点意义:
1、缓存线程、进行池化,可实现线程重复利用、避免重复创建和销毁所带来的性能开销。
2、当线程调度任务出现异常时,会重新创建一个线程替代掉发生异常的线程。
3、任务执行按照规定的调度规则执行。线程池通过队列形式来接收任务。再通过空闲线程来逐一取出进行任务调度。即线程池可以控制任务调度的执行顺序。
4、可制定拒绝策略。即任务队列已满时,后来任务的拒绝处理规则。
以上意义对于singleThreadExecutor来说也是适用的。普通线程和线程池中创建的线程其最大的区别就是有无一个管理者对线程进行管理。

2.1 线程池构造方法中各个参数的意义


/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial parameters and default thread factory.
     *
     * @param corePoolSize the number of threads to keep in the pool, even if they are idle,   
     *          unless {@code allowCoreThreadTimeOut} is set
     *      核心线程数,即使空闲也会保存在线程池中,除非设置allowCoreThreadTimeOut
     * @param maximumPoolSize the maximum number of threads to allow in the pool
     *        线程中允许的最大线程数
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     *      核心线程之外的空闲线程的回收时间
     * @param unit the time unit for the {@code keepAliveTime} argument
     *      上面时间的单位
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     *      任务队列
     * @param threadFactory the factory to use when the executor creates a new thread
     *      创建线程时使用的工厂,Executors提供了Executors.defaultThreadFactory()
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     *      当处理程序由于已达到线程边界和队列容量而阻塞时,handler被使用
     * @throws IllegalArgumentException if one of the following holds:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); }

测试类


package com.lhc.concurrent.executor.param;

import java.util.concurrent.*;

public class Constructor {
    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + " run ! " + System.currentTimeMillis());
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        ThreadPoolExecutor executor = getPoolBySynchronousQueue();

        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);

        Thread.sleep(300);

        System.out.println("first out core:" + executor.getCorePoolSize());
        System.out.println("first out :" + executor.getPoolSize());
        System.out.println("first out queue:" + executor.getQueue().size());

        Thread.sleep(10000);

        System.out.println("second out core:" + executor.getCorePoolSize());
        System.out.println("second out :" + executor.getPoolSize());
        System.out.println("second out queue:" + executor.getQueue().size());
    }

    /**
     * 如果使用LinkedBlockingDeque来构造,,当线程数量大于corePoolSize时,
     * 其余的任务直接放入队列中,maximumPoolSize参数的作用忽略
     * keepAliveTime参数的作用忽略
     * @return
     */
    public static ThreadPoolExecutor getPoolByLinkedBlockingDeque(){
        return new ThreadPoolExecutor(7, 8, 5,
                TimeUnit.SECONDS, new LinkedBlockingDeque());
    }

    /**
     * 如果使用SynchronousQueue来构造,maximumPoolSize参数的作用生效
     * keepAliveTime参数的作用生效
     * 当启动线程大于maximumPoolSize参数时,不会放入队列,会因为无法处理的任务直接抛出异常
     * @return
     */
    public static ThreadPoolExecutor getPoolBySynchronousQueue(){
        return new ThreadPoolExecutor(7, 8, 5,
                TimeUnit.SECONDS, new SynchronousQueue());
    }
}

2.2 关于 corePoolSize + LinkedBlockingDeque.size


/**
 * 启动线程数大于 corePoolSize + LinkedBlockingDeque.size 时,
 * 会启动非核心线程, 当启动数大于 maxSize + + LinkedBlockingDeque.size 时,
 * 多出来的任务不处理,抛出异常
 */
package com.lhc.concurrent.executor.queue;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyLinked {
    public static void main(String[] args){
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                try{
                    System.out.println("begin " + System.currentTimeMillis());
                    Thread.sleep(1000);
                    System.out.println("end " + System.currentTimeMillis());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        LinkedBlockingDeque deque = new LinkedBlockingDeque<>(2);
        System.out.println("deque "+ deque.size());
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 5,
                TimeUnit.SECONDS, deque);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        System.out.println("deque " + deque.size());
        System.out.println("poolSize " + executor.getPoolSize());
    }
}

    不建议使用Executors的最重要的原因是:Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
    使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会throw RejectedExecutionException这是个运行时异常,对于运行时异常编译器并不强制 catch它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
    使用线程池,还要注意异常处理的问题,例如通过ThreadPoolExecutor 对象的 execute()方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理。

6.生产者-消费者模式

    支持分阶段提交以提升性能利用生产者-消费者模式还可以轻松地支持一种分阶段提交的应用场景。写文件如果同步刷盘性能会很慢,所以对于不是很重要的数据,往往采用异步刷盘的方式。某个项目中,其中的日志组件是自己实现的,采用的就是异步刷盘方式,刷盘的时机是:ERROR 级别的日志需要立即刷盘;数据积累到 500 条需要立即刷盘;存在未刷盘数据,且 5 秒钟内未曾刷盘,需要立即刷盘。伪代码如下:



class Logger {
  //任务队列  
  final BlockingQueue<LogMsg> bq = new BlockingQueue<>();
  //flush批量  
  static final int batchSize=500;
  //只需要一个线程写日志
  ExecutorService es = Executors.newFixedThreadPool(1);
  //启动写日志线程
  void start(){
    File file=File.createTempFile("foo", ".log");
    final FileWriter writer = new FileWriter(file);
    this.es.execute(()->{
      try {
        //未刷盘日志数量
        int curIdx = 0;
        long preFT=System.currentTimeMillis();
        while (true) {
          LogMsg log = bq.poll(5, TimeUnit.SECONDS);
          //写日志
          if (log != null) {
            writer.write(log.toString());
            ++curIdx;
          }
          //如果不存在未刷盘数据,则无需刷盘
          if (curIdx <= 0) {
            continue;
          }
          //根据规则刷盘
          if (log!=null && log.level==LEVEL.ERROR ||
              curIdx == batchSize ||
              System.currentTimeMillis()-preFT>5000){
            writer.flush();
            curIdx = 0;
            preFT=System.currentTimeMillis();
          }
        }
      }catch(Exception e){
        e.printStackTrace();
      } finally {
        try {
          writer.flush();
          writer.close();
        }catch(IOException e){
          e.printStackTrace();
        }
      }
    });  
  }
  //写INFO级别日志
  void info(String msg) {
    bq.put(new LogMsg(LEVEL.INFO, msg));
  }
  //写ERROR级别日志
  void error(String msg) {
    bq.put(new LogMsg(LEVEL.ERROR, msg));
  }
}
//日志级别
enum LEVEL {
  INFO, ERROR
}
class LogMsg {
  LEVEL level;
  String msg;
  //省略构造函数实现
  LogMsg(LEVEL lvl, String msg){}
  //省略toString()实现
  String toString(){}
}

7.HiKariCP数据库连接池

关于HiKariCP的两个数据结构,一个是FastList,另一个是ConcurrentBag。

    HiKariCP 中的 FastList 相对于ArrayList的一个优化点就是将 remove(Object element)方法的查找顺序变成了逆序查找。除此之外,FastList还有另一个优化点,是 get(int index)方法没有对index参数进行越界检查,HiKariCP能保证不会越界,所以不用每次都进行越界检查。

    ConcurrentBag中最关键的属性有4个,分别是:用于存储所有的数据库连接的共享队列 sharedList、线程本地存储 threadList、等待数据库连接的线程数 waiters 以及分配数据库连接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于线程之间传递数据。

你可能感兴趣的:(多线程并发)