Java并发编程:三

共享模型之不可变

日期转换的问题

下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
         new Thread(() -> {
             try {
                 log.debug("{}", sdf.parse("1951-04-21"));
             } catch (Exception e) {
                 log.error("{}", e);
             }
         }).start();
    }
有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果
19:10:40.859 [Thread-2] c.TestDateParse - {} 
java.lang.NumberFormatException: For input string: "" 
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 
 at java.lang.Long.parseLong(Long.java:601) 
 at java.lang.Long.parseLong(Long.java:631) 
 at java.text.DigitList.getLong(DigitList.java:195) 
 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) 
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) 
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 
 at java.text.DateFormat.parse(DateFormat.java:364) 
 at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) 
 at java.lang.Thread.run(Thread.java:748)

如何解决呢?

一个思路是使用 synchronized 锁,但是这样会带来性能上的损失。

这里考虑另一种思路:不可变对象

如果一个对象不能够修改其内部状态(属性),那么它就是线程安全的,因为不会存在并发修改的问题。这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
     new Thread(() -> {
         LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
         log.debug("{}", date);
     }).start();
}

这个方法是线程安全的。

不可变对象,实际是另一种避免竞争的方式。


2. 不可变设计

Java中还有另外一个不可变设计:String 类

public final class String
     implements java.io.Serializable, Comparable<String>, CharSequence {
     /** The value is used for character storage. */
     private final char value[];
     /** Cache the hash code for the string */
     private int hash; // Default to 0
 
     // ...
 
}

final 的使用

发现该类、类中所有属性都是 final
  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝

substring 为例:
public String substring(int beginIndex) {
     if (beginIndex < 0) {
         throw new StringIndexOutOfBoundsException(beginIndex);
     }
     int subLen = value.length - beginIndex;
     if (subLen < 0) {
         throw new StringIndexOutOfBoundsException(subLen);
     }
     return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

可以看到,这里调用了 String 的构造方法新建了一个字符串,并没有对原来的字符串直接进行修改

public String(char value[], int offset, int count) {
     if (offset < 0) {
         throw new StringIndexOutOfBoundsException(offset);
     }
     if (count <= 0) {
         if (count < 0) {
             throw new StringIndexOutOfBoundsException(count);
         }
         if (offset <= value.length) {
             this.value = "".value;
             return;
         }
     }
     if (offset > value.length - count) {
         throw new StringIndexOutOfBoundsException(offset + count);
     }
     this.value = Arrays.copyOfRange(value, offset, offset+count);
}

在构造方法里也没有对原字符串进行修改,而是对内容进行复制,生成了一个新的 char[] 数组

这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy )】

3. 无状态

web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

本章小结

  • 不可变类使用
  • 不可变类设计
  • * 原理方面
    • final
  • 模式方面
    • 享元

共享模型之工具

1. 自定义线程池

1.1 设置阻塞队列

@Slf4j(topic = "c.t3")
class BlockingQueue<T> {
    // 1. 任务队列
    private Deque<T> queue = new ArrayDeque();

    // 2. 锁,防止多个线程同时争抢一个任务
    private ReentrantLock lock = new ReentrantLock();

    // 3. 生产者条件变量
    private Condition fullWaitSet = lock.newCondition();

    // 4. 消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();

