[Ilustración] JUC de subprocesos múltiples de Java desde la entrada hasta el dominio

¡Continúe creando, acelere el crecimiento! Este es el 4to día de mi participación en el "Nuggets Daily New Plan · June Update Challenge", haz clic para ver los detalles del evento

1. JUC multiproceso

1.1 Grupo de subprocesos

1.1.1 Revisión de hilos

1) Revisar el método de creación de hilos

  • Heredar subproceso
  • Implementar ejecutable

2) El estado del hilo.

expediente

  • NUEVO: recién creado, ninguna acción realizada

    Thread thread = new Thread();
    System.out.println(thread.getState());
    复制代码
  • EJECUTABLE: llama a ejecutar, se puede ejecutar, pero no significa que se deba ejecutar (EN EJECUCIÓN, LISTO)

    thread.start();
    System.out.println(thread.getState());
    复制代码
  • BLOQUEADO: No se puede agarrar el candado

            final byte[] lock = new byte[0];
            new Thread(new Runnable() {
                public void run() {
                    synchronized (lock){
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            Thread thread2 = new Thread(new Runnable() {
                public void run() {
                    synchronized (lock){
                    }
                }
            });
            thread2.start();
            Thread.sleep(1000);
            System.out.println(thread2.getState());
    复制代码
  • ESPERANDO

    Thread thread2 = new Thread(new Runnable() {
        public void run() {
            LockSupport.park();
        }
    });
    
    thread2.start();
    Thread.sleep(500);
    System.out.println(thread2.getState());
    LockSupport.unpark(thread2);
    Thread.sleep(500);
    System.out.println(thread2.getState());
    复制代码
  • TIMED_ESPERANDO

    Thread thread3 = new Thread(new Runnable() {
        public void run() {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    thread3.start();
    Thread.sleep(500);
    System.out.println(thread3.getState());
    复制代码
  • TERMINADO

    //等待1s后再来看
    Thread.sleep(1000);
    System.out.println(thread.getState());
    复制代码

3) Grupo de subprocesos

De acuerdo con el estado anterior, después de que se ejecuta el subproceso ordinario, entrará en TERMINADO y se destruirá, y el grupo de subprocesos creará un grupo de búfer para almacenar el subproceso.Después de que finalice la ejecución, el subproceso no morirá, pero sí regrese al grupo de subprocesos para volver a estar inactivo, esperando que llegue la siguiente tarea, lo que le da al grupo de subprocesos más ventajas que crear subprocesos manualmente:

  • Reducir el consumo de recursos del sistema y reducir el consumo causado por la creación y destrucción de subprocesos mediante la reutilización de subprocesos existentes;
  • Mejorar la velocidad de respuesta del sistema.Cuando llega una tarea, al reutilizar el hilo existente, se puede ejecutar inmediatamente sin esperar la creación de un nuevo hilo;
  • Es conveniente controlar el número de subprocesos concurrentes. Porque si el subproceso se crea sin límite, puede causar demasiado uso de memoria y OOM
  • Ahorre el costo de tiempo de los subprocesos de cambio de CPU (necesita mantener la escena del subproceso que se está ejecutando actualmente y restaurar la escena del subproceso que se ejecutará).
  • Proporcione funciones más potentes, grupo de subprocesos de tiempo de retraso. (Temporizador frente a ScheduledThreadPoolExecutor)

4) Sistema de grupo de subprocesos ( ver: ScheduledThreadPoolExecutor, diagrama de clase ForkJoinPool )

expediente

ilustrar:

  • El más utilizado es ThreadPoolExecutor
  • ScheduledThreadPoolExecutor para programar
  • ForkJoinPool para dividir y fusionar tareas
  • Los ejecutores son clases de herramientas que lo ayudan a crear grupos de subprocesos

1.1.2 Mecanismo de trabajo

1) Estado del grupo de subprocesos

expediente

  • RUNNING:初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。RUNNING状态下,能够接收新任务,以及对已添加的任务进行处理。

  • SHUTDOWN:SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

    //shutdown后不接受新任务,但是task1,仍然可以执行完成
    
    ExecutorService poolExecutor = Executors.newFixedThreadPool(5);
    poolExecutor.execute(new Runnable() {
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println("finish task 1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    poolExecutor.shutdown();
    poolExecutor.execute(new Runnable() {
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    System.out.println("ok");
    复制代码
  • STOP:不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。调用线程池的shutdownNow()接口时,线程池由(RUNNING 或 SHUTDOWN ) -> STOP

    注意:容易引发不可预知的结果!运行中的任务也许还会打印,直到结束,因为调的是Thread.interrupt

    //改为shutdownNow后,任务立马终止,sleep被打断,新任务无法提交,task1停止
    
    poolExecutor.shutdownNow();
    复制代码
  • TIDYING:所有的任务已终止,队列中的”任务数量”为0,线程池会变为TIDYING。线程池变为TIDYING状态时,会执行钩子函数terminated(),可以通过重载terminated()函数来实现自定义行为

    //自定义类,重写terminated方法
    public class MyExecutorService extends ThreadPoolExecutor {
    
        public MyExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }
    
        @Override
        protected void terminated() {
            super.terminated();
            System.out.println("terminated");
        }
        
        //调用 shutdownNow, ternimated方法被调用打印
        public static void main(String[] args) throws InterruptedException {
            MyExecutorService service = new MyExecutorService(1,2,10000,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(5));
            service.shutdownNow();
        }
    }
    
    
    复制代码
  • TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED

2)结构说明

​ 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部协调空闲的线程,如果有,则将任务交给某个空闲的线程。一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

(源码查看:两个集合,一个queue,一个hashset)

expediente

3)任务的提交

  • 添加任务,如果线程池中线程数没达到coreSize,直接创建新线程执行
  • 达到core,放入queue
  • queue已满,未达到maxSize继续创建线程
  • 达到maxSize,根据reject策略处理
  • 超时后,线程被释放,下降到coreSize

1.1.3 源码剖析

expediente

//任务提交阶段:(4个if条件路线)

public void execute(Runnable command) {
  if (command == null)
            throw new NullPointerException();
  int c = ctl.get();
  //判断工作数,如果小于coreSize,addWork,注意第二个参数core=true
  if (workerCountOf(c) < corePoolSize) {
      if (addWorker(command, true))
          return;
      c = ctl.get();
  }
  //否则,如果线程池还在运行,offer到队列
  if (isRunning(c) && workQueue.offer(command)) {
      //再检查一下状态
      int recheck = ctl.get();
      //如果线程池已经终止,直接移除任务,不再响应
      if (! isRunning(recheck) && remove(command))
          reject(command);
      //否则,如果没有可用线程的话(比如coreSize=0),创建一个空work
    	//该work创建时不会给指派任务(为null),但是会被放入works集合,进而从队列获取任务去执行
      else if (workerCountOf(recheck) == 0)
          addWorker(null, false);
  }
  //队列也满,继续调addWork,但是注意,core=false,开启到maxSize的大门
  //超出max的话,addWork会返回false,进入reject
  else if (!addWorker(command, false))
      reject(command);
}
复制代码
//线程创建

private boolean addWorker(Runnable firstTask, boolean core) {
    //第一步,计数判断,不符合条件打回false
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.

        for (;;) {
            int wc = workerCountOf(c);
            //判断线程数,注意这里!
            //也就说明线程池的线程数是不可能设置任意大的。
            //最大29位(CAPACITY=29位二进制)
          	//超出规定范围,返回false,表示不允许再开启新工作线程,创建worker失败!
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
	//第二步,创建新work放入线程集合works(一个HashSet)
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //符合条件,创建新的work并包装task
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
          	//加锁,workers是一个hashset,这里要保障线程安全性
            mainLock.lock();
            try {   
              			//...
                    //在这里!!!
                    workers.add(w);
                    
                  	//...                    
                    workerAdded = true;
                
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                //注意,只要是成功add了新的work,那么将该新work立即启动,任务得到执行
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
复制代码
//任务获取与执行
  
//在worker执行runWorker()的时候,不停循环,先查看自己有没有携带Task,如果有,执行
while (task != null || (task = getTask()) != null)

//如果没用,会调用getTask,从队列获取任务

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // ...

        int wc = workerCountOf(c);

        // Are workers subject to culling? - 很形象,要不要乖乖的被“捕杀”?
        //判断是不是要超时处理,重点!!!决定了当前线程要不要被释放
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
				//线程数超出max,并且上次循环中poll等待超时了,那么说明该线程已终止
        //将线程队列数量原子性减
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            //计数器做原子递减,递减成功后,返回null,for被中止
            if (compareAndDecrementWorkerCount(c))
                return null;
            //递减失败,继续下一轮循环,直到成功
            continue;
        }

        try {
            //重点!!!
            //如果线程可被释放,那就poll,释放的时间为:keepAliveTime
            //否则,线程是不会被释放的,take一直被阻塞在这里,直到来了新任务继续工作
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            //到这里说明可被释放的线程等待超时,已经销毁,设置该标记,下次循环将线程数减少
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
复制代码

1.1.4 Executors

以上构造函数比较多,为了方便使用,juc提供了一个Executors工具类,内部提供静态方法

1)newCachedThreadPool() : 弹性线程数

2)newFixedThreadPool(int nThreads) : 固定线程数

3)newSingleThreadExecutor() : 单一线程数

4)newScheduledThreadPool(int corePoolSize) : 可调度,常用于定时

1.1.5 经典面试

1)线程池是如何保证线程不被销毁的呢?

答案:如果队列中没有任务时,核心线程会一直阻塞在获取任务的方法,直到返回任务。而任务执行完后,又会进入下一轮 work.runWork()中循环

验证:秘密就藏在核心源码里 ThreadPoolExecutor.getTask()

//work.runWork():
while (task != null || (task = getTask()) != null)
    
    
//work.getTask():
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();
复制代码

2)那么线程池中的线程会处于什么状态?

答案:RUNNABLE,WAITING

验证:起一个线程池,放置一个任务sleep,debug查看结束前后的状态

//debug add watcher:
((ThreadPoolExecutor) poolExecutor).workers.iterator().next().thread.getState()
复制代码
ThreadPoolExecutor poolExecutor = Executors.newFixedThreadPool(1);
poolExecutor.execute(new Runnable() {
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
System.out.println("ok");
复制代码

3)核心线程与非核心线程有区别吗?

答案:没有。被销毁的线程和创建的先后无关。即便是第一个被创建的核心线程,仍然有可能被销毁

验证:看源码,每个work在runWork()的时候去getTask(),在getTask内部,并没有针对性的区分当前work是否是核心线程或者类似的标记。只要判断works数量超出core,就会调用poll(),否则take()

1.2 Fork/Join

1.2.1 概念

​ ForkJoin是由JDK1.7后提供多线并发处理框架。可以理解为一种特殊的线程池。

​ 1.任务分割:Fork(分岔),先把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。

  2.合并结果:join,分割后的子任务被多个线程执行后,再合并结果,得到最终的完整输出。

1.2.2 组成

  • ForkJoinTask:主要提供fork和join两个方法用于任务拆分与合并;一般用子类 RecursiveAction(无返回值的任务)和RecursiveTask(需要返回值)来实现compute方法。

expediente

  • ForkJoinPool:调度ForkJoinTask的线程池;

expediente

  • ForkJoinWorkerThread:Thread的子类,存放于线程池中的工作线程(Worker);

expediente

  • WorkQueue:任务队列,用于保存任务;

1.2.3 基本使用

一个典型的例子:计算1-1000的和

package com.itheima.thread;

import java.util.concurrent.*;

public class SumTask {

    private static final Integer MAX = 100;

    static class SubTask extends RecursiveTask<Integer> {
        // 子任务开始计算的值
        private Integer start;

        // 子任务结束计算的值
        private Integer end;

        public SubTask(Integer start , Integer end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if(end - start < MAX) {
                //小于边界,开始计算
                System.out.println("start = " + start + ";end = " + end);
                Integer totalValue = 0;
                for(int index = this.start ; index <= this.end  ; index++) {
                    totalValue += index;
                }
                return totalValue;
            }else {
                //否则,中间劈开继续拆分
                SubTask subTask1 = new SubTask(start, (start + end) / 2);
                subTask1.fork();
                SubTask subTask2 = new SubTask((start + end) / 2 + 1 , end);
                subTask2.fork();
                return subTask1.join() + subTask2.join();
            }
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        Future<Integer> taskFuture =  pool.submit(new SubTask(1,1000));
        try {
            Integer result = taskFuture.get();
            System.out.println("result = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace(System.out);
        }
    }
}
复制代码

1.2.4 设计思想

  • 普通线程池内部有两个重要集合:工作线程集合(普通线程),和任务队列。

  • ForkJoinPool也类似,线程集合里放的是特殊线程ForkJoinWorkerThread,任务队列里放的是特殊任务ForkJoinTask

  • 不同之处在于,普通线程池只有一个队列。而ForkJoinPool的工作线程ForkJoinWorkerThread每个线程内都绑定一个双端队列。

expediente

  • 在fork的时候,也就是任务拆分,将拆分的task会被当前线程放到自己的队列中。

  • 如果有任务,那么线程优先从自己的队列里取任务执行,默认从队尾

  • 当自己队列中执行完后,工作线程会跑到其他队列的队首偷任务来执行。 也就是所说的 “窃取”

1.2.5 注意点

使用ForkJoin将相同的计算任务通过多线程执行。但是在使用中需要注意:

  • 注意任务切分的粒度,也就是fork的界限。并非越小越好
  • 判断要不要使用ForkJoin。任务量不是太大的话,串行可能优于并行。因为多线程会涉及到上下文的切换

1.3 原子操作

1.3.1 概念

​ 原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为"不可被中断的一个或一系列操作" 。类比于数据库事务,redis的multi。

1.3.2 CAS

​ Compare And Set(或Compare And Swap),翻译过来就是比较并替换,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。从第一视角来看,理解为:我认为位置 V 应该是 A,如果是A,则将 B 放到这个位置;否则,不要更改,只告诉我这个位置现在的值即可。所以cas内部一般伴随着while循环操作,不停的去尝试

​ juc中提供了Atomic开头的类,基于cas实现原子性操作,最基本的应用就是计数器

package com.itheima;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private static AtomicInteger i = new AtomicInteger(0);
    public int get(){
        return i.get();
    }
    public void inc(){
        i.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        final AtomicCounter counter = new AtomicCounter();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    counter.inc();
                }
            }).start();
        }
        Thread.sleep(3000);
        //可以正确输出10
        System.out.println(counter.i.get());
    }
}
复制代码

注:AtomicInteger源码。基于unsafe类cas思想实现,性能篇会讲到

1.3.3 atomic

​ 上面展示了AtomicInteger,关于atomic包,还有很多其他类型:

  • 基本类型
    • AtomicBoolean:以原子更新的方式更新boolean;
    • AtomicInteger:以原子更新的方式更新Integer;
    • AtomicLong:以原子更新的方式更新Long;
  • 引用类型
    • AtomicReference : 原子更新引用类型
    • AtomicReferenceFieldUpdater :原子更新引用类型的字段
    • AtomicMarkableReference : 原子更新带有标志位的引用类型
  • 数组
    • AtomicIntegerArray:原子更新整型数组里的元素。
    • AtomicLongArray:原子更新长整型数组里的元素。
    • AtomicReferenceArray:原子更新引用类型数组里的元素。
  • 字段
    • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
    • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
    • AtomicStampedReference:原子更新带有版本号的引用类型。

1.3.4 注意!

使用atomic要注意原子性的边界,把握不好会起不到应有的效果,原子性被破坏。

案例:原子性被破坏现象

package com.itheima;

import java.util.concurrent.atomic.AtomicInteger;

public class BadAtomic {
    AtomicInteger i = new AtomicInteger(0);
    static int j=0;

    public void badInc(){
        int k = i.incrementAndGet();
        try {
            Thread.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        j=k;
    }

    public static void main(String[] args) throws InterruptedException {
        BadAtomic atomic = new BadAtomic();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                atomic.badInc();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(atomic.j);
    }
}
复制代码

结果分析:

  • 每次都不一样,总之不是10
  • i是原子性的,没问题。但是再赋值,变成了两部操作,原子性被打破
  • 在badInc上加synchronized,问题解决

1.4 AQS

1.4.1 概念

​ 首先搞清楚,AbstractQueuedSynchronizer抽象的队列式同步器,是一个抽象类,这个类在java.util.concurrent.locks包。

​ 除了java自带的synchronized关键字之外,jdk提供的另外一种锁机制。如果需要自己实现灵活的锁逻辑,可以考虑使用AQS,非常的便捷。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable
复制代码

​ jdk中使用AQS的线程工具类很多,自旋锁、互斥锁、读锁写锁、信号量、通过类继承关系可以轻松查看,所以说,AQS是juc中很多类的基石。

expediente

1.4.2 原理

expediente

  • state:状态,int类型的成员变量,当state>0时表示锁有人占着,当state = 0时表示释放了锁。

  • 队列:拿不到锁的线程进队列。

1.4.3 源码

​ AQS使用了模板设计模式。只需要实现指定的锁获取方法即可,内部的机制AQS已帮你封装好。

(AQS源码idea中查看)

需要子类继承AQS,并实现的方法(protected):

protected boolean tryAcquire(int arg) //独占式获取同步状态
protected boolean tryRelease(int arg) //独占式释放同步状态
protected int tryAcquireShared(int arg) //共享式获取同步状态
protected boolean tryReleaseShared(int arg) //共享式释放同步状态

复制代码

使用时,调用的是父类的方法(public)

public final void acquire(int arg) //独享锁获取
public final boolean release(int arg) //独享锁释放
public final void acquireShared(int arg) //共享锁获取
public final boolean releaseShared(int arg) //共享锁释放

复制代码

源码分析

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //可共享式获取锁,外部调用,模板模式
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    //需要实现的部分,空protected方法,被上面的对外方法所调用
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    
    //同理,锁的释放,模板模式
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    
    //独占式获取
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    //独占式释放
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
   
    
}    
复制代码

1.4.4 经典面试

一个阿里面试题:自己实现一个锁,最大允许指定数量的线程并行运作。其他排队等候

package com.itheima;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MyLock extends AbstractQueuedSynchronizer {
    public MyLock(int count){
        setState(count);
    }

    @Override
    protected int tryAcquireShared(int arg) {
        //自旋,cas方式不停获取数量
        for (; ; ) {
            int current = getState();
            int newCount = current - arg;
            if (newCount < 0 || compareAndSetState(current, newCount)) {
                return newCount;
            }
        }
    }

    @Override
    protected boolean tryReleaseShared(int arg) {
        for (; ; ) {
            int current = getState();
            int newState = current + arg;
            if (compareAndSetState(current, newState)) {
                return true;
            }
        }
    }


    public static void main(String[] args) {
        final MyLock lock = new MyLock(3);

        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                public void run() {
                    lock.acquireShared(1);
                    try {
                        Thread.sleep(1000);
                        System.out.println("ok");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.releaseShared(1);
                    }

                }
            }).start();
        }
    }
}
复制代码

