JUC必须掌握的重要问题!!!

介绍一下线程池的几大参数

下面看一下参数最多的 一个线程方法

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){
}
  1. corePoolSize 核心线程数目 (最多保留的线程数)

  2. maximumPoolSize 最大线程数目(核心线程数加上救急线程数)

  3. keepAliveTime 救急线程的生存时间(核心线程没有生存时间这个东西,核心线程会一直运行)

  4. unit 时间单位 - 针对救急线程

  5. workQueue 阻塞队列(存放任务)

    1. 有界阻塞队列 ArrayBlockingQueue
    2. 无界阻塞队列 LinkedBlockingQueue
    3. 最多只有一个同步元素的 SynchronousQueue
    4. 优先队列 PriorityBlockingQueue
  6. threadFactory 线程工厂 - 可以为线程创建时起个好名字,不然只能使用默认名

  7. handler 拒绝策略

参数的设计:
1:核心线程数(corePoolSize)
核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理;
2:任务队列长度(workQueue)任务队列长度一般设计为:核心线程数/单个任务执行时间2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200;
3:最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)单个任务执行时间;既: 最大线程数=(1000-200)*0.1=80个;
4:最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;

image-20220601084139974
AQS抢锁底层逻辑

AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
|注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
AQS实现的具体方式如下:
image-20220601095049203

如图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:getState();setState();compareAndSetState();
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
不同的自定义的同步器争用共享资源的方式也不同。
AQS底层使用了模板方法模式。同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
    这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

AQS抢锁底层逻辑的答案:
ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。
以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。在acquire() acquireShared()两种方式下,线程在等待队列中都是忽略中断的,**acquireInterruptibly()/acquireSharedInterruptibly()是支持响应中断的。

原子类的底层

在多线程的场景中,我们需要保证数据安全,就会考虑同步的方案,通常会使用synchronized或者lock来处理,使用了synchronized意味着内核态的一次切换。这是一个很重的操作。 有没有一种方式,可以比较便利的实现一些简单的数据同步,比如计数器等等。concurrent包下的atomic提供我们这么一种轻量级的数据同步的选择。

class MyThread implements Runnable {
    static AtomicInteger ai=new AtomicInteger(0);
    public void run() {
        for (int m = 0; m < 1000000; m++) {
            ai.getAndIncrement();
        }
    }
};
public class TestAtomicInteger {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt);
        Thread t2 = new Thread(mt);
        t1.start();
        t2.start();
        Thread.sleep(500);
        System.out.println(MyThread.ai.get());
    }
}