    // 5. 容量
    private int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    // 带超时时间的阻塞获取
    public T poll(long timeout, TimeUnit unit){
        lock.lock();

        long nanos = unit.toNanos(timeout);

        try {
            while (queue.isEmpty()) {
                try {
                    if (nanos <= 0){
                        return null;
                    }
                    // 返回值是 最大等待时间 - 已等待时间 = 剩余时间
                    // 防止虚假唤醒后继续重复等待。等够总时间后就会返回 null
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞获取
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞添加
    public void put(T task){
        lock.lock();
        try {
            while(queue.size() == capacity){
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        }finally {
            lock.unlock();
        }
    }

    //带超时时间的阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit){
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while(queue.size() == capacity){
                try {
                    if (nanos <= 0){
                        return false;
                    }
                    log.debug("等待带入任务队列{}。。。",task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        }finally {
            lock.unlock();
        }
    }


    // 获取容量大小
    public int getSize(){
        lock.lock();
        try {
            return queue.size();
        }finally {
            lock.unlock();
        }
    }

    // 带拒接策略的阻塞添加
    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            if (queue.size() == capacity){
                // 任务队列已满,执行拒接策略,具体实现看用户
                rejectPolicy.reject(this,task);
            }else {
                // 加入任务队列
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        }finally {
            lock.unlock();
        }
    }
}

BlockQueue的作用是供线程获取任务,以及存放生产者生成的任务

// 带超时时间的阻塞获取
public T poll(long timeout, TimeUnit unit)

// 阻塞添加
public void put(T element)


1.2 线程池

@Slf4j(topic = "c.t2")
class ThreadPool{
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

    // 获取任务的超时时间
    private long timeout;

    private TimeUnit unit;

    // 拒接策略
    private RejectPolicy<Runnable> rejectPolicy;

    // 执行任务
    public void execute(Runnable task){
        // 如果任务数没有超过 coreSize 时,新建线程去执行任务
        // 否则,任务加入阻塞队列 BlockQueue
        synchronized (workers) {
            if (workers.size() < coreSize){
                Worker worker = new Worker(task);
                log.debug("新增 worker {},{}",worker,task);
                workers.add(worker);
                worker.start();
            }else {
                // taskQueue.put(task);
                // 换为带拒接策略的阻塞添加
                taskQueue.tryPut(rejectPolicy,task);
            }
        }
    }

    class Worker extends Thread{
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 执行任务
            // 当task 不为空时,执行任务
            // 当 task 执行完毕时,再从 任务队列 中试图获取任务
            //while (task != null || (task = taskQueue.take()) != null){
            while (task != null || (task = taskQueue.poll(timeout,unit)) != null){
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers){
                // 当任务队列为空时,从线程池中移除当前空闲线程
                log.debug("worker 被移除{}",this);
                workers.remove(this);
            }
        }
    }

    public ThreadPool(int coreSize, long timeout, TimeUnit unit, int queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.unit = unit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }
}

线程池类:包含 任务队列taskQueue 和 线程集合workers,以及继承了 Thread 的内部类worker

提供了方法 execute() ,用于提供线程执行任务

worker 中重写了 run 方法,线程循环从任务队列中获取任务。当任务队列为空时,当前空闲线程销毁

1.3 测试

public class testPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(2, 1000, TimeUnit.MICROSECONDS, 10);
        for (int i = 0; i < 15; i++) {
            int j = i;
            threadPool.execute(()->{
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("任务{}",j);
            });
        }
    }
}

可以看出,如果总任务数超出了任务队列的最大存放容量,那么 main 线程便会阻塞在多余任务的添加上。

解决:设置拒接策略 


1.4 拒接策略 

设置接口

@FunctionalInterface
interface RejectPolicy<T>{
    void reject(BlockingQueue<T> queue, T task);
}

在 BlockQueue 中实现

    // 带拒接策略的阻塞添加
    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            if (queue.size() == capacity){
                // 任务队列已满,执行拒接策略,具体实现看用户
                rejectPolicy.reject(this,task);
            }else {
                // 加入任务队列
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        }finally {
            lock.unlock();
        }
    }

并在 ThreadPool 中调用

    // 执行任务
    public void execute(Runnable task){
        // 如果任务数没有超过 coreSize 时,新建线程去执行任务
        // 否则,任务加入阻塞队列 BlockQueue
        synchronized (workers) {
            if (workers.size() < coreSize){
                Worker worker = new Worker(task);
                log.debug("新增 worker {},{}",worker,task);
                workers.add(worker);
                worker.start();
            }else {
                // taskQueue.put(task);
                // 换为带拒接策略的阻塞添加
                taskQueue.tryPut(rejectPolicy,task);
            }
        }
    }

更新 ThreadPool 构造方法

public ThreadPool(int coreSize, long timeout, TimeUnit unit, int queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.unit = unit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }

测试

@Slf4j(topic = "c.t1")
public class testPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(2, 1000, TimeUnit.MICROSECONDS, 10,(queue,task)->{
            //死等
            //queue.put(task);
            // 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
            // 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);
            // 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);
            // 5) 让调用者自己执行任务
            task.run();
        });
        for (int i = 0; i < 15; i++) {
            int j = i;
            threadPool.execute(()->{
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("任务{}",j);
            });
        }
    }
}

