多线程知识总结(synchronize,volatile,ThreadLocal,线程池,AQS)

目录

1、线程

线程和进程的区别

线程的生命周期和状态

线程的上下文切换:

sleep方法和wait方法的区别:

2、synchronized:

synchronized关键字的三种使用方式

synchronized实现原理:

3、ThreadLocal

ThreadLocal内存泄漏问题

4、线程池

5、Atomic原子类:

6、AQS

volatile

JMM内存模型

happen-before规则

7、常见线程池题目


1、线程

线程和进程的区别

进程是一个程序的执行过程,,系统运行一个程序既是一个进程从创建运行到消亡的过程

线程与进程相似,但是是比进程更小的执行单位,多个线程共享进程的堆和方法区,但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈

线程的生命周期和状态

线程的状态如下,就绪和运行中都笼统称为运行中状态,线程创建之后处于new状态,

调用start()方法后处于ready状态,可运行线程在获得cpu时间片(timeslice)后就处于running状态

调用wait()方法后线程进入waiting状态,如果调用的是wait(long millis)方法,则处于time_waiting超时等待状态,即在等待状态的基础上增加了超时限制,在达到超时时间后返回到running状态

当线程调同步方法时,如果没有获取到锁,则进入blocked阻塞状态

执行完run()方法后进入terminated终止状态

Java 线程状态变迁

线程的上下文切换:

如果多线程中线程的个数大于cpu核心的个数,而cpu核心在任意时刻只能被一个线程使用,为了让这些线程都得到有效执行,cpu采取的策略是给每个线程分配时间片并轮转的方式,当一个线程的时间片用完时就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换

当前任务在时间片用完,cpu将要切换到其他任务时,会保存自己的状态,以便下次再切回时可以重新加载这个任务的状态,任务从保存到下一个任务的加载的过程就是一次上下文切换

线程和进程上下文切换的区别

进程的一次系统调用:涉及到用户态和内核态的切换,所以,系统调用过程通常称为特权模式切换而不是上下文切换

进程的上下文切换比系统调用多了一步,上下文切换还需要把该进程的虚拟内存,栈等保存下来

线程与进程最大的区别在与,线程是调度的基本单位,而进程则是资源拥有的基本单位

线程上下文切换与进程上下文切换的区别:当只有一个线程时,线程切换就是进程切换,当一个进程的多个线程进行切换时,由于共享了虚拟内存等资源,因此这些不需要切换,只需要切换线程的私有数据,程序计数器等,因此能比进程切换消耗更少的资源,

这也是多线程替代多进程的一个优势。

关于上下文切换很好的文章:https://www.jianshu.com/p/4393a4537eca

线程死锁

两个线程分别持有对方的资源锁,产生死锁的四个条件

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
sleep方法和wait方法的区别:

sleep方法不会释放锁,执行完成后线程自动苏醒,类似于wait(timeout)超时等待后自动苏醒。通常用于暂停执行

wait方法会释放锁,释放后不会自动苏醒,需要调用notify或者notifyAll来唤醒

线程命名:如果使用线程池并且想给线程命名,可以自定义ThreadFactory

可以看到线程池默认的线程工厂,线程命名是用原子类来实现的,所以如果是线程池中线程,默认名字都是pool-pollNumber-thread-threadNumber

而如果直接创建线程,线程命名是用了一个自增的static常量,可以保证线程名字全局唯一

所以默认名字是Thread-threadNum

2、synchronized:

关于synchronized可以看如下文章:https://www.cnblogs.com/1013wang/p/11806019.html 写的很全面

synchronized实际上有两个队列waitSet和entryList。

1. 当多个线程进⼊同步代码块时,⾸先进⼊entryList

2. 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1

3. 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁

4. 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null

synchronized关键字的三种使用方式

1、修饰实例方法 作用于当前对象实例

2、修饰静态方法 给当前类加锁,会作用于类的所有对象实例,一个线程a调用一个实例对象的非静态synchronized方法,而线程b需要调用这个对象所属类的静态synchronized方法,不会发生互斥,因为访问静态synchronized方法占用的是当前类的锁,而访问非静态synchronized方法占用的是当前实例对象的锁