在以上代码中,使用AtomicInteger声明了一个全局变量,并且在多线程中进行自增,代码中并没有进行显示的加锁。以上代码的输出结果,永远都是2000000。如果将AtomicInteger换成Integer,打印结果基本都是小于2000000。 也就说明AtomicInteger声明的变量,在多线程场景中的自增操作是可以保证线程安全的。接下来我们分析下其原理。
原理: 这里,我们来看看AtomicInteger是如何使用非阻塞算法来实现并发控制的。AtomicInteger的关键域只有一下3个:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    /**
     * Creates a new AtomicInteger with the given initial value.
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }
    ......
}

这里, unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。 valueOffset是用来记录value本身在内存的编译地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。value是用来存储整数的时间变量,这里被声明为volatile。volatile只能保证这个变量的可见性。不能保证他的原子性。 可以看看getAndIncrement这个类似i++的函数,可以发现,是调用了UnSafe中的getAndAddInt。

    /**
     * Atomically increments by one the current value.@return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    /**
     * Atomically sets to the given value and returns the old value.
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
            //使用unsafe的native方法,实现高效的硬件级别CAS
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
        return var5;
    }

如何保证原子性:自旋 + CAS(乐观锁)。在这个过程中,通过compareAndSwapInt比较更新value值,如果更新失败,重新获取旧值,然后更新.
优缺点:CAS相对于其他锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu资源会消耗很高。换句话说,CAS+自旋适合使用在低并发有同步数据的应用场景。

Java 8做出的改进和努力: 在Java 8中引入了4个新的计数器类型,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator。他们都是继承于Striped64。
在LongAdder 与AtomicLong有什么区别?
Atomic*遇到的问题是,只能运用于低并发场景。因此LongAddr在这基础上引入了分段锁的概念。可以参考《JDK8系列之LongAdder解析》一起看看做了什么。 大概就是当竞争不激烈的时候,所有线程都是通过CAS对同一个变量(Base)进行修改,当竞争激烈的时候,会将根据当前线程哈希到对于Cell上进行修改(多段锁)。

在多线程同时操作一个变量的时候,类似++这种方式,用锁就显得大材小用了,所以用到了atomic原子类,可以保证在多线程的情况下,安全高性能的执行程序更新变量,atomic原子类的底层不是传统意义上的锁机制,而是无锁化的cas机制,CAS 全称compare and set 比较后再设置.简单理解就是多线程操作的情况下先获取一个值,看谁先发起cas操作,判断时候和获取的时候值一样 如果一样就修改值,如果不一样就重新获取再进行修改.jdk1.8之前内就是通过cas机制不断地循环判断,这样在并发量高的情况下很容易造成资源和性能的浪费,所以在1.8之后对atomci原子类进行了优化加入了分段锁的概念在LongAdder底层实现过程中,有个base值,当并发量提升之后就会分段实施cas操作,把基数分不到多个段中,这样就降低了更新同一个数值时的cas操作,当一段cas操作失败之后就会去其他段进行操作,如果获取总值就把剩下的加起来

cas基于哪个类

了解CAS,首先要清楚JUC,那么什么是JUC呢?JUC就是java.util.concurrent包的简称。它核心就是CAS与AQS。CAS是java.util.concurrent.atomic包的基础,如AtomicInteger、AtomicBoolean、AtomicLong等等类都是基于CAS。什么是CAS呢?全称Compare And Swap,比较并交换。CAS有三个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。
java中CAS操作依赖于Unsafe类,Unsafe类所有方法都是native的,直接调用操作系统底层资源执行相应任务,它可以像C一样操作内存指针,是非线程安全的。
Java中无锁操作CAS基于以下3个方法实现:

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);       public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
unsafe底层

JDK底层的unsafe是什么?
小陈:哦,unsafe?unsafe是个啥东西啊?我还没有接触过
老王:说起unsafe啊,是JDK提供的一个工具类,unsafe里面的方法大多是native方法,你可以理解为unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。
(1)比如说通过unsafe可以让操作系统直接给你分配内存、释放内存。
(2)突破java语法本身的限制,直接从内存级别去操作堆里面的某个对象的数据;
(3)调用操作系统的CAS指令,实现CAS的功能
(4)操作系统层次将线程挂起和恢复
(5)提供操作系统级别的内存屏障(之前说过的Load屏障和Store屏障),读取数据强制走主存,修改数据直接刷新到主存
总之unsafe就相当于JDK给你提供的一个直接跟操作系统打交道的一个工具类,通过unsafe可做一些非常底层的指令和行为。
小陈:额,竟然可以直接通过unsafe分配内存,那岂不是不需要通过堆内存也可以直接分配内存了吗?这样岂不是很危险,万一使用者分配大量的内存,没有及时回收,岂不是很容易造成内存溢出的风险?又或者分配了内存,但是忘记回收了,容易造成内存泄露啊
老王:是啊,unsafe提供了很多操作系统级别的方法,在提供使用者便利的同时,也是隐藏着很多风险的。
但是unsafe提供的这些操作系统级别的方法对于JDK底层的一些工具类、上层的一些框架来说在实现层方便了许多。
比如著名的并发基础工具类AQS底层就是通过unsafe提供的CAS操作来进行加锁的,加锁失败的线程又是通过unsafe提供的park、unpark操作将线程挂起和唤醒的,还有一些非常著名的开源框架比如netty分配直接内存的方式底层也还是通过unsafe分配直接内存。
老王:所以啊,去了解一下unsafe底层的一些操作还是很有必要的,对于后面我们要学习的很多线程安全的类,比如Atomic系列的类、基于AQS一些列的同步工具还是很有必要的,因为这些底层都是通过unsafe提供的操作去实现的。
老王:下面啊,我就分几类将一些unsafe提供的一些重要功能

1.unsafe直接分配和释放内存

2.unsafe提供的CAS操作

老王:这块内容比较重要,JUC提供的很多Atomic原子类基于AQS实现的并发工具,底层都是通过CAS操作去实现的。下面我们就说说unsafe提供的cas操作:假如目前有一个Test类是这样子的:

    public class Test {
        private DemoClass demo;
        private int intValue;
        private long longValue;    
    }

有一个Test类的对象 Test o = new Test();这个时候想要突破java语法的限制,直接修改对象o的private修饰的demo属性。可以通过CAS操作直接去修改对象o里面的demo属性,使用unsafe提供的下面方法:(1)o就是你要操作的对象(2)offset就是demo属性在对象o内部的位置,或者偏移量(3)expected就是demo期待的值(4)x就是你希望设置的新值,只有demo的值 == expect的时候,才能将demo的值设置成x

public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);      

执行CAS操作大致是这样的,根据 对象o的地址,demo属性相对于o的偏移量offset,直接计算得到demo所在内存的位置,然后直接将demo的值从内存取出进行CAS(比较替换操作):

image-20220601114159530

同理对于,执行CAS操作替换Test类对象o内部的int值和long值,unsafe提供了如下两个方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,  long x);

底层执行CAS替换的原理跟上面画图的demo其实是一样的,这里就不再赘述了。

3.unsafe将线程挂起和恢复
unsafe类提供类将一个线程挂起、讲一个挂起的线程唤醒的方法,分别是park和unpark,我们看如下的代码:
park方法:

    //线程调用该方法,线程将一直阻塞直到被唤醒,或者超时,或者中断条件出现。  
    public native void park(boolean isAbsolute, long time);  

(1) isAbsolute是否是绝对时间,当isAbsolute == true ,后面time的时间单位是ms;当为false的时候,后面time参数的时间单位是ns。(2)time > 0时候,表示大概要将线程挂起time的时间,过了时间后自动将线程唤醒。当time = 0的时候,表示一直将线程挂起,直到有人调用unpark方法将线程唤醒。
unpark方法:public native void unpark(Object thread);// 直接将正在被挂起的thread线程唤醒,让它继续干活

LockSupport"LockSupport是对unsafe中park和unpark功能封装的一个工具类,提供了阻塞和唤醒功能。我们可以直接使用LockSupport的方法达到挂起和恢复线程的效果,LockSupport方法的源码如下:

public class LockSupport {
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        // 这里直接调用unsafe的park方法将线程挂起
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    public static void unpark(Thread thread) {
        if (thread != null)
            // 直接调用unsafe的unpark方法将线程唤醒
            UNSAFE.unpark(thread);
    }
}

由于我们自己编写的java程序不能直接使用unsafe工具类,所以JDK还是有一些工具类对unsafe类的功能进行封装,然后我们就直接使用这些封装的工具类即可。

4.内存屏障

说说你所了解的锁,从不同维度做个概括,比如重入与可重入,并说说这些锁都有哪些实现

(1)公平锁/非公平锁

在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,而AbstractQueuedSynchronizer又继承自AbstractOwnableSynchronizer,其中AbstractOwnableSynchronizer是提供了设置占用当前锁的线程信息的方法,主要的锁的实现还是在AbstractQueuedSynchronizer中实现的,在AbstractQueuedSynchronizer中通过CLH队列实现了多线程锁的排队使用,但是该队列的实现并不能保证锁的公平竞争,但是在某些业务场景中会需要保证先到的线程先得到锁,所以就有了公平锁和非公平锁的诞生。
通过分析ReentrantLock中的公平锁和非公平锁的实现,其中tryAcquire是公平锁和非公平锁实现的区别,下面的两种类型的锁的tryAcquire的实现,从中我们可以看出在公平锁中,每一次的tryAcquire都会检查CLH队列中是否仍有前驱的元素,如果仍然有那么继续等待,通过这种方式来保证先来先服务的原则;而非公平锁,首先是检查并设置锁的状态,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与排队线程中的对头线程竞争(但是排队的线程是先来先服务的),所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经等待的线程们是仍然保证先来先服务的,所以总结一下公平锁和非公平锁的区别:1、公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。2、非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。

(2)可重入锁/不可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。例如

// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的.可重入降低了编程复杂性
public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (this) {
					System.out.println("第1次获取锁,这个锁是:" + this);
					int index = 1;
					while (true) {
						synchronized (this) {
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
						}
						if (index == 10) {
							break;
						}
					}
				}
			}
		}).start();
	}
public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					lock.lock();
					System.out.println("第1次获取锁,这个锁是:" + lock);
					int index = 1;
					while (true) {
						try {
							lock.lock();
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
							try {
								Thread.sleep(new Random().nextInt(200));
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							if (index == 10) {
								break;
							}
						} finally {
							lock.unlock();
						}
					}
				} finally {
					lock.unlock();
				}
			}
		}).start();
	}

可以发现没发生死锁,可以多次获取相同的锁

可重入锁有: 1.synchronized 2.ReentrantLock

使用ReentrantLock的注意点:ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样

  • 独享锁/共享锁

AQS实现锁机制并不是通过synchronized——给对象加锁实现的,事实上它仅仅是一个工具类!它没有使用更高级的机器指令,也不靠关键字,更不依靠JDK编译时的特殊处理,仅仅作为一个普普通通的类就完成了代码块的访问控制。AQS使用标记位+队列的方式,记录获取锁、竞争锁、释放锁等一些类锁操作。但更准确的说,AQS并不关心什么是锁,对于AQS来说它只是实现了一系列的用于判断资源是否可以访问的API,并且封装了在访问资源受限时,将请求访问的线程加入队列、挂起、唤醒等操作。AQS关心的问题如下:资源不可访问时,怎么处理?资源时可以被同时访问,还是在同一时间只能被一个线程访问?如果有线程等不及资源了,怎么从AQS队列中退出?
至于资源能否被访问的问题,则交给子类去实现。
站在使用者的角度,AQS的功能主要分为两类:独占锁和共享锁。在它的所有子类中,要么实现了它的独占功能的API,要么实现了共享功能的API,但不会同时使用两套API,即使是ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别使用两套API来实现的。
当AQS的子类实现独占功能时,如ReentrantLock,资源是否可以被访问被定义为:只要AQS的state变量不为0,并且持有锁的线程不是当前线程,那么代表资源不可访问。
当AQS的子类实现共享功能时,如CountDownLatch,资源是否可以被访问被定义为:只要AQS的state变量不为0,那么代表资源不可以为访问。
独占锁
ReentrantLock是AQS独占功能的一个实现,通常的使用方式如下:

reentrantLock.lock();
// do something
reentrantLock.unlock();

ReentrantLock会保证执行do something在同一时间有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。ReentrantLock的加锁全部委托给内部代理类完成,ReentrantLock只是封装了统一的一套API而已,而ReentrantLock又分为公平锁和非公平锁。
共享锁
共享功能的主要实现为CountDownLatch,CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示所有需要等待的时间都已经发生。如果计数器值非零,那么await会一直阻塞直到计数器为零,或者等待线程中断,或者等待超时。

  • 互斥锁/读写锁(一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁)

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:
线程进入读锁的前提条件:没有其他线程的写锁;没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:没有其他线程的读锁;没有其他线程的写锁
读写锁有以下三个重要的特性:(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。(2)重进入:读锁和写锁都支持线程重进入。(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。 仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

  • 乐观锁/悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景:从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式:

  1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。当需要对账户信息表进行更新的时候,需要首先读取version字段。
操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
操作员 A 完成了修改工作,提交更新之前会先看数据库的版本和自己读取到的版本是否一致,一致的话,就会将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
操作员 B 完成了操作,提交更新之前会先看数据库的版本和自己读取到的版本是否一致,但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,而自己读取到的版本号为1 ,不满足 “ 当前最后更新的version与操作员第一次读取的版本号相等 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

  1. CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数: 需要读写的内存值 V;进行比较的值 A;拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点:
1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

  • 分段锁

总是记不清分段锁与ConcurrentHashMap的实现原理,今天来用自己的理解类比一下ConcurrentHashMap中分段锁的实现。ConcurrentHashMap使用了分段锁来保证线程安全,效率比起使用synchronized的HashTable要高的很多。每个集合都可以看作是一个存储东西的房子,HashTable与ConcurrentHashMap存储的都是HashEntry数组(每个数组里面是链表,暂且忽略,直到就好)。
一.HashTable: 在HashTable这个房子中,只有一个房间,就像是一个大仓库,里面是一大长列的存放Entry的货架(数组);只要有一个人进了这个房间,他就会把这个房间锁起来,直到这个人在房间里面做完了事情出来之后才会把门打开。此间如果有其他人想要进去,就只能在外面等这个人把房门打开,然后才能进去。这样的话会导致外面等了很多人,效率不高。
二.ConcurrentHashMap:在ConcurrentHashMap这个房子中,有许多的房间,每个房间都存着一部分的Entry货架(Entry数组的不同段,将一整个的Entry数组分开了),而这些房间各自又有着不同的锁。一个人在访问某一个房间的时候,会把这个房间锁起来,其他的房间依然是可以进去访问的,这样就大大的提高了效率。
在HashTable中,如果线程A想要访问Entry数组前面位置的元素,线程B想要访问数组尾部的位置的元素,但是A先进房间访问了,那么房子就被锁了,B不得不等待。在ConcurrentHashMap中,A进到了前面位置元素所在的房间访问,B仍然可以去尾部元素所在的房间,因为他们处在不同的房间。
分段锁在我的理解中是先分段再锁,将原本的一整个的Entry数组分成了若干段,分别将这若干段放在了不同的新的Segment数组中(分房间),每个Segment有各自的锁,以此提高效率。

image-20220601161752883
  • 偏向锁/轻量级锁/重量级锁

在 Java 中主要2种加锁机制:
synchronized 关键字
java.util.concurrent.Lock (Lock是一个接口,ReentrantLock是该接口一个很常用的实现)

这两种机制的底层原理存在一定的差别
synchronized 关键字通过一对字节码指令 monitorenter/monitorexit 实现, 这对指令被 JVM 规范所描述。
java.util.concurrent.Lock 通过 Java 代码搭配sun.misc.Unsafe 中的本地调用实现的

锁的状态:无锁状态;偏向锁状态;轻量级锁状态;重量级锁状态.四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。要注意的是,这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。每种状态在并发竞争情况下需要消耗的资源由低到高,性能由高到低。重量级锁需要通过操作系统在用户态与核心态之间切换,就像它的名字是一个重量级操作,这也是synchronized效率不高的原因,JDK1.6对synchronized进行了优化,引入了偏向锁与轻量级锁,提高了性能降低了资源消耗。

偏向锁?:通过对大量数据的分析可以发现,大多数情况下锁竞争是不会发生的,往往是一个线程多次获得同一个锁,于是引入了偏向锁,偏向锁不会被刻意的释放,如果没有竞争,线程再次请求锁时可以直接获得锁。
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,然后优先使用轻量级锁,若是失败了才换回重量级锁。
什么是重量级锁?重量级锁在JVM中有一个监视器(Monitor),保持了两个队列:锁竞争队列和信号阻塞队列,一个实现线程互斥,另一个实现线程同步。重量级锁在底层是靠操作系统的Mutex Lock实现的,线程在阻塞和唤醒状态间切换需要操作系统将线程在用户态与核心态之间转换,成本很高,所以最早的synchronized效率不高。

image-20220601182346292
  • 自旋锁 VS 适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
image-20220601182643326

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自适应自旋锁:所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数.

了解哪些并发工具

介绍几个并发编程常用的工具类,它们分别是:1.CountDownLatch(闭锁,我觉得叫门闩更好理解)2.CyclicBarrier(栅栏)3.Semophore(信号量)4.Exchanger(交换器). 都位于java.util.concurrent.Semaphore工具类提供了一种并发流程控制的手段。 Exchanger工具类则提供了在线程间交换数据的手段。
1.等待多线程完成的CountDowmLatch:CountDownLatch允许一个或多个线程等待其他线程完成操作。在JDK1.5之后的并发包中提供了CountDowmLatch可以实现join的功能,并且比join功能更多。

public class CountDowmLatchTest{
	static CountDowmLatch c  = new CountDownLatch(2);
	public static void main(String[]arg)throw InterruptedException{
			new Thread(new Runnable()){
				@Override
				public void rim(){
					System.out.println(1);
					c.countDown();
					System.out.println(2);
					c.countDowm();
				}
			}).start();
			c.await();
			System.out.pringln("3");
	}
}

CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待n个点完成。就传入N。当我们调用CountDownLatch的countDown方法时,N就会减少1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里锁的N个点可以是N个线程,也可以是1个线程里面的N个步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
2.同步屏障CyclicBarrier:CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。
它要做的事情是,让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。
CyclicBarrier[循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线
程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执
行。跟CountdownLatch一样,但这个可以重用

CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
    for (int i=0;i<3;i++){
      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.控制并发线程数的Semaphore:Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

public static void main(String[] args) {
    // 1. 创建 semaphore 对象
    Semaphore semaphore = new Semaphore(3);
    // 2. 10个线程同时运行
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        // 3. 获取许可
        try {
          semaphore.acquire();
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
        try {
          log.debug("running...");
          sleep(1);
          log.debug("end...");
       } finally {
          // 4. 释放许可
          semaphore.release();
       }
     }).start();
   }
 }

image-20220601193727831

4.线程间交换数据的Exchanger:Exchanger(交换者)是一个用于线程间协作的工具类。Exchager用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchanger方法,当两个线程都到达同步点,这两个线程可以交换数据,将本线程生产出来的数据传递给对方。

说说可重入锁如何实现

什么是重入锁?:通常情况下,锁可以用来控制多线程的访问行为。那对于同一个线程,如果连续两次对同一把锁进行lock,会怎么样了?对于一般的锁来说,这个线程就会被永远卡死在那边,比如:

void handle() {
    lock();
    lock();  //和上一个lock()操作同一个锁对象,那么这里就永远等待了
    unlock();
    unlock();
}

这个特性相当不好用,因为在实际的开发过程中,函数之间的调用关系可能错综复杂,一个不小心就可能在多个不同的函数中,反复调用lock(),这样的话,线程就自己和自己卡死了。所以,对于希望傻瓜式编程的我们来说,重入锁就是用来解决这个问题的。重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死。因此,如果我们使用的是重入锁,那么上述代码就 可以正常工作。你唯一需要保证的,就是unlock()的次数和lock()一样多。这样是不是方便很多呢?Java中的锁都来自与Lock接口,如下图中红框内的,就是重入锁。image-20220602154450297

重入锁提供的最重要的方法就是lock()

  • void lock():加锁,如果锁已经被别人占用了,就无限等待。

    这个lock()方法,提供了锁最基本的功能,拿到锁就返回,拿不到就等待。因此,大规模得在复杂场景中使用,是有可能因此死锁的。因此,使用这个方法得非常小心。

    如果要预防可能发生的死锁,可以尝试使用下面这个方法:

  • boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断。

    这是一个比单纯lock()更具有工程价值的方法,如果大家阅读过JDK的一些内部代码,就不难发现,tryLock()在JDK内部被大量的使用。

    与lock()相比,tryLock()至少有下面一个好处:

  • 当然了,当锁使用完后,千万不要忘记把它释放了。不然,程序可能就会崩溃啦~
    void unlock() :释放锁
    此外, 重入锁还有一个不带任何参数的tryLock()。
    public boolean tryLock():这个不带任何参数的tryLock()不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false,特别适合在应用层自己对锁进行管理,在应用层进行自旋等待。

重入锁的实现原理(重要):重入锁内部实现的主要类如下图:
image-20220602154835966

重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。实现重入锁的方法很简单,就是基于一个状态变量state。这个变量保存在AbstractQueuedSynchronizer对象中:
private volatile int state;
当这个state==0时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock()的最简单的实现是:

 final void lock() {
 // compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
 if (compareAndSetState(0, 1))
     setExclusiveOwnerThread(Thread.currentThread());
 else
 //如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待
     acquire(1);
}
//下面是acquire()  的实现:
 public final void acquire(int arg) {
 //tryAcquire() 再次尝试获取锁,
 //如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
 //同时宣布获得所成功,这正是重入的关键所在
 if (!tryAcquire(arg) &&
     // 如果获取失败,那么就在这里入队等待
     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
     //如果在等待过程中 被中断了,那么重新把中断标志位设置上
     selfInterrupt();
}

公平的重入锁:默认情况下,重入锁是不公平的。如果你是一个公平主义者,强烈坚持先到先得的话,那么你就需要在构造重入锁的时候,指定这是一个公平锁:ReentrantLock fairLock = new ReentrantLock(true);这样一来,每一个请求锁的线程,都会乖乖的把自己放入请求队列,而不是上来就进行争抢。但一定要注意,公平锁是有代价的。维持公平竞争是以牺牲系统性能为代价的。如果你愿意承担这个损失,公平锁至少提供了一种普世价值观的实现吧!那公平锁和非公平锁实现的核心区别在哪里呢?来看一下这段lock()的代码:

//非公平锁 
 final void lock() {
     //上来不管三七二十一,直接抢了再说
     if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
     else
         //抢不到,就进队列慢慢等着
         acquire(1);
 }
 //公平锁
 final void lock() {
     //直接进队列等着
     acquire(1);
 }
//从上面的代码中也不难看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。
//对于tryLock()也是非常类似的:
 //非公平锁 
 final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {
          //上来不管三七二十一,直接抢了再说
          if (compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      //如果就是当前线程占用了锁,那么就更新一下state,表示重复占用锁的次数
      //这是“重入”的关键所在
      else if (current == getExclusiveOwnerThread()) {
          //我又来了哦~~~
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          setState(nextc);
          return true;
      }
      return false;
  }
 
 //公平锁
 protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     if (c == 0) {
         //先看看有没有别人在等,没有人等我才会去抢,有人在我前面 ,我就不抢啦
         if (!hasQueuedPredecessors() &&
             compareAndSetState(0, acquires)) {
             setExclusiveOwnerThread(current);
             return true;
         }
     }
     else if (current == getExclusiveOwnerThread()) {
         int nextc = c + acquires;
         if (nextc < 0)
             throw new Error("Maximum lock count exceeded");
         setState(nextc);
         return true;
     }
     return false;
 }

总结:1.对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致。2.默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁3.重入锁的内部实现是基于CAS操作的。

说说AQS,等待队列如何保证线程安全

state状态

项目中用过哪些juc实现类(待解决)
说说线程池

new Thread的弊端如下:
a. 每次new Thread新建对象性能差。b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。c. 提供定时执行、定期执行、单线程、并发数控制等功能。
Java 线程池:Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池的作用:线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排 队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
1、Executor、ExecutorService和ScheduledExecutorService,它们都是接口,它们的关系是ScheduledExecutorService继承ExecutorService而ExecutorService 又继承Executor。
2、对于Executor接口,它只有一个方法void execute(Runnable command);而其后的ExecutorService和ScheduledExecutorService就各自增加了各自需要的方法。
其中ExecutorService主要是跟线程池有关,而ScheduledExecutorService是用来执行定时任务的。
3、由于它们都是接口,所以要实例化的话都要有个普通类,但是Java提供了一个工厂类Executors专门生成各种Executor,形如:
ScheduledExecutorService se = Executors.newSingleThreadScheduledExecutor();
ExecutorService es = Executors.newCachedThreadPool();
ExecutorService es = Executors.newFixedThreadPool(threadnum);

在Java 5之后,并发编程引入了一堆新的启动、调度和管理线程的API。Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。
一、Executor的UML图:(常用的几个接口和子类)image-20220602202346101

Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
二、Executor和ExecutorService
Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类,一般来说,Runnable任务开辟在新线程中的使用方法为:new Thread(new RunnableTask())).start(),但在Executor中,可以使用Executor而不用显示地创建线程:executor.execute(new RunnableTask()); // 异步执行
ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
通过 ExecutorService.submit() 方法返回的 Future 对象,可以调用isDone()方法查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get()获取结果,在这种情况下,get()将阻塞,直至结果准备就绪,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。ExecutorService 部分代码如下:

public interface ExecutorService extends Executor {
	void shutdown();
	<T> Future<T> submit(Callable<T> task);
	<T> Future<T> submit(Runnable task, T result);
	<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
}

三、Executors类: 主要用于提供线程池相关的操作

Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
1、public static ExecutorService newFiexedThreadPool(int Threads) 创建固定数目线程的线程池。
2、public static ExecutorService newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
3、public static ExecutorService newSingleThreadExecutor():创建一个单线程化的Executor。
4、public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

newCachedThreadPool() :缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中.缓存型池子通常用于执行一些生存期很短的异步型任务, 因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。能reuse的线程,必须是timeout IDLE内的池中线程,缺省 timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。
newFixedThreadPool(int) :-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子-和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器.从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:fixed池线程数固定,并且是0秒IDLE(无IDLE).cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE .
newScheduledThreadPool(int): -调度型线程池 -这个池子里的线程可以按schedule依次delay执行,或周期执行
SingleThreadExecutor(): -单例线程,任意时间池中只能有一个线程 -用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)
四、Executor VS ExecutorService VS Executors
正如上面所说,这三者均是 Executor 框架中的一部分。Java 开发者很有必要学习和理解他们,以便更高效的使用 Java 提供的不同类型的线程池。总结一下这三者间的区别,以便大家更好的理解:
Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。
Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。可以通过 《Java Concurrency in Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。
Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
下面给出一个Executor执行Callable任务的示例代码:

public class CallableDemo{   
    public static void main(String[] args){   
        ExecutorService executorService = Executors.newCachedThreadPool();   
        List<Future<String>> resultList = new ArrayList<Future<String>>();   
        //创建10个任务并执行   
        for (int i = 0; i < 10; i++){   
            //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中   
            Future<String> future = executorService.submit(new TaskWithResult(i));   
            //将任务执行结果存储到List中   
            resultList.add(future);   
        }   
        //遍历任务的结果   
        for (Future<String> fs : resultList){   
                try{   
                    while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成  
                    System.out.println(fs.get());     //打印各个线程(任务)执行的结果   
                }catch(InterruptedException e){   
                    e.printStackTrace();   
                }catch(ExecutionException e){   
                    e.printStackTrace();   
                }finally{   
                    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务  
                    executorService.shutdown();   
                }   
        }   
    }   
}    
class TaskWithResult implements Callable<String>{   
    private int id;   
  
    public TaskWithResult(int id){   
        this.id = id;   
    }   
    /**  
     * 任务的具体过程,一旦任务传给ExecutorService的submit方法, 则该方法自动在一个线程上执行 
     */   
    public String call() throws Exception {  
        System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());   
        //该返回结果将被Future的get方法得到  
        return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName(); 
    }   
}  

