(六):并发编程底层原理

1.知道AQS吗?能否介绍下,它的核心思想是什么?

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS 内部数据和方法,可以简单拆分为:
1.一个 volatile 的整数成员表征状态,同时提供了 setState 和 getState 方法

private volatile int state;

2.一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是 AQS 机制的核心之一。
3.各种基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法。

例子:线程A调用了lock()方法,通过CAS将state赋值为1,然后将该锁标记为线程A加锁。如果线程A还未释放锁时,线程B来请求,会查询锁标记的状态,因为当前的锁标记为 线程A,线程B未能匹配上,所以线程B会加入阻塞队列,直到线程A触发了 unlock() 方法,这时线程B才有机会去拿到锁,但是不⼀定肯定拿到。

2.你知道的AQS有几种同步方式,实现同步器⼀般要覆盖哪些方法?

独占式: 比如ReentrantLock。
共享式:比如Semaphore。
存在组合:组合式的如ReentrantReadWriteLock,AQS为使用提供了底层支撑,使用者可以自由组装实现。

不需要全部实现,根据获取的锁的种类可以选择实现不同的方法,比如
实现支持独占锁的同步器应该实现tryAcquire()、 tryRelease()、isHeldExclusively()。
实现支持共享获取的同步器应该实现tryAcquireShared()、tryReleaseShared()、isHeldExclusively()。

3.源码解析ReentrantLock实现原理

参考ReentrantLock非公平锁源码解析

4.ReentrantLock和synchronized使用的场景是什么,实现机制有什么不同?

ReentrantLock和synchronized都是独占锁

synchronized:

1、是悲观锁会引起其他线程阻塞,java内置关键字,
2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平。
3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活。
4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁。
5、synchronized操作的应该是对象头中mark word。

ReentrantLock:

1、是个Lock接口的实现类,是悲观锁,
2、可以判断是否获取到锁,可重入、可判断、可公平可不公平
3、需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用,在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致 其他线程无法获得该锁。
5、创建的时候通过传进参数true创建公平锁,如果传入的是false或没传参数,则创建的是非公平锁。
6、底层不同是AQS的state和FIFO队列来控制加锁。

5.知道ReentrantReadWriteLock吗?和ReentrantLock有啥不同?

ReentrantReadWriteLock

1、读写锁接口ReadWriteLock的⼀个具体实现,实现了读写锁的分离。
2、支持公平和非公平,底层也是基于AQS实现。
3、允许从写锁降级为读锁。
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
4、重入:获取了读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁又可以获取读锁
核心:读锁是共享的,写锁是独占的。 读和读之间不会互斥,读和写、写和读、写和写之
间才会互斥,主要是提升了读写的性能
ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据, 线程B在读数据造成的数据不一致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口。

6.知道阻塞队列BlockingQueue不?介绍下常见的阻塞队列?

BlockingQueue: java.util.concurrent包下的提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现,都是基于阻塞队列实现的。
1、当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满。
2、从阻塞队列读数据时,如果队列为空,线程将会阻塞等待直到队列里面是非空的时候。

另一个 BlockingQueue 经常被考察的点,就是是否有界(Bounded、Unbounded),这一点也往往会影响我们在应用开发中的选择,我这里简单总结一下。
1、ArrayBlockingQueue 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建 ArrayBlockingQueue 时,都要指定容量,如

public ArrayBlockingQueue(int capacity, boolean fair)

2、LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为 Integer.MAX_VALUE,成为了无界队列。
3、SynchronousQueue,这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。
4、PriorityBlockingQueue 是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响。
5、DelayedQueue 和 LinkedTransferQueue 同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是 put 操作永远也不会发生其他 BlockingQueue 的那种等待情况。
如果我们分析不同队列的底层实现,BlockingQueue 基本都是基于锁实现,一起来看看典型的 LinkedBlockingQueue。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