3、修饰代码块 需要指定加锁的对象

synchronized实现原理:

同步语句块的情况:

使用了monitorenter和monitorexit,monitorenter指向同步代码块的开始位置,monitorexit指向同步代码块的结束位置,执行monitorenter,线程获取monitor的持有权,当计数器为0时则可以成功获取,获取后将锁计数器设为1(+1,因为synchronized是可重入锁,同一线程在再次请求该锁时仍可以请求到,此时计数器+1,等执行结束时-1,直到为0时其他线程才可以获取该锁),执行monitorexit则将锁计数器更新为0,即锁被释放

修饰方法的情况:

ACC_SYNCHRONIZED 标识,指明了该方法是一个同步方法

ReentrantLock和synchronized区别:两者都是可重入锁

相⽐于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是⽤JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下⼏点:

1. 等待可中断,当持有锁的线程⻓时间不释放锁的时候,等待中的线程可以选择放弃等待,转⽽处理其他的任务。

2. 公平锁:synchronized和ReentrantLock默认都是⾮公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使⽤公平锁的话会导致性能急剧下降。

3. 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。

synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程

可以用来解决生产者消费者实现 Java多线程 ReentrantLock与Condition_java retreenlock condition-CSDN博客

3、ThreadLocal

ThreadLocal本身不存储值,他会被作为key放到ThreadLocalMap中,value即为对应的值,然后ThreadLocalMap由当前的线程所引用,ThreadLocalMap中维护了一个entry用来存储threadlocal的key和值的value

具体代码如下:

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护,用来解决子父线程共享变量问题
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}
。。。
ThreadLocal    
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
。。。
ThreadLocalMap
 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal内存泄漏问题

ThreadLocal可以理解为线程本地变量,他会在每个线程都创建⼀个副本,那么在线程之间访问内部副本变量就⾏了,做到了线程之间互相隔离,相⽐于synchronized的做法是⽤空间来换时间。ThreadLocal有⼀个静态内部类ThreadLocalMap,ThreadLocalMap⼜包含了⼀个Entry数组,Entry本身是⼀个弱引⽤,他的key是指向ThreadLocal的弱引⽤,Entry具备了保存key value键值对的能⼒。弱引⽤的⽬的是为了防⽌内存泄露,如果是强引⽤那么ThreadLocal对象除⾮线程结束否则始终⽆法被回收,弱引⽤则会在下⼀次GC的时候被回收。但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除⾮线程结束运⾏。但是只要ThreadLocal使⽤恰当,在使⽤完之后调⽤remove⽅法删除Entry对象,实际上是不会出现这个问题的。

另外其实只要这个线程对象及时被gc回收,这个内存泄露问题影响不大,但在threadLocal设为null到线程结束中间这段时间不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用,就可能出现内存泄露。

https://www.cnblogs.com/fsmly/p/11020641.html

InheritableThreadLocal

此变量同样由thread类所持有,用来解决子父线程共享数据问题,每当新开线程,就创建新的ThreadLocalMap与Entry,遍历原线程的Entry[],直接塞到新entry里(浅拷贝),key为原ThreadLocal对象,value为原值

赋值:直接将引用赋值给变量  浅拷贝:如果是基本数据类型则复制值,如果是引用类型则复制一个引用,和原引用指向同一个堆内存(对象) 深拷贝:复制引用和堆内存,对整个对象图进行拷贝,即不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。

https://www.cnblogs.com/shakinghead/p/7651502.html

ThreadLoca解决hash碰撞的方法:线性探测法

threadlocal是由线程持有一个threadlocalmap,其中key即是threadlocal,既然是map, 那么就可能产生hash碰撞,而threadlocal解决hash碰撞的方法就是如果当前i有值了