五、自定义线程池:自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池,这里先贴上示例程序:

public class ThreadPoolTest{   
    public static void main(String[] args){   
        //创建等待队列   
        BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);   
        //创建线程池,池中保存的线程数为3,允许的最大线程数为5  
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);   
        //创建七个任务   
        Runnable t1 = new MyThread();Runnable t2 = new MyThread();Runnable t3 = new MyThread();Runnable t4 = new MyThread();Runnable t5 = new MyThread();Runnable t6 = new MyThread();Runnable t7 = new MyThread();   
        //每个任务会在一个线程上执行  
        pool.execute(t1);    pool.execute(t2);   pool.execute(t3);   pool.execute(t4);   pool.execute(t5);pool.execute(t6); pool.execute(t7);   
        //关闭线程池   
        pool.shutdown();   
    }   
}   
class MyThread implements Runnable{   
    @Override   
    public void run(){   
        System.out.println(Thread.currentThread().getName() + "正在执行。。。");   
        try{   
            Thread.sleep(100);   
        }catch(InterruptedException e){   
            e.printStackTrace();   
        }   
    }   
}  

运行结果如下:image-20220602204004778

从结果中可以看出,七个任务是在线程池的三个线程上执行的。这里简要说明下用到的ThreadPoolExecuror类的构造方法中各个参数的含义。public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue workQueue)
corePoolSize:线程池中所保存的核心线程数,包括空闲线程。maximumPoolSize:池中允许的最大线程数。keepAliveTime:线程池中的空闲线程所能持续的最长时间。unit:持续时间的单位。workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。
根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法将一个Runnable任务添加到线程池中时,按照如下顺序来处理:
1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务
2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);
3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;
4、如果线程池中的线程数量等于了maximumPoolSize,有4种处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。
总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