前面介绍了各种队列实现,在日常的应用开发中,如何进行选择呢?
以 LinkedBlockingQueue、ArrayBlockingQueue 和 SynchronousQueue 为例,我们一起来分析一下,根据需求可以从很多方面考量:
1、考虑应用场景中对队列边界的要求。ArrayBlockingQueue 是有明确的容量限制的,而 LinkedBlockingQueue 则取决于我们是否在创建时指定,SynchronousQueue 则干脆不能缓存任何元素。
2、从空间利用角度,数组结构的 ArrayBlockingQueue 要比 LinkedBlockingQueue 紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。通用场景中,LinkedBlockingQueue 的吞吐量一般优于 ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
3、ArrayBlockingQueue 实现比较简单,性能更好预测,属于表现稳定的“选手”。
4、如果我们需要实现的是两个线程之间接力性(handoff)的场景,你可能会选择CountDownLatch,但是SynchronousQueue也是完美符合这种场景的,而且线程间协调和数据传输统一起来,代码更加规范。
5、可能令人意外的是,很多时候 SynchronousQueue 的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。

7.你知道非阻塞队列ConcurrentLinkedQueue不,它怎么实现线程安全的?

ConcurrentLinkedQueue是基于链表实现的无界线程安全队列,采用FIFO进行排序
1、底层结构是Node,链表头部和尾部节点是head和tail,节点变量和内部类属性使用volatile声明保证了有序和可见性。

private static class Node{
        volatile E item;
        volatile Node next;
}

private transient volatile Node head;

private transient volatile Node tail;

2、插入、移除、更新操作使用CAS无锁操作,保证了原子性。
3、假如多线程并发修改导致 CAS 更新失败,采用for循环插入保证更新操作成功。例如offer(E e)方法:

 public boolean offer(E e) {
        checkNotNull(e);
        final Node newNode = new Node(e);

        for (Node t = tail, p = t;;) {
            Node q = p.next;
            if (q == null) {
                // p is last node
                if (p.casNext(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this queue,
                    // and for newNode to become "live".
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)
                // We have fallen off list.  If tail is unchanged, it
                // will also be off-list, in which case we need to
                // jump to head, from which all live nodes are always
                // reachable.  Else the new tail is a better bet.
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

8.说出你遵循的多线程最佳实践。

1、给不同模块的线程起名称,方便后续排查问题。
2、使用同步代码块或者同步的方法的时候,尽量减小同步范围。
3、多用并发集合少用同步集合。
同步集合:Hashtable/Vector/同步工具类包装Collections.synXXX
并发集合:ConcurrentHashMap、CopyOnWriteArrayList
4、线上业务需要使用多线程,优先考虑线程池是否更加合适,然后判断哪种线程池比较好,最后才是自己创建单一线程。

9.Java 并发类库提供的线程池有哪几种? 分别有什么特点?

Executors 目前提供了 5 种不同的线程池创建配置:

1、newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
2、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
3、newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
4、newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
5、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因?

1、newFixedThreadPool和newSingleThreadExecutor:队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM
2、newScheduledThreadPool和newCachedThreadPool:线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM

ThreadPoolExecutor构造函数里面的参数及作用。

1、corePoolSize,所谓的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了 allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如 newFixedThreadPool 会将其设置为 nThreads,而对于 newCachedThreadPool 则是为 0。
2、maximumPoolSize,顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对于 newFixedThreadPool,当然就是 nThreads,因为其要求是固定大小,而 newCachedThreadPool 则是 Integer.MAX_VALUE。
3、keepAliveTime 和 TimeUnit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。
4、workQueue,工作队列,必须是 BlockingQueue。
5、threadFactory:创建新线程时使用的工厂。
6、handler: RejectedExecutionHandler是⼀个接口且只有⼀个方法,线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认有4种策略AbortPolicy、
CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。

public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)

线程池大小的选择策略

线程池大小不合适,太多或太少,都会导致麻烦,所以我们需要去考虑一个合适的线程池大小。虽然不能完全确定,但是有一些相对普适的规则和思路。
1、如果我们的任务主要是进行计算,那么就意味着 CPU 的处理能力是稀缺的资源,我们能够通过大量增加线程数提高计算能力吗?往往是不能的,如果线程太多,反倒可能导致大量的上下文切换开销。所以,这种情况下,通常建议按照 CPU 核的数目 N 或者 N+1。
2、如果是需要较多等待的任务,例如 I/O 操作比较多,可以参考 Brain Goetz 推荐的计算方法:

线程数 = CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间)

3、上面是仅仅考虑了 CPU 等限制,实际还可能受各种系统资源限制影响,如果我们不能调整资源的容量,那么就只能限制工作线程的数目了。这里的资源可以是文件句柄、内存等。
不要把解决问题的思路全部指望到调整线程池上,很多时候架构上的改变更能解决问题。

你可能感兴趣的:((六):并发编程底层原理)