则会去尝试下一个位置i+1,循环探测直到找到空位置,如果达到上限len,则重置为0重新探测,使用了环形数组的设计(因为散列出来的是不规则的,因此需要循环检测)、

代码如下:

            // 遍历index位置的值,如果不为空则继续循环探测,
            //如果key值和当前key值相等,则替换并返回,否则如果e不为空但是key为空,则替换并返回
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
        
        // 寻找下一个位置索引,如果大于长度,则置为0

        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

4、线程池

线程池的好处:1降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的损耗 2提高响应速度3提高线程的可管理性

实现runnable和callable的区别:runnable没有返回值也不会抛出异常,callable可以

execute()和submit()方法的区别:execute方法用于提交不需要返回值的任务,无法判断执行是否成功,submit会返回一个future类型的对象,通过这个对象可以判断任务是否执行成功,get方法可以获取返回值,不过此方法会阻塞当前线程直到任务完成。实际上submit内部是创建了一个futuretask,然后将此对象传入execute对象中执行,然后返回此对象

线程池创建方法:1、通过构造方法实现,public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

corePoolSize:核心线程数,定义了最小可以同时运行的线程数量

maximumPoolSize:当队列中存放的线程数量达到队列容量的时候,当前可以同时运行的线程数变为最大线程数

keepAliveTime:当线程池中的线程数量大于corePoolSize时,核心线程外的线程不会立即销毁,而是会等待,等待时间超过此参数才会被销毁

workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到了则先存放到队列中

threadFactory:executor创建新线程的时候会用到

handler:饱和策略

2、通过Executor框架的工具类Executors来实现,可以创造三种类型的ThreadPoolExecutor 

FixedThreadPool:返回拥有固定线程数量的线程池,线程池重的线程数量始终不变,当有新的任务提交时,如果有空闲线程则立即执行,如果没有,则暂存在任务队列中,当有线程空闲时,则处理在任务队列中的任务

SingleThreadExecutor:返回只有一个线程的线程池,如果有多一个任务提交到该线程池,则保存在一个任务队列中,线程空闲的时候按照先入先出的顺序执行队列中的任务

CachedThreadPool:返回一个可根据实际情况调整线程池数量的的线程池,线程池线程数量不确定,如果有空闲的线程可复用,则优先使用可复用的线程,如果所有线程都在工作,又有新的任务提交,则创建新的线程处理工作,线程执行完毕后,返回线程池进行复用。

ThreadPoolExecutor饱和策略

ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。

ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。

ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

线程池执行逻辑如下图:

图解线程池实现原理

相关问题:1、线程池创建以后里面有线程吗,如果没有,有什么方法对线程池进行预热

如果线程池创建后没有任务过来,里面没有线程,如果需要预热可以调用如下方法:

一个是全部启动,一个是仅启动一个

2、核心线程数会被回收吗,需要什么设置

默认不会被回收,如果想回收可以调用allowCoreThreadTimeOut

3、如何配置合理线程数

要看要运行的任务是IO密集型还是CPU密集型

如果是CPU密集型(即需要大量的运算,而没有阻塞),则配置线程数=核心cpu数+1 即尽量配置比较少的线程数,在单核下配置多线程,任务速度不会加快,因为cpu运算能力就那些

如果是IO密集型,因为不是一直在执行任务(io多,很多阻塞),则可以配置尽量多的线程 比如 核心cpu数*2或者参考公式

cpu核数/1-阻塞系数             阻塞系数在0.8~0.9之间

5、Atomic原子类:

具有原子操作特性的类,操作不可中断

public final int get() //获取当前的值 public final int getAndSet(int newValue)//获取当前的值,并设置新的值 public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

atomicInteger原理:主要利用CAS(compare and swap)+volatile+native方法来保证原子操作,从而避免synchronize的高开销,执行效率大为提升

CAS属于乐观锁,对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

可能存在aba问题,即由A变成B,又由B变成A,用CAS发现不了这种改变,但是并不能说明其他线程没有改变过他,解决方法:java中通过添加一个是否改变的标记来解决

AtomicMarkableReference  定义了一个被volatile修饰的pair