什么场景需要什么样的拒绝策略

中止策略:无特殊场景。
丢弃策略:无关紧要的任务(博客阅读量)。
弃老策略:发布消息。
调用者运行策略:不允许失败场景(对性能要求不高、并发量较小)。
1.AbortPolicy中止策略:丢弃任务并抛出RejectedExecutionException异常。
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
功能:当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程.
使用场景:这个就没有特殊的场景了,但是有一点要正确处理抛出的异常。ThreadPoolExecutor中默认的策略就是AbortPolicy,ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
2.DiscardPolicy丢弃策略:ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
功能:直接静悄悄的丢弃这个任务,不触发任何动作。
使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了。
3.DiscardOldestPolicy弃老策略:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。
4.CallerRunsPolicy调用者运行策略:由调用线程处理该任务。
功能:当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
5.dubbo中的线程拒绝策略。当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因。
(1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在。
(2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
(3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性。
6.Netty中的线程池拒绝策略。Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常。
7.activeMq中的线程池拒绝策略。activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。
8.pinpoint中的线程池拒绝策略。pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。

对JUC的理解

image-20220602213036959image-20220602213320958image-20220602213512607

在Java 5.0 提供了java.util.concurrent(简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection 实现等。JUC目的就是为了更好的支持高并发任务,让开发者利用这个包进行的多线程编程时可以有效的减少竞争条件和死锁线程。
1,tools(工具类):又叫信号量三组工具类,包含有:1)CountDownLatch(闭锁) 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待2)CyclicBarrier(栅栏) 之所以叫barrier,是因为是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。3)Semaphore(信号量) 是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
2,executor(执行者):是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService,里面包含的类有:1)ScheduledExecutorService 解决那些需要任务重复执行的问题2)ScheduledThreadPoolExecutor 周期性任务调度的类实现
3,atomic(原子性包):是JDK提供的一组原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。
4,locks(锁包):是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁包含的实现类有:1)ReentrantLock 它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。2)ReentrantReadWriteLock 它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。3)LockSupport 它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
5,collections(集合类):主要是提供线程安全的集合,比如:1)ArrayList对应的高并发类是CopyOnWriteArrayList,2)HashSet对应的高并发类是 CopyOnWriteArraySet,3)HashMap对应的高并发类是ConcurrentHashMap等等