2. ThreadPoolExecutor

2.1 线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数
从数字上比较, TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

2.2 构造方法

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略
工作流程:
  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会新建一个线程来执行这个任务
  • 当 corePoolSize 核心线程数目已经达到最大值,如果这时仍有新任务交给线程池,那么新任务就会被暂存至任务队列中
  • 当任务队列已满,但是还有新任务,并且核心线程都没有空闲时,线程池会创建 救急线程 来执行新任务(不是任务队列中等待的任务)。救急线程 的最大数目为 maxmunPoolSize - corePoolSize
  • 如果线程池已经到达了 maxmunPoolSize 时仍有新任务,这时就会执行拒绝策略
    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当全部任务执行完毕,救急线程 一段时间内得不到任务后就会销毁以节约资源,这个时间由 keepAliveTime 和 unit 控制

2.3 newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
 return new ThreadPoolExecutor(nThreads, nThreads,
                               0L, TimeUnit.MILLISECONDS,
                               new LinkedBlockingQueue<Runnable>());
}
特点
  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
这个方法生成的线程池适合任务量已知,相对耗时的工作

2.4 newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                               60L, TimeUnit.SECONDS,
                               new SynchronousQueue<Runnable>());
}
特点
  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
    • 全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

线程数会根据任务量不断增加,没有上限。当任务执行完毕后,空闲线程会在一分钟后被释放。

这个线程池适合任务数量密集,同时耗时比较短的情况


2.5 newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
     return new FinalizableDelegatedExecutorService
         (new ThreadPoolExecutor(1, 1,
          0L, TimeUnit.MILLISECONDS,
          new LinkedBlockingQueue<Runnable>()));
}
使用场景:
希望多个任务排队执行。线程数固定为 1 ,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

区别:
  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而newSingleThreadExecutor线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

2.6 提交任务API

// 执行任务
void execute(Runnable command);

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
     throws InterruptedException;

// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                              long timeout, TimeUnit unit)
     throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
     throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
 throws InterruptedException, ExecutionException, TimeoutException;

2.7 关闭线程池API

shutdown