private volatile Pair<V> pair;
private static class Pair<T> {
//封装了我们传递的对象
 final T reference;
//这个就是boolean标记
 final boolean mark;
 private Pair(T reference, boolean mark) {
 this.reference = reference;
 this.mark = mark;
 }
 static <T> Pair<T> of(T reference, boolean mark) {
 return new Pair<T>(reference, mark);
 }
}

它是通过把操作的对象和一个boolean类型的标记封装成Pair,而Pair有被volatile修饰,说明只要更改其他线程立刻可见,而只有Pair中的两个成员变量都相等,来解决CAS中ABA的问题的。

上面我们知道了AtomicMarkableReference是通过添加一个boolean类型标记和操作的对象封装成Pair来解决ABA问题的,但是如果想知道被操作对象更改了几次,这个类就无法处理了,因为它仅仅用一个boolean去标记,所以AtomicStampedReference就是解决这个问题的,它通过一个int类型标记来代替boolean类型的标记。

6、AQS

AQS是一个用来构建锁和同步器的框架

原理:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效线程,然后将共享资源设置为锁定状态,如果被请求的共享资源被占用,则需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS是通过CLH队列锁实现的,即将暂时取不到锁的线程加入到队列中

AQS原理图

AQS使用了模版方法模式:如果想自定义同步器,只需要继承AbstractQueuedSynchronizer并重写指定的方法,其实主要就是对于state的维护,state有点类似于计数器,通过volatile和CAS保证其原子性

有两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

AQS组件总结:

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。acquire(int permits):获取permits个许可,如果没有足够的许可,当前线程会被阻塞,直到有足够的许可。该方法会对许可数量进行减少。如果调用该方法时许可数量为0,则当前线程会一直被阻塞,直到有另一个线程释放了许可。release(int permits):释放permits个许可,使得其他被阻塞的线程可以获取许可。该方法会对许可数量进行增加。如果释放许可后,有被阻塞的线程可以获取许可,则会唤醒这些线程。
  • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

Java中的锁和偏向锁/轻量级锁/重量级锁

https://www.cnblogs.com/deltadeblog/p/9559035.html

volatile

volatile保证了变量的可见性 

使⽤volatile声明的变量,可以确保值被更新的时候对其他线程⽴刻可⻅。volatile使⽤内存屏障来保证不会发⽣指令重排,解决了内存可⻅性的问题。

如果x变量用volatile修饰,则线程A再次读取变量X的话,CPU就会根据缓存⼀致性协议强制线程A重新从主内存加载最新的值到⾃⼰的⼯作内存,⽽不是直接⽤缓存中的值。

不过并不能保证并发下线程安全,还是需要cas来保证,因此对于基本数据类型做count++这种操作,建议使用AtomicLong或者LongAdder等线程安全的原子类

阿里为什么推荐使用LongAdder,而不是volatile? - 知乎

JMM内存模型

本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加⼊了⾼速缓存,⾼速缓存⼀般可以分为L1、L2、L3三级缓存。基于上⾯的例⼦我们知道了这导致了缓存⼀致性的问题,所以加⼊了缓存⼀致性协议,同时导致了内存可⻅性的问题,⽽编译器和CPU的重排序导致了原⼦性和有序性的问题,JMM内存模型正是对多线程操作下的⼀系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程

序在不同的平台下达到⼀致的内存访问效果,同时也是保证在⾼效并发的时候程序能够正确执⾏。

原⼦性:Java内存模型通过read、load、assign、use、store、write来保证原⼦性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。

可⻅性:可⻅性的问题在上⾯的回答已经说过,Java保证可⻅性可以认为通过volatile、synchronized、final来实现。

有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。

happen-before规则

JMM可以通过happens-before关系提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。简单的说就是Java虚拟机会对指令重排做出⼀些规则限制,并不能让所有的指令都随意的改变执⾏位置,具体如下:

1. 单线程每个操作,happen-before于该线程中任意后续操作

2. volatile写happen-before于后续对这个变量的读