说说信号量

1 Semaphore的概述:public class Semaphore extends Object implements Serializable
Semaphore来自于JDK1.5的JUC包,直译过来就是信号量,被作为一种多线程并发控制工具来使用。Semaphore可以控制同时访问共享资源的线程个数,线程通过 acquire方法获取一个信号量,信号量减一,如果没有就等待;通过release方法释放一个信号量,信号量加一。它通过控制信号量的总数量,以及每个线程所需获取的信号量数量,进而控制多个线程对共享资源访问的并发度,以保证合理的使用共享资源。相比synchronized和独占锁一次只能允许一个线程访问共享资源,功能更加强大,有点类似于共享锁!
2 Semaphore的原理
2.1 基本结构
image-20220602224504508

根据uml类图,可以很明显的看出来Semaphore和CountDownLatch一样都是直接使用AQS实现的。区别就是Semaphore还分别实现了公平模式FairSync和非公平模式NonfairSync两个内部类。实际上公平与非公平只是在获取信号量的时候得到体现,它们的释放信号量的方法都是一样的,这就类似于ReentrantLock:公平与非公平只是在获取锁的时候得到体现,它们的释放锁的方法都是一样的!或许这里有人在想,信号量是不是可以看作锁资源呢?某些时候这么看是没问题的,比如都是获取了只有获取了“信号量”或者“锁”才能访问共享资源,但是它们又有区别,锁资源会和线程绑定,而信号量则不会和线程绑定。在构造器部分,如同CountDownLatch 构造函数传递的初始化计数个数count被赋给了AQS 的state 状态变量一样,Semaphore的信号量个数permits同样赋给了AQS 的state 值。在创建Semaphore时可以使用一个fair变量指定是否使用公平策略,默认是非公平的模式。公平模式会确保所有等待的获取信号量的线程按照先进先出的顺序获取信号量,而非公平模式则没有这个保证。非公平模式的吞吐量比公平模式的吞吐量要高,而公平模式则可以避免线程饥饿。