验证结果:虽然30个一次性start,但是会每1s输出3个ok,达到了并发控制

1.5 并发容器

juc中还包含很多其他的并发容器(了解)

1.ConcurrentHashMap

对应:HashMap

目标:代替Hashtable、synchronizedMap,使用最多,源码篇会详细讲解

原理:JDK7中采用Segment分段锁,JDK8中采用CAS+synchronized

2.CopyOnWriteArrayList

对应:ArrayList

目标:代替Vector、synchronizedList

原理:高并发往往是读多写少的特性,读操作不加锁,而对写操作加Lock独享锁,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性。

查看源码:volatile array,lock加锁,数组复制

3.CopyOnWriteArraySet

对应:HashSet

目标:代替synchronizedSet

原理:与CopyOnWriteArrayList实现原理类似。

4.ConcurrentSkipListMap

对应:TreeMap

目标:代替synchronizedSortedMap(TreeMap)

原理:基于Skip list(跳表)来代替平衡树,按照分层key上下链接指针来实现。

附加:跳表

5.ConcurrentSkipListSet

对应:TreeSet

目标:代替synchronizedSortedSet(TreeSet)

原理:内部基于ConcurrentSkipListMap实现,原理一致

6.ConcurrentLinkedQueue

对应:LinkedList

对应:无界线程安全队列

原理:通过队首队尾指针,以及Node类元素的next实现FIFO队列

7.BlockingQueue

对应:Queue

特点:拓展了Queue,增加了可阻塞的插入和获取等操作

原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒

实现类:

  • LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
  • ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
  • PriorityBlockingQueue:按优先级排序的队列
往期干货:

Este artículo es publicado por Chuanzhi Education Erudition Valley - Wild Architect Teaching and Research Team, ¡indique la fuente!

Supongo que te gusta

Origin juejin.im/post/7104155708098084872
Recomendado
Clasificación