3. synchronized解锁happen-before后续对这个锁的加锁

4. final变量的写happen-before于final域对象的读,happen-before后续对final变量的读

5. 传递性规则,A先于B,B先于C,那么A⼀定先于C发⽣

7、常见线程池题目

题目描述参考答案

按序打印

三个不同的线程 A、B、C 将会共用一个 Foo 实例。

线程 A 将会调用 first() 方法
线程 B 将会调用 second() 方法
线程 C 将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
 

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

多线程-难度middle

打印零和奇偶数

给你类 ZeroEvenOdd 的一个实例,该类中有三个函数:zero、even 和 odd 。ZeroEvenOdd 的相同实例将会传递给三个不同线程:

线程 A:调用 zero() ,只输出 0
线程 B:调用 even() ,只输出偶数
线程 C:调用 odd() ,只输出奇数
修改给出的类,以输出序列 "010203040506..." ,其中序列的长度必须为 2n 

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

编写一个Java函数,该函数可以批量执行100个任务,每个任务对同一个变量count执行+1操作,返回最终count的结果。要求:

  1. 任务需要分批次并发执行,一批执行完之后才能执行下一批

  2. 每批次任务最多允许5个任务同时执行

  3. 所有任务执行结束后,才能返回count结果

  4. 需要考虑多线程安全问题

class Task {
    public Task () {

    }
}
public class Test {
    public static void main (String[] args) {
        List<Task> tasks = new ArrayList<>(10);
        for (int i = 0; i < 100; i++) {
            tasks.add(new Task());
        }
        System.out.println(runBatch(10, tasks));
    }

    public static int runBatch (int batchSize, List<Task> tasks) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<List<Task>> batches = getBatches(tasks, batchSize);
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        AtomicInteger count = new AtomicInteger(0);
        Semaphore semaphore = new Semaphore(5);

        for (List<Task> batch : batches) {
            for (Task page : batch) {
                final CompletableFuture<Void> listCompletableFuture = CompletableFuture.runAsync(() -> {
                    try {
                        semaphore.acquire();
                        count.incrementAndGet();
                        semaphore.release();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }, executorService);
                futures.add(listCompletableFuture);
            }
            futures.forEach(CompletableFuture::join);
        }
        return count.get();
    }

    private static List<List<Task>> getBatches (List<Task> tasks, int batchSize) {
        final int size = tasks.size();
        int batchNum = size % batchSize == 0 ? size / batchSize : size / batchSize + 1;
        List<List<Task>> batches = new ArrayList<>();
        for (int i = 0; i < batchNum; i++) {
            final List<Task> batchs = tasks.subList(i * batchSize, Math.min((i + 1) * batchSize, tasks.size()));
            batches.add(batchs);
        }
        return batches;
    }
}

上述问题有多种解决方案,具体分为以下几种

synchronized