private final Sync sync;//保存某个AQS子类实例
/**
 * 创建具有给定的信号量数和非公平的公平模式的 Semaphore。
 * @param permits 初始的可用信号量数目。此值可能为负数,在这种情况下,必须在授予任何获取信号量前进行释放信号量。
 */
public Semaphore(int permits) {
    sync = new NonfairSync(permits);//默认初始化NonfairSync实例
}
/**
 * 创建具有给定的信号量数和给定的公平设置的 Semaphore。
 * @param permits 初始的可用信号量数目。此值可能为负数,在这种情况下,必须在授予任何获取信号量前进行释放信号量。
 * @param fair    如果此信号量保证在争用时按先进先出的顺序授予信号量,则为 true;否则为 false。
 */
public Semaphore(int permits, boolean fair) {
    //根据fair参数选择初始化一个公平FairSync类或者非公平NonfairSync类的实例
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//非公平模式的实现
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;
    NonfairSync(int permits) {
        super(permits);
    }
    //…………其他方法后面再讲
}
//公平模式的实现
static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;
    FairSync(int permits) {
        super(permits);
    }
    //…………其他方法后面再讲
}
//信号量的同步实现。 使用 AQS 的state状态表示信号量。子分类为公平和非公平模式。
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;
    //构造器@param permits 初始的可用信号量数目。
    Sync(int permits) {
        //被设置为state值
        setState(permits);
    }
    //…………其他方法后面再讲
}

2.2 可中断获取信号量
public void acquire():可中断的获取一个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒该线程或者线程被中断。获取一个信号量就立即返回,将可用的信号量数减 1。 如果调用此方法时已被中断或者等待时被中断,则抛出 InterruptedException,并且清除当前线程的已中断状态。
public void acquire(int permits):可中断的获取permits 个信号量。
内部调用AQS的acquireSharedInterruptibly方法,这实际上就是共享式可中断获取资源的模版方法,因此Semaphore和CountDownLatch一样都是基于共享资源模式。

/*Semaphore的acquire方法
 * 从信号量获取一个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒或者线程被中断。
 * @throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public void acquire() throws InterruptedException {
    //内部调用AQS的acquireSharedInterruptibly方法,这实际上就是共享式可中断获取资源模版方法
    sync.acquireSharedInterruptibly(1);
}
/*从信号量获取permits个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒或者线程被中断。
 * @param permits 需要获取的信号量数量
 * @throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    //参数就是permits
    sync.acquireSharedInterruptibly(permits);
}
/**
 1. AQS的acquireSharedInterruptibly方法
 2. 共享式可中断获取信号量资源的模版方法
 3.  4. @param arg 需要获取的信号量资源数量
 5. @throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //最开始就检查一次,如果当前线程是被中断状态,直接抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //调用tryAcquireShared尝试获取共享信号量资源,这个方法是子类自己重写的
    //如果返回值小于0,表示当前线程共享信号量资源失败,否则表示成功
    //Semaphore的FairSync和NonfairSync对tryAcquireShared分别做出了公平和不公平的实现
    if (tryAcquireShared(arg) < 0)
        //获取不到就执行doAcquireSharedInterruptibly方法
        doAcquireSharedInterruptibly(arg);
}

在获取共享信号量资源的时候,Semaphore还实现了公平模式和非公平模式!它们的实现实际上和lock锁的实现中锁资源的公平、非公平获取非常类似!
2.2.1 公平模式
公平模式调用FairSync的tryAcquireShared方法!如果我们学习了AQS、ReentrantLock、ReadWriteLock的源码,我们第一个就会发现hasQueuedPredecessors方法,这个方法是AQS为实现公平模式的预定义的方法,AQS帮我们实现好了,该方法用于查询是否有任何线程等待获取信号量资源的时间超过当前线程。
大概步骤为:
1.开启一个死循环:
2.调用hasQueuedPredecessors方法,判断是否有线程比当前线程更早地请求获取信号量资源。如果该方法返回true,则表示有线程比当前线程更早地请求获取信号量资源,由于是公平的的,因此当前线程不应该获取信号量资源,直接返回-1,表示获取信号量资源失败。
3.到这里还没有返回,表示当前线程就是最早请求获取信号量资源,可以尝试获取。
4.获取state的值available,我们知道state代表信号量资源数量。remaining为available减去需要获取的信号量资源数量之后的差值。
5.如果remaining小于0,那么返回remaining值,由于是负数,因此获取失败,如果大于等于0,那么表示可以获取成功,尝试CAS的更新state,更新成功之后同样返回remaining,由于是大于等于0的数,因此获取成功。
6.如果remaining大于等于0,但是CAS更新state失败,那么循环重试。

原理还是很简单的,就是判断目前的信号量资源数量—state的值,是否满足要获取的信号量资源数量,acquire()方法默认获取1个资源。获取到了就是CAS的原子性的将state递减,否则表示获取资源失败,那么可能会阻塞。但是我们也会发现:如果remaining大于等于0,但是CAS更新state失败,那么会循环重试,这里为什么要重试呢?实际上我们的在AQS文章的“可重入共享锁的实现” 部分已经讲过:因为可能会有多个线程同时获取信号量资源,但是由于CAS只能保证一次只有一个线程成功,因此其他线程必定失败,但此时,实际上还是存在剩余的信号量资源没有被获取完毕的,因此让其他线程重试,相比于直接加入到同步队列中,对于信号量资源的利用率更高!

//公平模式
static final class FairSync extends Sync {
    /* 尝试公平的获取共享信号量资源
     * @param acquires 获取信号量资源数量
     * @return 如果返回值小于0,表示当前线程共享信号量资源失败,否则表示成功
     */
    protected int tryAcquireShared(int acquires) {
        /*开启一个循环尝试获取共享信号量资源*/
        for (; ; ) {
            //这是AQS实现公平模式的预定义的方法,AQS帮我们实现好了。该方法用于查询是否有任何线程等待获取信号量资源的时间超过当前线程.如果该方法返回true,则表示有线程比当前线程更早地请求获取信号量资源。由于是公平的的,因此当前线程不应该获取信号量资源,直接返回-1,表示获取信号量资源失败
            if (hasQueuedPredecessors())
                return -1;
            //到这里,表示当前线程就是最早请求获取信号量资源,可以尝试获取.获取state的值available,我们知道state代表信号量资源数量
            int available = getState();
            //remaining为available减去需要获取的信号量资源数量之后的差值
            int remaining = available - acquires;
            //如果remaining小于0,那么返回remaining值,由于是负数,因此获取失败.如果大于等于0,那么表示可以获取成功,尝试CAS的更新state,更新成功之后同样返回remaining,由于是大于等于0的数,因此获取成功
            if (remaining < 0 || compareAndSetState(available, remaining)) return remaining;
            //如果remaining大于等于0,但是CAS更新state失败,那么循环重试
        }
    }
}

2.2.2 非公平模式