/*
线程池状态变为 SHUTDOWN
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();
public void shutdown() {
 final ReentrantLock mainLock = this.mainLock;
 mainLock.lock();
 try {
     checkShutdownAccess();
 // 修改线程池状态
     advanceRunState(SHUTDOWN);
 // 仅会打断空闲线程
     interruptIdleWorkers();
     onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
 } finally {
     mainLock.unlock();
 }
 // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
 tryTerminate();
}

main 线程调用了 A线程池的 shutdown 方法,即使A线程中还有任务未执行完,main 线程也不会阻塞等待,而是继续往下执行 

shutdownNow

/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
public List<Runnable> shutdownNow() {
 List<Runnable> tasks;
 final ReentrantLock mainLock = this.mainLock;
 mainLock.lock();
 try {
     checkShutdownAccess();
     // 修改线程池状态
     advanceRunState(STOP);
     // 打断所有线程
     interruptWorkers();
     // 获取队列中剩余任务
     tasks = drainQueue();
 } finally {
     mainLock.unlock();
 }
 // 尝试终结
 tryTerminate();
 return tasks;
}

其他方法

// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();

// 线程池状态是否是 TERMINATED
boolean isTerminated();

// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

2.8 任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能, Timer 的优点在于简单易用。
但缺点是由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。

可以使用ScheduledExecutorService

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
     System.out.println("任务1,执行时间:" + new Date());
     try { 
        Thread.sleep(2000); 
     } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);

executor.schedule(() -> {
     System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);

输出

任务1,执行时间:Thu Jan 03 12:45:17 CST 2019 
任务2,执行时间:Thu Jan 03 12:45:17 CST 2019

可以看到,使用ScheduledExecutorService后,只要线程池中还有空闲线程,那么就是并发执行的

另外还有 scheduleAtFixedRate() 方法,接收四个参数,分别是任务执行对象,初始延迟时间,执行时间间隔和时间单位

利用这个方法可以固定间隔执行同一任务。但是,如果任务执行所需时间大于时间间隔,那么时间间隔会变为任务执行所需时间

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
     log.debug("running...");
     sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s

21:44:30.311 c.TestTimer [main] - start... 
21:44:31.360 c.TestTimer [pool-1-thread-1] - running... 
21:44:33.361 c.TestTimer [pool-1-thread-1] - running... 
21:44:35.362 c.TestTimer [pool-1-thread-1] - running... 
21:44:37.362 c.TestTimer [pool-1-thread-1] - running...
scheduleWithFixedDelay
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
     log.debug("running...");
     sleep(2);
}, 1, 1, TimeUnit.SECONDS);
输出分析:一开始,延时 1s scheduleWithFixedDelay 的间隔是 上一个任务结束 < - > 延时 < - > 下一个任务开始 所以间隔都是 3s
21:40:55.078 c.TestTimer [main] - start... 
21:40:56.140 c.TestTimer [pool-1-thread-1] - running... 
21:40:59.143 c.TestTimer [pool-1-thread-1] - running... 
21:41:02.145 c.TestTimer [pool-1-thread-1] - running... 
21:41:05.147 c.TestTimer [pool-1-thread-1] - running...

2.9 正确处理执行任务异常

方法一:使用 try/catch 主动捕捉异常

ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
 try {
     log.debug("task1");
     int i = 1 / 0;
 } catch (Exception e) {
     log.error("error:", e);
 }
});
21:59:04.558 c.TestTimer [pool-1-thread-1] - task1 
21:59:04.562 c.TestTimer [pool-1-thread-1] - error: 
java.lang.ArithmeticException: / by zero 
     at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28)

方法二:使用 Future

ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Boolean> f = pool.submit(() -> {
     log.debug("task1");
     int i = 1 / 0;
     return true;
 });
log.debug("result:{}", f.get());
21:54:58.208 c.TestTimer [pool-1-thread-1] - task1 
Exception in thread "main" java.util.concurrent.ExecutionException: 
java.lang.ArithmeticException: / by zero 
     at java.util.concurrent.FutureTask.report(FutureTask.java:122) 
     at java.util.concurrent.FutureTask.get(FutureTask.java:192) 
     at cn.itcast.n8.TestTimer.main(TestTimer.java:31) 
Caused by: java.lang.ArithmeticException: / by zero 
     at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28)

2.10 tomcat 线程池

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor ,行为稍有不同
  • 如果总线程数达到 maximumPoolSize
    • 这时不会立刻抛 RejectedExecutionException 异常
    • 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
Connector 配置
Executor 线程配置

3. 读写锁

3.1 ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 读-读 可以并发,提高性能。 类似于数据库中的 select ... from ... lock in share mode
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainer {
     private Object data;
     private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
     private ReentrantReadWriteLock.ReadLock r = rw.readLock();
     private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

     public Object read() {
         log.debug("获取读锁...");
         r.lock();
         try {
             log.debug("读取");
             sleep(1);
             return data;
         } finally {
             log.debug("释放读锁...");
             r.unlock();
         }
     }
     public void write() {
         log.debug("获取写锁...");
         w.lock();
         try {
             log.debug("写入");
             sleep(1);
         } finally {
             log.debug("释放写锁...");
             w.unlock();
         }
     }
}

读写锁的特点:

  • 读-读锁可以并发,也就是可以多个线程同时获得读锁
  • 读-写锁会相互阻塞,
  • 写-写锁也会相互阻塞

注意事项:

  • 读锁不支持条件变量
  • 锁重入时不支持锁升级,即已经持有读锁的情况下,重入时不能升级为写锁,会导致获取写锁永久等待
  • 锁重入时支持锁降级,持有写锁的情况下可以再获取读锁

3.2 StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加/解 读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加/解 写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读

StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
     // 锁升级
}
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped {
     private int data;
     private final StampedLock lock = new StampedLock();
     public DataContainerStamped(int data) {
         this.data = data;
     }
     public int read(int readTime) {
         long stamp = lock.tryOptimisticRead();
         log.debug("optimistic read locking...{}", stamp);
         sleep(readTime);
         if (lock.validate(stamp)) {
             log.debug("read finish...{}, data:{}", stamp, data);
             return data;
         }
         // 锁升级 - 读锁
         log.debug("updating to read lock... {}", stamp);
         try {
             stamp = lock.readLock();
             log.debug("read lock {}", stamp);
             sleep(readTime);
             log.debug("read finish...{}, data:{}", stamp, data);
             return data;
         } finally {
             log.debug("read unlock {}", stamp);
             lock.unlockRead(stamp);
         }
     }
     public void write(int newData) {
         long stamp = lock.writeLock();
         log.debug("write lock {}", stamp);
         try {
             sleep(2);
             this.data = newData;
         } finally {
             log.debug("write unlock {}", stamp);
             lock.unlockWrite(stamp);
         }
     }
}

同时使用两个读锁进行测试

public static void main(String[] args) {
     DataContainerStamped dataContainer = new DataContainerStamped(1);
     new Thread(() -> {
         dataContainer.read(1);
     }, "t1").start();
     sleep(0.5);
     new Thread(() -> {
         dataContainer.read(0);
     }, "t2").start();
}

输出结果,可以看到实际上并没有加读锁

15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256 
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256 
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1 
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1

同时加 读锁 与写锁

public static void main(String[] args) {
     DataContainerStamped dataContainer = new DataContainerStamped(1);
     new Thread(() -> {
         dataContainer.read(1);
     }, "t1").start();
     sleep(0.5);
     new Thread(() -> {
         dataContainer.write(100);
     }, "t2").start();
}

输出结果

15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256 
15:57:00.717 c.DataContainerStamped [t2] - write lock 384 
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256 
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384 
15:57:02.719 c.DataContainerStamped [t1] - read lock 513 
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000 
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513

注意事项:

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

3.3 Semaphore

Semaphore 是 Java 中的一个同步工具,用于管理一个共享资源的访问权限。它允许多个线程在同一时刻访问共享资源,但可以限制同时访问资源的线程数量。Semaphore 是基于计数的信号量,它维护一个可用许可证的数量,线程需要获得许可证才能执行临界区的代码。

输出结果

可以看到,同一时刻只能有不大于 信号量 数量的线程访问共享资源


3.4 CountDownLatch

用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值, await() 用来等待计数归零, countDown() 用来让计数减一

此外, CountDownLatch 也可以配合线程池使用


3.5 CycleBarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执
行到某个需要 同步 的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
它与 CountDownLatch 最大的区别就是, CycleBarrier 的是可以重用的,可以理解为 “人满发车”
CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
new Thread(()->{
     System.out.println("线程1开始.."+new Date());
     try {
         cb.await(); // 当个数不足时,等待
     } catch (InterruptedException | BrokenBarrierException e) {
         e.printStackTrace();
     }
     System.out.println("线程1继续向下运行..."+new Date());
}).start();

new Thread(()->{
     System.out.println("线程2开始.."+new Date());
     try { Thread.sleep(2000); } catch (InterruptedException e) { }
     try {
         cb.await(); // 2 秒后,线程个数够2,继续运行
     } catch (InterruptedException | BrokenBarrierException e) {
         e.printStackTrace();
     }
     System.out.println("线程2继续向下运行..."+new Date());
}).start();

3.6 线程安全集合类概述

线程安全集合类可以分为三大类:
  • 遗留的线程安全集合如 Hashtable Vector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*
java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking CopyOnWrite Concurrent
  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
      • 求大小弱一致性,size 操作未必是 100% 准确
      • 读取弱一致性
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值