class ZeroEvenOdd {
    private int n;
    private volatile int flag = 1;
    
    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            synchronized (this) {
                while (flag % 2 == 0) wait();
                printNumber.accept(0);
                flag ++;
                notifyAll();
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            synchronized (this) {
                while (flag % 4 != 0) wait();
                printNumber.accept(i);
                flag ++;
                notifyAll();
            }
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            synchronized(this) {
                while (flag % 4 != 2) wait();
                printNumber.accept(i);
                flag ++;
                notifyAll();
            }
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

LockSupport

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.LockSupport;

class ZeroEvenOdd {
    private int n;
    volatile int flag = 1;
    ConcurrentHashMap<Integer, Thread> map = new ConcurrentHashMap<>();

    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        map.putIfAbsent(1, Thread.currentThread());

        for (int i=1; i<=n; i++) {
            while (flag % 2 == 0) LockSupport.park();
            printNumber.accept(0);
            flag ++;
            if (flag % 4 == 0) LockSupport.unpark(map.get(0));
            if (flag % 4 == 2) LockSupport.unpark(map.get(1));
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        map.putIfAbsent(0, Thread.currentThread());
        for (int i=2; i<=n; i+=2) {
            while (flag % 4 != 0) LockSupport.park();
            printNumber.accept(i);
            flag ++;
            LockSupport.unpark(map.get(1));
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            while (flag % 4 != 2) LockSupport.park();
            printNumber.accept(i);
            flag ++;
            LockSupport.unpark(map.get(1));
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

yield + 原子类

import java.util.concurrent.atomic.AtomicInteger;

class ZeroEvenOdd {
    private int n;
    volatile AtomicInteger dice = new AtomicInteger(1);


    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            while (dice.get() % 2 == 0) Thread.yield();
            printNumber.accept(0);
            dice.getAndIncrement();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            while (dice.get() % 4 != 0) Thread.yield();
            printNumber.accept(i);
            dice.getAndIncrement();
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            while (dice.get() % 4 != 2) Thread.yield();
            printNumber.accept(i);
            dice.getAndIncrement();
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 ReentrantLock + Condition

class ZeroEvenOdd {
    private int n;
    private volatile int flag = 1;
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    
    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            lock.lock();
            printNumber.accept(0);
            flag ++;
            condition.signalAll();
            while (flag %2 == 0) condition.await();
            lock.unlock();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            lock.lock();
            while (flag % 4 != 0) condition.await();
            printNumber.accept(i);
            flag ++;
            condition.signalAll();
            lock.unlock();
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            lock.lock();
            while (flag % 4 != 2) condition.await();
            printNumber.accept(i);
            flag ++;
            condition.signalAll();
            lock.unlock();
        }       
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Semaphore

import java.util.concurrent.Semaphore;

class ZeroEvenOdd {
    private int n;
    Semaphore[] semaphores = new Semaphore[3];

    public ZeroEvenOdd(int n) {
        this.n = n;
        semaphores[0] = new Semaphore(1);
        semaphores[1] = new Semaphore(0);
        semaphores[2] = new Semaphore(0);
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            semaphores[0].acquire();
            printNumber.accept(0);
            if (i % 2 == 0) semaphores[1].release();
            else semaphores[2].release();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i=i+2) {
            semaphores[1].acquire();
            printNumber.accept(i);
            semaphores[0].release();
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i=i+2) {
            semaphores[2].acquire();
            printNumber.accept(i);
            semaphores[0].release();
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SynchronousQueue

import java.util.concurrent.SynchronousQueue;

class ZeroEvenOdd {
    private int n;
    private SynchronousQueue<Integer>[] queues = new SynchronousQueue[3];

    public ZeroEvenOdd(int n) {
        this.n = n;
        for (int i=0; i<queues.length; i++) queues[i] = new SynchronousQueue<>();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            printNumber.accept(0);
            if (i % 2 == 1) {
                queues[1].put(1);
            } else {
                queues[2].put(1);
            }
            queues[0].take();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            queues[2].take();
            printNumber.accept(i);
            queues[0].put(1);
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            queues[1].take();
            printNumber.accept(i);
            queues[0].put(1);
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

LinkedBlockingQueue

import java.util.concurrent.LinkedBlockingQueue;

class ZeroEvenOdd {
    private int n;
    private LinkedBlockingQueue<Integer>[] queues = new LinkedBlockingQueue[3];

    public ZeroEvenOdd(int n) {
        this.n = n;
        for (int i=0; i<queues.length; i++) {
            queues[i] = new LinkedBlockingQueue<>(1);
        }
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        queues[0].put(1);
        for (int i=1; i<=n; i++) {
            queues[0].take();
            printNumber.accept(0);
            if (i % 2 == 0) {
                queues[2].put(1);
            } else {
                queues[1].put(1);
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            queues[2].take();
            printNumber.accept(i);
            if (i < n) queues[0].put(1);
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            queues[1].take();
            printNumber.accept(i);
            if (i < n) queues[0].put(1);
        }
    }
}

作者:社会边角料
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/1623560/by-be_a_better_coder-axp4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

CyclicBarrier

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值