非公平模式调用NonfairSync的tryAcquireShared方法!
相比于公平模式的实现,少了hasQueuedPredecessors的判断。可以想象:如果某线程A 先调用了aquire()方法获取信号量,但是如果当前信号量个数为0,那么线程A 会被放入AQS 的同步队列阻塞。过一段时间后线程B调用了release()方法释放了一个信号量,他它会唤醒队列中等待的线程A,但是这时线程C又调用了aquire()方法。如果采用非公平策略,那么线程C就会和线程A 去竞争这个信号量资源。由nonfairTryAcquireShared的代码可知,线程C完全可以在线程A 被激活前,或者激活后先于线程A 获取到该信号量,也就是在这种模式下阻塞线程和当前请求的线程是竞争关系,而不遵循先来先得的策略。另外,非公平模式的具体实现是在父类Sync中的nonfairTryAcquireShared方方法,为什么该方法要实现在父类中的,因为无论是指定的公平模式还是非公平模式,它们的tryAcquire方法都是调用的nonfairTryAcquireShared方法,即非公平的,因此实现在父类中!

// 非公平模式
static final class NonfairSync extends Sync {
    /* 尝试非公平的获取共享信号量资源
     * @param acquires 获取信号量资源数量
     * @return 如果返回值小于0,表示当前线程共享信号量资源失败,否则表示成功
     */
    protected int tryAcquireShared(int acquires) {
        //调用父类Sync的nonfairTryAcquireShared方法
        //为什么该方法要实现在父类中的,因为无论是指定的公平模式还是非公平模式,
        //它们的tryAcquire方法都是调用的nonfairTryAcquireShared方法,即非公平的,因此实现在父类中
        return nonfairTryAcquireShared(acquires);
    }
}
//AQS的实现,作为公平和非公平模式的父类,有一些共享方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    /** 尝试非公平的获取共享信号量资源
     * @param acquires 获取信号量资源数量
     * @return 如果返回值小于0,表示当前线程共享信号量资源失败,否则表示成功
     */
    final int nonfairTryAcquireShared(int acquires) {
        /*开启一个循环尝试获取共享信号量资源*/
        for (; ; ) {
            //相比于公平模式,少了hasQueuedPredecessors的实现
            //获取state的值available,我们知道state代表信号量资源数量
            int available = getState();
            //remaining为available减去需要获取的信号量资源数量之后的差值
            int remaining = available - acquires;
            //如果remaining小于0,那么返回remaining值,由于是负数,因此获取失败
            //如果大于等于0,那么表示可以获取成功,尝试CAS的更新state,更新成功之后同样返回remaining,由于是大于等于0的数,因此获取成功
            if (remaining < 0 || compareAndSetState(available, remaining)) return remaining;
            //如果remaining大于等于0,但是CAS更新state失败,那么循环重试
        }
    }
}

2.3 不可中断获取信号量
public void acquireUninterruptibly()不可中断的获取一个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒该线程。获取一个信号量就立即返回,将可用的信号量数减 1。相比于acquire()方法,该方法不响应中断,不会抛出InterruptedException.
public void acquireUninterruptibly(int permits):不可中断的获取permits个信号量。
相比于acquire方法,acquireUninterruptibly方法不响应中断,不会抛出InterruptedException。实际上内部调用AQS的acquireShared方法,这实际上就是共享式获取资源的模版方法(和acquireSharedInterruptibly对应)。

//获取一个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒该线程。获取一个信号量就立即返回,将可用的信号量数减 1。
public void acquireUninterruptibly() {
    //内部调用AQS的acquireShared方法.这实际上就是共享式不可中断获取资源模版方法
    sync.acquireShared(1);
}
//AQS的acquireShared方法.共享式不可中断获取资源模版方法 @param arg 获取的资源数量
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  //并没有检查中断
        doAcquireShared(arg);
}
//获取permits个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒该线程。@param permits 获取的信号量数量
public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    //参数就是permits
    sync.acquireShared(permits);
}

2.4 超时可中断获取信号量
public boolean tryAcquire(long timeout, TimeUnit unit):超时可中断的获取一个信号量,没有则一直阻塞,直到在其他线程提供信号量并唤醒该线程或者线程被中断或者阻塞超时。获取一个信号量就立即返回,将可用的信号量数减 1。如果调用此方法时已被中断或者等待时被中断,则抛出 InterruptedException,并且清除当前线程的已中断状态。
public boolean tryAcquire(int permits,long timeout,TimeUnit unit):超时可中断的获取permits 个信号量。
实际上内部调用AQS的tryAcquireSharedNanos方法,这实际上就是共享式超时可中断获取资源的模版方法。

/**@param timeout 超时时间@param unit 时间单位@return 是否获取资源成功@throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
    //实际上就是调用的AQS的共享式超时获取资源的方法,获取1个资源
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/** @param permits 获取的资源数量@param timeout 超时时间@param unit  时间单位@return 是否获取资源成功@throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    //实际上就是调用的AQS的共享式超时获取资源的方法,获取permits个资源
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
/* AQS的共享式超时获取资源的模版方法,支持中断 @param arg  参数@param nanosTimeout 超时时间,纳秒 @return 是否获取资源成功@throws InterruptedException 如果调用此方法时已被中断或者等待时被中断
 */
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    //最开始就检查一次,如果当前线程是被中断状态,直接抛出异常
    if (Thread.interrupted()) throw new InterruptedException();
    //下面是一个||运算进行短路连接的代码,同样左边是调用子类实现的tryAcquireShared尝试获取资源,获取到了直接返回true.获取不到资源就执行doAcquireSharedNanos方法,这个方法是AQS的方法,因此超时机制是AQS帮我们实现的!
    return tryAcquireShared(arg) >= 0 ||doAcquireSharedNanos(arg, nanosTimeout);
}

2.5 尝试获取信号量
public boolean tryAcquire():仅在调用时至少存在至少一个可用信号量,才尝试获取一个信号量。
public boolean tryAcquire(int permits):仅在调用时至少存在permits个的信号量,才尝试获取permits个信号量。
实际上内部就是直接调用的nonfairTryAcquireShared方法,即公平模式和非公平模式的tryAcquire实现是一样的!并且该方法不会阻塞线程,获取成功返回true,获取失败返回false!

public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0; //调用nonfairTryAcquireShared方法
}

2.6 释放信号量
public void release():释放一个信号量,信号量总数加1。释放成功后,将唤醒在同步队列中等待获取信号量的结点(线程)!
public void release(int permits):释放permits个信号量,信号量总数加permits。释放成功后,将唤醒在同步队列中等待获取信号量的结点(线程)!
公平模式和非公平模式的信号量的释放都是一样的。实际上内部调用AQS的releaseShared方法,这实际上就是共享式释放资源的模版方法。

//信号量,信号量总数加1。
public void release() {
    //内部调用AQS的releaseShared方法.实际上就是共享式释放资源的模版方法
    sync.releaseShared(1);
}
//放permits个信号量,信号量总数加permits。 @param permits 释放的信号量个数
public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    //参数就是permits
    sync.releaseShared(permits);
}
//AQS的共享模式下释放资源的模版方法。 如果成功释放则会调用doReleaseShared
public final boolean releaseShared(int arg) {
    //tryReleaseShared释放信号量资源,该方法由子类自己实现
    if (tryReleaseShared(arg)) {
        //释放成功,必定调用doReleaseShared尝试唤醒后继结点,即阻塞的线程
        doReleaseShared();
        return true;
    }
    return false;
}
//Sync的tryReleaseShared实现 @param releases 要释放的资源数量 @return true 成功 false 失败
protected final boolean tryReleaseShared(int releases) {
    for (; ; ) {
        int current = getState();   //很简单,就是尝试CAS的增加state值,增加releases
        int next = current + releases;
        //这里可以知道,信号量资源数量不可超过int的最大值
        if (next < current) // overflow . realease不能为负数
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next)) return true;//CAS的增加state值,CAS成功之后返回true,否则循环重试
    }
}

3 Semaphore的使用
Semaphore可以用来控制多线程对于共享资源访问的并发量!案例:若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用,每个工人之多工作10秒,最后统计工作量。我们可以通过Semaphore与之前的CountDownLatch搭配线程池来轻松实现。我们能发现,采用非公平模式的Semaphore时工人的总工作量大部分情况下要高于采用公平模式的工人总工作量,即非公平模式的执行效率更高(这是不一定的)。我们还能发现,在非公平模式工人的总工作量高于公平模式的工人总工作量时,非公平模式下总会有某些工人工(特别是工人0、1、2)作量更多,而另一些工人工作量更少,这就是线程饥饿!
4 Semaphore的总结
Semaphore和CountDownLatch的原理都差不多,都是直接使用AQS的共享模式实现自己的逻辑,都是对于AQS的state资源的利用,但是它们却实现了不同的功能,CountDownLatch中state被看作一个倒计数器,当state变为0时,表示线程可以放开执行。而Semaphore中的state被看作信号量资源,获取不到资源则可能会阻塞,获取到资源则可以访问共享区域,共享区域使用完毕要记得还回信号量。
很明显Semaphore的信号量资源很像锁资源,但是我们前面就说过他们的不同,那就是锁资源是和获得锁的线程绑定的,而这里的信号量资源并没有和线程绑定,也就是说你可以让一些线程不停的“释放信号量”,而另一些线程只是不停的“获取信号量”,这在AQS内部实际上就是对state状态的值的改变而已,与线程无关!
通常Semaphore可以用来控制多线程对于共享资源访问的并发量,在上面的案例中我们就见过!另外还需要注意的是,如果在AQS的同步队列中队头结点线程需要获取n个资源,目前有m个资源,如果m小于n,那么这个队列中的头结点线程以及后面的所有结点线程都会因为不能获取到资源而继续阻塞,即使头结点后面的结点中的线程所需的资源数量小于m也不行。即已经在AQS同步队列中阻塞的线程,只能按照先进先出的顺序去获取资源,如果头部线程因为所需资源数量不够而一直阻塞,那么队列后面的线程必定不能获取资源!
和CountDownLatch一样,Semaphore的源码看起来非常简单,那是因为复杂的线程等待、唤醒机制都被AQS实现了,如果想要真正了解Semaphore的原理,那么AQS是必须要了解的。实际上如果学会了AQS,那么JUC中的锁或者其他同步组件就很简单了!

syn和reentrylock的区别?

相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
功能区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
性能的区别:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
1.Synchronized
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
2.ReentrantLock
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

AQS的底层实现有哪些数据结构

同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,即AQS是同步组件实现的核心部分。AQS(AbstractQueuedSynchronizer),简称同步器,是用来构建锁和其它同步组件的基础框架。AQS的组成可以理解如下图:image-20220603140701010

要想掌握AQS的底层实现,我们就要学习这些模板方法,首先我们就得了解AQS中的同步队列是个什么样的数据结构,因为同步队列是AQS对同步状态管理的基石
同步队列:当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。AQS中的同步队列则是通过链式方式进行实现。在AQS有一个静态内部类Node,这是我们同步队列的每个具体节点。在这个类中有如下属性:image-20220603140956152

现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是一个双向队列。AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,获取锁成功的线程进行出队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:

image-20220603141208790

AQS的底层实现
独占锁
独占锁的获取:调用lock()方法是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。

final void lock() {//使用CAS来尝试将同步状态改为1,若成功则将同步状态持有线程置为当前线程。否则将调用AQS提供的aquire()方法。
  if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread());
  else acquire(1);
}
public final void acquire(int arg) {// 再次尝试获取同步状态,如果成功则方法直接返回,如果失败则先调用addWaiter()方法再调用acquireQueued()方法
  if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}
image-20220603142043886

接下来我们就分别来研究一下addWaiter()方法和acquireQueued()方法。
addWaiter()方法——入队

private Node addWaiter(Node mode) {
  Node node = new Node(Thread.currentThread(), mode); // 将当前线程包装称为Node类型
  // Try the fast path of enq; backup to full enq on failure
  Node pred = tail;
  if (pred != null) // 当前尾节点不为空
    // 将当前线程以尾插的方式插入同步队列中
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
   }
 }
  // 当前尾节点为空或CAS尾插失败
  enq(node);
  return node;
}

image-20220603142242537addWaiter()方法的流程图如下:
上述流程图我们可以发现,enq()方法的执行结果一定是成功的,那么原因是什么呢?我们来分析一下enq()的源码:

private Node enq(final Node node) {
  for (;;) {
    Node t = tail;
    if (t == null) { // Must initialize
      // 头结点初始化
      if (compareAndSetHead(new Node()))
        tail = head;
   } else {
      node.prev = t;
      // CAS尾插,失败进行自旋重试直到成功为止。
      if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
     }
   }
 }
}

enq()的流程图如下:
image-20220603142619014

现在我们已经很清楚获取独占式锁失败的线程包装成Node然后插入同步队列的过程了,那么我们就要清楚在同步队列中的结点(线程)如何来保证自己能够有机会获得独占式锁了,来分析一下acquireQueued()方法。

final boolean acquireQueued(final Node node, int arg) {//acquireQueued()方法——排队获取锁
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      final Node p = node.predecessor();// 获得当前节点的前驱节点
      // 当前节点能否获取独占式锁
      // 当前节点的前驱节点是头结点
      if (p == head && tryAcquire(arg)) {
        setHead(node); // 队列头指针指向当前节点
        // 释放前驱节点
        p.next = null; // help GC
        failed = false;
        return interrupted;
     }
      // 获取同步状态失败,线程进入等待状态等待获取独占锁
      if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) interrupted = true;
   }
 } finally {
    if (failed)
      cancelAcquire(node);
 }
}

acquireQueued()方法的流程图如下:
image-20220603143003704

独占式锁的释放:独占式锁的释放调用unlock()方法,而该方法实际调用了AQS的release方法

public void unlock() {
  sync.release(1);
}
public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0) unparkSuccessor(h);
    return true;
 }
  return false;
}
image-20220603143453407

独占式锁的总结:线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;释放锁的时候会唤醒后继节点;
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。

创建线程的方式

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程,如下所示:1)继承Thread类创建线程2)实现Runnable接口创建线程3)使用Callable和Future创建线程4)使用线程池例如用Executor框架.
通过继承Thread类来创建并启动多线程的一般步骤如下:1】定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。2】创建Thread子类的实例,也就是创建了线程对象3】启动线程,即调用线程的start()方法

通过实现Runnable接口创建并启动线程一般步骤如下:1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象3】第三部依然是通过调用线程对象的start()方法来启动线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。call()方法可以有返回值,call()方法可以声明抛出异常.Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
boolean isDone():若Callable任务完成,返回True
boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象。
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

如何设计使用核心线程和最大线程两个参数

核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系
统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我
们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照
百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理;
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)单个任务执
行时间;既: 最大线程数=(1000-200)*0.1=80个;

posted @   zhangshuai2496689659  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示