【JAVA面试题整理】JAVA基础(四)

JAVA的多线程和并发库

 一、线程创建的方式

1、在Thread子类覆盖run方法中实现运行代码

2、在传递给Thread对象的runnable对象的run方法中实现运行代码

总结:无论以哪种方式运行,都是最终要实现Runnable接口的run方法。

public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

二、线程的互斥和同步

多线程的情况下,多个线程同时运行的情况下难免引发一些相互限制的场景,这样的场景主要分为互斥和同步。

互斥:只多个线程共享同一个资源,如CPU、共享IO设备、数据库资源等,需要等正在使用当前资源的线程释放掉资源后当前线程才可以继续执行。

同步:这种场景主要是线程之间存在前后制约的业务逻辑,如线程A(员工)等着办离职手续要在线程B(老板)签字确认后才可以走之后的流程,否则就只能继续等待,这种制约关系就叫做同步。

下面通过一段代码来加深理解线程之间的互斥和同步

public static void main(String[] args) {
    final Bussinesss bussinesss = new Bussinesss();
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                bussinesss.subMethod();
            }
        }
    }).start();
    for (int i = 0; i < 3; i++) {
        bussinesss.mainMethod();
    }
}


static class Bussinesss {
    private boolean subFlag = true;


    public synchronized void mainMethod() {
        while (subFlag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": main thread running loop count --" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        subFlag = true;
        notify();
    }


    public synchronized void subMethod() {
        while (!subFlag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": sub thread running loop count --" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        subFlag = false;
        notify();
    }
}

输出结果

从代码和结果中我们分析得到,本来subMethod在线程中执行, mainMethod在主方法中执行,这两个是可以同步执行的,表现结果就应该是两个结果交替输出,但是由于subFlag在其中做了限制,执行 subMethod的同时就让mainMethod进行等待,直到subMethod执行结束之后再唤醒mainMethod,这就是线程之间的互斥。而线程中的subMethod要等主方法中的mainMethod方法执行结束后才能继续执行,这也是线程之间的同步。所以我们也可以理解互斥是一种特殊的同步现象。

三、什么是线程局部变量ThreadLocal?

作用:由于实现线程内的数据共享。即对于相同代码启动多线程运行时,一个线程中共享一份数据,另一个线程中共享另一份数据,互相不冲突。

使用方法:每个线程调用全局ThreadLocal对象的set方法插入记录,调用get方法获取ThreadLocal存放的值,线程结束时,可以使用remove方法,释放内存,当然不调用也可以,因为线程结束后也可以自动释放相关ThreadLocal变量。

应用场景:

  • 订单处理操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也在同一个线程中处理,如果累加公司应收款操作失败了,则应该把前面的操作回滚,否则提交所有的操作,这要求所有的操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。

  • 银行转账包含一系列操作:把转出账户余额减少,把转入账户余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是连个不同账户对象的方法。

  • 例如Struts2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中,getContext方法拿到的都是同一个。

ThreadLocal源码分析:

从ThreadLocal类的结构图可知,ThreadLocal可以使用的方法有get、set和remove等。

get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

从源码中可以得知,get方法首先会获取到当前线程,并对当前线程创建一个ThreadLocalMap类型的map,其中ThreadLocalMap为ThreadLocal的内部类,并将map中的value返回。

set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set方法中,会把ThreadLocal本身作为map的key,传出的value作为map的值进行存放。

remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

remove方法中,会将当前线程的ThreadLocal变量进行置空。

四、java的线程并发库有哪些?

1、java.util.concurrent包(多线程并发库)

包含许多线程安全、测试良好、高性能的并发构建块,目的就是要实现Collection框架对数据结构所执行的并发操作。通过这些可靠的、高性能的并发 构建块,卡法人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

2、java.util.concurrent.atomic包(多线程的院子型操作提供的工具类)

它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据进行多线程的操作。

3、java.util.concrrent.lock包(多线程的锁机制)

为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。本包有三大接口,下面简单介绍下:

  • Lock接口:支持哪些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括hand-over-hand和锁重排算法)中使用这些规则。主要实现是ReentrantLock。

  • ReadWriteLock接口:以类似方式定义了一些读者可以共享而写入者独占的锁。此包只提供了一个实现,即ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。

  • Condition接口:描述了可能会与锁有关联的变量。这些变量在用法上与使用Object.wait访问的隐式监视器类似,但提供了更强大的功能。特别需要指出的是,单个Lock可能与多个Condition对象关联。为了避免兼容性问题,Condition方法的名称与对应的Object版本中的不同。

五、线程池详解

1、线程池的作用:线程池的作用就是用来限制系统中执行线程的数量。

  1. 用线程池控制可以执行的线程数量,其他线程排队等候;

  2. 一个任务执行完毕,再从队列中取最前面的任务开始执行;

  3. 如果队列中没有等待的线程,线程池这一资源也处于等待;

  4. 当一个新的任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行,否则进入等待队列;

2、为什么使用线程池:

  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务;

  • 可以根据系统的承受能力,调整线程池中工作现成的数据,防止内存消耗过多,导致服务器崩溃;

3、Executors详解:

java里面线程池的顶级接口是Executor,但是严格意义上来讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。TreadPoolExecutor是Executors类的底层实现。

java5的并发库中,线程池创建线程大致可以分为下面三种:

//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();

除此之外,还有一个可延迟启动的线程池

//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorServicepool=Executors.newScheduledThreadPool(2);

4、委托给ExecutorSerice执行的几种方式:

  1. execute(Runnable):Runnable接口放入线程池中异步执行

  2. submit(Runnable):返回一个Future对象,这个Future对象可以用来检查Runnable是否已经执行完毕

  3. submit(Callable):类似于submit(Runnable),Callable实例除了他的call()方法能够返回结果外,Callable的结果可以通过submit(Callable)的方法返回的Future进行获取。

  4. invokeAny(...):方法要求一系列的Callable或者其子接口的实例对象。调用这个方法并不会返回一个Future,蛋挞返回其中一个Callable的对象的结果。无法保证返回的是哪个Callable的结果,只能表明其中一个已经执行结束。如果

  5. 其中一个任务执行结束(或跑了一个异常),其他Callable将被取消。上述代码回答引出给定Callable集合中其中一个的结果,但不一定是其中哪一个。

  6. invokeAll(...):方法将调用你在集合中传给ExecutorService的所有Callable对象。并返回一系列Future对象,通过它们可以获取每个Callable的执行结果。

5、Executors关闭

使用shutdown和shutdownNow可以关闭线程池

两者区别在于:

shutdown知识将空闲的线程interrupt()了,shutdown()之前提交的任务可以继续执行直到结束。

shutdownNow是interrupt所有线程,因此大部分线程将立刻被中断。之所以是大部分,不是全部,是因为interrupt能力有限# #!

6、ThreadPoolExecutor(线程执行者)详解

线程池中能够包含多少线程,由以下变量决定:

  • corePoolSize

  • maximumPoolSize

当一个任务委托给线程池时,如果线程池中线程数低于corePoolSize,一个新的线程将被创建,即使池中可能上游空闲线程。如果内部任务队列已满,而且有至少corePoolSize正在运行,但是运行线程的数量低于maximumPoolSize,一个新的线程将被创建取执行该任务。

intcorePoolSize=5;
intmaxPoolSize=10;
longkeepAliveTime=5000;
ExecutorServicethreadPoolExecutor=
newThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
newLinkedBlockingQueue<Runnable>()
);

构造方法参数列表解释:

corePoolSize -池中所保存的线程数,包括空闲线程。

maximumPoolSize -池中允许的最大线程数。

keepAliveTime -当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

unit -keepAliveTime 参数的时间单位。

workQueue -执行前用于保持任务的队列。此队列仅保持由execute 方法提交的Runnable 任务。

7、ScheduledPoolExecutor(定时线程池)详解

java.util.concurrent.ScheduledExecutorService 是一个ExecutorService,它能将任务延后执行,或者间隔固定时间多次执行。

ScheduledPoolExecutor例子:

ScheduledExecutorServicescheduledExecutorService=
Executors.newScheduledThreadPool(5);
ScheduledFuturescheduledFuture=
scheduledExecutorService.schedule(newCallable(){
publicObjectcall()throwsException{
System.out.println("Executed!");
return"Called!";
}
},
5,
TimeUnit.SECONDS);//5秒后执行

首先一个内置5 个线程的ScheduledExecutorService 被创建。之后一个Callable 接口的匿名类示例被创建然后传递给schedule() 方法。后边的俩参数定义了Callable 将在5秒钟之后被执行。

ScheduledExecutorService的使用:

一旦你创建了一个ScheduledExecutorService,你可以通过调用它的以下方法:

 

正如ExecutorService,在你使用结束之后你需要把ScheduledExecutorService 关闭掉。否则他将导致JVM 继续运行,即使所有其他线程已经全被关闭。

你可以使用从ExecutorService 接口继承来的shutdown() 或shutdownNow() 方法将ScheduledExecutorService 关闭。

 

 

  • schedule (Callable task, long delay, TimeUnit timeunit):

  • schedule (Runnable task, long delay, TimeUnit timeunit):同上,知识线程创建方式不同

  • scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit):这一方法规划一个任务将被定期执行。该任务将会在首个initialDelay 之后得到执行,然后每个period 时间之后重复执行。如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行。ScheduledExecutorService 被关闭。如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。

  • scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit):除了period 有不同的解释之外这个方法和scheduleAtFixedRate() 非常像。scheduleAtFixedRate() 方法中,period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。


六、并发-阻塞队列

常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者使用CAS非阻塞算法实现。

阻塞队列介绍:阻塞队列(BlockingQueue)是java.util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进入插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列获取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类都是基于BlockingQueue实现的。

BlockingQueue阻塞队列

BlockingQueue通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是这个原理的阐述:

一个线程往里边放,另外一个线程从里边取的一个BlockingQueue。

一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的邻接点。也就是说,它是有限的。如果该阻塞队列达到了其邻接点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞中,直到负责消费的线程从队列中拿走一个对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到生产线程把一个对象丢进队列里。

BlockingQueue的方法

BlockingQueue具有4组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下: 

方法\处理方式

抛出异常

返回特殊值

一直阻塞

超时退出

插入方法

add(e)

offer(e)

put(e)

offer(e,time,unit)

移除方法

remove()

poll()

take()

poll(time,unit)

检查方法

element()

peek()

不可用

不可用

抛异常:如果试图的操作无法立即执行,抛一个异常

特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是true\false)

阻塞:如果试图的操作无法立即执行,该方法会发生阻塞,直到能够执行。

超时:如果试图的操作无法立即执行,该方法会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(常常是true\false)

PS:无法向一个BlockingQueue中插入null,如果你试图插入null,BlockingQueue将会跑出一个NullPointException。

BlockingQueue的实现类

BlockingQueue是一个接口,你需要使用它的实现之一来使用BlockingQueue,java.util.concurrent包下具有以下BlockingQueue接口的实现类:

  • ArrayBlockingQueue:是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改。

  • DelayQueue:对元素进行持有直到一个特定的延迟到期。

  • LinkedBlockingQueue:内部以一个链式结构对其元素进行存储。如果需要的话,这一练市结构可以选择一个上限。如果没有定义上限,将使用Integer.MAX_VALUE作为上限。

  • ProrityBlockingQueue:是一个无界的并发队列。它使用了和类java.util.ProrityQueue一样的排序规则。你无法想这个队列插入null。所有插入到PriorityBlockingQueue的元素实现java.lang.Comparable接口。因此该队列中元素的排序就取决于你自己的Comparable实现。

  • SynchronousQueue:是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。

ArrayBlockingQueue阻塞队列

如上图ArrayBlockingQueue内部有个数组items用来存放队列元素,putindex下标标示入队元素下标,takeIndex是出兑下标,count统计队列元素个数,从定义可知道并没有使用volatile修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁lock用来对出入队操作加锁,这导致同时只有一个线程可以访问入队、出队,另外notEmpty、notFull条件变量用来进行出入队的同步。

另外构造函数必须传入队列大小参数,所以为有界队列,默认是Lock为非公平锁。

公平锁:就是在并发环境中,每个县城在获取所示会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就站有所,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的那种方式。

ArrayBlockingQueue方法

  • offer方法

在队尾插入元素,如果队列满则返回false,否则入队返回true

这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是在CPU缓存或者寄存器里面的值,释放锁后修改的共享变量之会刷新到主内存中。

另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时有些特殊。另外insert后调用notEmpty.signal()是为了激活调用notEmpty.await()阻塞后放入notEmpty条件队列中的线程。

  • put操作

在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。

需要注意的是,如果队列满了,那么当前线程会阻塞,直到出队操作调用了notFull.signal方法激活该线程。代码逻辑很简单,但是这里需要思考一个问题:为啥调用lockInterruptibly方法而不是lock方法。我的理解是因为调用了条件变量await方法,而await方法会在中断标志设置后抛出InterruptException异常后退出,所以还不如加锁时先看中断表示是不是被设置了,如果设置了直接抛出InterruptException异常,就不用再去获取锁了。然后看了其他并发类里面方式调用了await方法获取锁的时候都是使用lockInterruptibly方法而不是lock也验证了这个想法。

  • poll操作

从队头获取并移除元素,队列为空,则返回null

  • take操作

从队头获取元素,如果队列为空则阻塞直到队列持有元素。

需要注意的是如果队列为空,当前线程会被挂起放到notEmpty的条件队列里面,直到入队操作执行调用notEmpty.signal后当前线程才会被激活,await才会返回。

  • peek操作

返回队列头元素但不移除该元素,队列为空,则返回null。

  • size操作

获取队列元素个数,非常精确因为size计算时候加了独占锁,其他线程不能入队或者出队或者删除元素。

ArrayBlockingQueue小结

ArrayBlockingQueue通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的力度比较大,有点儿类似在方法上添加synchronized的意思。其中offer、poll操作通过简单地枷锁进行入队出队操作,而put、take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比LinkedBlockingQueue、ArrayBlockingQueue的size操作结果是精确地,因为计算前加了全局锁。

ArrayBlockingQueue示例

在多线程操作下,一个数组中最多只能存放3个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入。

public static void main(String[] args) {
    final BlockingQueue queue = new ArrayBlockingQueue(3);
    for (int i = 0; i < 2; i++) {
        new Thread() {
            public void run() {
                while (true) {
                    try {
                        Thread.sleep((long) (Math.random() * 1000));
                        System.out.println(Thread.currentThread().getName() + " 准备放数据!");
                        queue.put(1);
                        System.out.println(Thread.currentThread().getName() + "已经放了数据" + "队列目前有" + queue.size() + "个数据");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
    new Thread() {
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "准备取数据!");
                    System.out.println(queue.take());
                    System.out.println(Thread.currentThread().getName() + "已经取走数据," + "队列目前有" + queue.size() + "个数据");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();
}

输出结果

LinkedBlockingQueue阻塞队列

LinkedBlockingQueue中也有两个Node分别用来存放首尾节点,并且里面有个初始值为0的原子变量count用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁,其中takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock控制同时只有一个线程可以获取锁去添加元素,其他线程必须等待。另外notEmpty和notFull用来实现入队和出队的同步。另外由于出入队是两个非公平独占锁,所以可以同时有一个线程入队和一个线程出队,其实这个是个生产者-消费者模型,如下类图:

如图默认队列容量为0x7fffffff;用户也可以自己指定容量。

  • 带时间的offer操作--生产者

从源码中可以看出,带时间的offer操作首先基于给定的时间进行判读,如果给定时间小于0,则返回false,否则就根据notFull的信号量进行判断,从而进行循环重试的操作;另外每次插入队列时,都是插到队尾。

public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    return offerLast(e, timeout, unit);
}
public boolean offerLast(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    if (e == null) throw new NullPointerException();
    Node<E> node = new Node<E>(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (!linkLast(node)) {
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        return true;
    } finally {
        lock.unlock();
    }
}
private boolean linkLast(Node<E> node) {
    // assert lock.isHeldByCurrentThread();
    if (count >= capacity)
        return false;
    Node<E> l = last;
    node.prev = l;
    last = node;
    if (first == null)
        first = node;
    else
        l.next = node;
    ++count;
    notEmpty.signal();
    return true;
}
  • 带时间的poll操作--消费者

获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null。

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    return pollFirst(timeout, unit);
}
public E pollFirst(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        E x;
        while ( (x = unlinkFirst()) == null) {
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return x;
    } finally {
        lock.unlock();
    }
}
private E unlinkFirst() {
    // assert lock.isHeldByCurrentThread();
    Node<E> f = first;
    if (f == null)
        return null;
    Node<E> n = f.next;
    E item = f.item;
    f.item = null;
    f.next = f; // help GC
    first = n;
    if (n == null)
        last = null;
    else
        n.prev = null;
    --count;
    notFull.signal();
    return item;
}

首先获取独占锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回null。

超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去1前队列元素大于1则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。

最后如果减去1前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程。

  • put操作--生产者

与带超时时间的poll类似不同在于put时候如果当前队列满了它会一直等待其他线程调用notFull.signal才会被唤醒。

  • take操作--消费者

与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒。

  • size操作-消费者

当前队列元素个数,如代码直接使用原子变量count获取。

public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return count;
    } finally {
        lock.unlock();
    }
}
  • peek操作

获取但是不移除当前队列的头元素,没有则返回null。

public E peek() {
    return peekFirst();
}
public E peekFirst() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (first == null) ? null : first.item;
    } finally {
        lock.unlock();
    }
}
  • remove操作

删除队列里面的一个元素,有则删除返回true,没有则返回false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。

public E remove() {
    return removeFirst();
}
public E removeFirst() {
    E x = pollFirst();
    if (x == null) throw new NoSuchElementException();
    return x;
}
public E pollFirst() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return unlinkFirst();
    } finally {
        lock.unlock();
    }
}
private E unlinkFirst() {
    // assert lock.isHeldByCurrentThread();
    Node<E> f = first;
    if (f == null)
        return null;
    Node<E> n = f.next;
    E item = f.item;
    f.item = null;
    f.next = f; // help GC
    first = n;
    if (n == null)
        last = null;
    else
        n.prev = null;
    --count;
    notFull.signal();
    return item;
}

LinkedBlockingQueue示例

并发库中的BlockingQueue是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法put()和take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从head取一个对象,如果没有对象,就等待直到有可取的对象。

下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中,另外四个写线程用于取出文件对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该Demo也使用到了线程池和原子整型(AtomicInteger),AtomicInteger可以在并发情况下达到原子化更新,避免使用了synchronized,而且性能非常高。由于阻塞队列的put和take操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫“哨兵”,当发现这个哨兵后,写线程就退出。

当然线程池也要显式退出了。

final static BlockingQueue<File> queue = new LinkedBlockingDeque<File>(100);
final static ExecutorService exec = Executors.newFixedThreadPool(5);
final static File root = new File("D:\\aaa");
final static File exitFile = new File("");
final static AtomicInteger rc = new AtomicInteger();
final static AtomicInteger wc = new AtomicInteger();


public static void main(String[] args) {
    Runnable read = new Runnable() {
        @Override
        public void run() {
            scanFile(root);
            scanFile(exitFile);
        }
    };
    exec.submit(read);
    for (int index = 0; index < 4; index++) {
        final int NO = index;
        Runnable write = new Runnable() {
            String threadNam = "write" + NO;


            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        int index = wc.incrementAndGet();
                        File file = queue.take();
                        if (file == exitFile) {
                            queue.put(exitFile);
                            break;
                        }
                        System.out.println(threadNam + ":" + index + " " + file.getPath());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        exec.submit(write);
    }
    exec.shutdown();
}


public static void scanFile(File file) {
    if (file.isDirectory()) {
        File[] files = file.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory() || pathname.getPath().endsWith(".java");
            }
        });
        for (File one : files) {
            scanFile(one);
        }
    } else {
        try {
            int index = rc.incrementAndGet();
            System.out.println("Read0:" + index + " " + file.getPath());
            queue.put(file);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

PriorityBlockingQueue无界阻塞优先级队列

PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现,研究过数组方式存放最小堆节点的都知道,直接遍历队列元素是无序的。

如图PriorityBlockingQueue内部有个数组queue用来存放队列元素,size用来存放队列元素个数,allocationSpinLockOffset是用来在扩容队列时候做cas的,目的是保证只有一个线程可以进行扩容。

由于这是一个优先级队列所以有个比较器comparator用来比较元素大小。lock独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty条件变量用来实现take方法阻塞模式。这里没有notFull 条件变量是因为这里的put操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。最后PriorityQueue q用来搞序列化的。

如下构造函数,默认队列容量为11,默认比较器为null;、

private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}
  • offer操作

在队列插入一个元素,由于是误解队列,所以一直为成功返回true。

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}
private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

tryGrow目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用cas控制只有一个线程可以扩容成功。我的理解是为了性能,因为扩容时候是需要花时间的,如果这些操作时候还占用锁那么其他线程在这个时候是不能进行出队操作的,也不能进行入队操作,这大大降低了并发性。

所以在扩容前释放锁,这允许其他出队线程可以进行出队操作,但是由于释放了锁,所以也允许在扩容时候进行入队操作,这就会导致多个线程进行扩容会出现问题,所以这里使用了一个spinlock用cas控制只有一个线程可以进行扩容,失败的线程调用Thread.yield()让出cpu,目的意在让扩容线程扩容后优先调用lock.lock重新获取锁,但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁。

那copy元素数据到新数组为啥放到获取锁后面那?原因应该是因为可见性问题,因为queue并没有被volatile修饰。另外有可能在扩容时候进行了出队操作,如果直接拷贝可能看到的数组元素不是最新的。而通过调用Lock后,获取的数组则是最新的,并且在释放锁前数组内容不会变化。

  • poll操作

在队列头部获取并移除一个元素,如果队列为空,则返回null。

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return dequeue();
    } finally {
        lock.unlock();
    }
}
private E dequeue() {
    int n = size - 1;
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];
        E x = (E) array[n];
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}
  • put操作

内部调用的offer,由于是无界队列,所以不需要阻塞

public void put(E e) {
    offer(e); // never need to block
}
  • take操作

获取队列头元素,如果队列为空则阻塞。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}
  • size操作

获取队列个数,由于加了独占锁,所以返回结果是精确的

public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return size;
    } finally {
        lock.unlock();
    }
}

PriorityBlockingQueue小结

PriorityBlockingQueue类似于ArrayBlockingQueue内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个notEmpty条件变量而没有notFull这是因为前者是无界队列,当put时候永远不会处于await所以也不需要被唤醒。

PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。

值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过cas保证同时只有一个线程可以扩容成功。

PriorityBlockingQueue示例

PriorityBlockingQueue类是JDK提供的优先级队列本身是线程安全的内部使用显示锁保证线程安全。

PriorityBlockingQueue存储的对象必须是实现Comparable接口的因为PriorityBlockingQueue队列会根据内部存储的每一个元素的compareTo方法比较每个元素的大小。这样在take出来的时候会根据优先级将优先级最小的最先取出。

publicstaticPriorityBlockingQueue<User>queue=newPriorityBlockingQueue<User>();
publicstaticvoidmain(String[]args){
queue.add(newUser(1,"wu"));
queue.add(newUser(5,"wu5"));
queue.add(newUser(23,"wu23"));
queue.add(newUser(55,"wu55"));
queue.add(newUser(9,"wu9"));
queue.add(newUser(3,"wu3"));
for(Useruser:queue){
try{
System.out.println(queue.take().name);
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
//静态内部类
staticclassUserimplementsComparable<User>{
publicUser(intage,Stringname){
this.age=age;
this.name=name;
}
intage;
Stringname;
@Override
publicintcompareTo(Usero){
returnthis.age>o.age?-1:1;
}
}

SychronousQueue同步队列

SynchronousQueue是一个比较特别的队列,由于在线城市方面有所应用,为了更好地理解线程池的实现原理,此队列源码中充斥着大量的CAS语句。

SychronousQueue简单使用

SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。

public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
}
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
一种输出结果如下:
put thread start
take thread start
take from putThread: 1
put thread end
take thread end

从结果可以看出,put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。

SychronousQueue总结

SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。

DeplayQueue延时无界阻塞队列

在谈到DelayQueue的使用和原理的时候,我们首先介绍一下DelayQueue,DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。

DelayQueue阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检查看数据是否过期等,首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要12:00点执行的任务可能12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用DelayQueue。

Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)。DelayQueue内部是使用PriorityQueue实现的。

DelayQueue = BlockingQueue +PriorityQueue + Delayed

DelayQueue的关键元素BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。

public interface Comparable<T> {
public int compareTo(T o);
}
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
public class DelayQueue<E extends Delayed> implements BlockingQueue<E> {
private final PriorityQueue<E> q = new PriorityQueue<E>();
}

DelayQueue内部的实现使用了一个优先队列。当调用DelayQueue的offer方法时,把Delayed对象加入到优先队列q中。如下:

public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}

DelayQueue的take方法,把优先队列q的first拿出来(peek),如果没有达到延时阀值,则进行await处理。如下:

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay =  first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
}
}
}
} finally {
lock.unlock();
}
}

7、并发-非阻塞队列

非阻塞队列

首先我们要简单理解下什么是非阻塞队列:

与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队还是生产者的入队。

在底层,非阻塞队列使用的是CAS(compare and swap)来实现线程执行额非阻塞。

非阻塞队列简单操作

与阻塞队列相同,非阻塞队列中的常用方法,也是出队和入队

入队方法:

  • add():底层调用offer()

  • offer():Queue接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回true;

出队方法:

  • poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回null

  • peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回null;

非阻塞算法CAS

首先我们要了解悲观锁和乐观锁

悲观锁:

嘉定并发环境是悲观的,如果发生冲突,就会破坏已执行,所以要通过独占锁彻底禁止冲突发生,有一个经典比喻,如果你不锁门,那么捣蛋鬼就会闯入并搞得一团糟,所以你只能一次打开门放进一个人,才能时刻盯着他

乐观锁:

假定并发环境是乐观的,即虽然会有并发冲突,但冲突可发现且并不会造成损害。所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道,所以你大可以放进所有人,等发现他们想破坏时在做决定。通常认为乐观锁的性能比悲观锁更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争只发生在最小的并发冲突处,如果用悲观锁来理解,就是锁的最小力度。但乐观锁的设计往往比较复杂,一次,发咋场景还是多用悲观锁。

CAS:

乐观锁的实现往往需要硬件的支持,多数处理器都实现了一个CAS指令,实现compare and swap的语义,构成了基本的乐观锁。

CAS包含3个操作数

  • 需要读写的内存位置V

  • 进行比较的值A

  • 拟写入的新值B

当且仅当位置V的值等于A是,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。

ConcurrentLinkedQueue非阻塞无界链表队列

ConcurrentLinkedQueue是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来说队列的长度可以无限扩大。

与其它队列相同,ConcurrentLinkedQueue也采用的是FIFO入队规则,对元素进行排序。当我们向队列中添加元素时,新插入的元素会插入到队列的尾部;当我们获取一个元素时,他会从队列的头部取出。

因为ConcurrentLinkedQueue是链表结构,所以当入队时,插入的元素依次向后延伸,形成链表;而出队时,则从链表的第一个元素开始获取,依次递增。

如图ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存在列表的首尾节点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表。

public ConcurrentLinkedDeque() {
    head = tail = new Node<E>(null);
}

如上代码初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。

ConcurrentLinkedQuere方法

  • offer操作

offer操作是在链表末尾添加一个元素,下面看看实现原理。

public boolean offer(E e) {
    return offerLast(e);
}
public boolean offerLast(E e) {
    linkLast(e);
    return true;
}
private void linkLast(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);


    restartFromTail:
    for (;;)
        for (Node<E> t = tail, p = t, q;;) {
            if ((q = p.next) != null &&
                (q = (p = q).next) != null)
                // Check for tail updates every other hop.
                // If p == q, we are sure to follow tail instead.
                p = (t != (t = tail)) ? t : q;
            else if (p.prev == p) // NEXT_TERMINATOR
                continue restartFromTail;
            else {
                // p is last node
                newNode.lazySetPrev(p); // CAS piggyback
                if (p.casNext(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this deque,
                    // and for newNode to become "live".
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return;
                }
                // Lost CAS race to another thread; re-read next
            }
        }
}

从构造函数知道一开始有个item为null的哨兵节点,并且head和tail都是指向这个节点,然后当一个线程调用offer时候首先

如图首先查找尾节点,q==null,p就是尾节点,所以执行p.casNext通过cas设置p的next为新增节点,这时候p==t所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法,所以可能两个线程同时执行到了(1)进行cas,那么只有一个会成功(假如线程1成功了),成功后的链表为:

失败的线程会循环一次这时候指针为:

这时候会执行(3)所以p=q,然后在循环后指针位置为:

所以没有其他线程干扰的情况下会执行(1)执行cas把新增节点插入到尾部,没有干扰的情况下线程2 cas会成功,然后去更新尾节点tail,由于p!=t所以更新。这时候链表和指针为:

假如线程2cas时候线程3也在执行,那么线程3会失败,循环一次后,线程3的节点状态为:

这时候p!=t ;并且t的原始值为told,t的新值为tnew ,所以told!=tnew,所以 p=tnew=tail; 然后在循环一下后节点状态:

q==null所以执行(1)。

现在就差p==q这个分支还没有走,这个要在执行poll操作后才会出现这个情况。poll后会存在下面的状态

这个时候添加元素时候指针分布为:

所以会执行(2)分支 结果 p=head 然后循环,循环后指针分布:

所以执行(1),然后p!=t所以设置tail节点。现在分布图:

自引用的节点会被垃圾回收掉。

  • add操作

add操作是在链表末尾添加一个元素,下面看看实现原理。 其实内部调用的还是offer

public boolean add(E e) {
    return offerLast(e);
}
  • poll操作

poll操作是在链表头部获取并且移除一个元素

public E poll()           { return pollFirst(); }
public E pollFirst() {
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null && p.casItem(item, null)) {
            unlink(p);
            return item;
        }
    }
    return null;
}
  • peek操作

peek操作是获取链表头部一个元素(只读取不移除),下面看看实现原理。 代码与poll类似,只是少了castItem.并且peek操作会改变head指向,offer后head指向哨兵节点,第一次peek后head会指向第一个真的节点元素。

public E peek()           { return peekFirst(); }
public E peekFirst() {
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null)
            return item;
    }
    return null;
}
  • size操作

获取当前队列元素个数,在并发环境下不是很有用,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。

  • remove操作

如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回false

  • contains操作

判断队列里面是否含有指定对象,由于是遍历整个队列,所以类似size 不是那么精确,有可能调用该方法时候元素还在队列里面,但是遍历过程中才把该元素删除了,那么就会返回false.

ConcurrentLinkedQuere总结

ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。

下面总结下如何实现线程安全的,可知入队出队函数都是操作volatile变量:head,tail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。

对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。

ConcurrentHashMap非阻塞Hash集合

ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现,ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentLinkedQuere实现原理

众所周知,哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。

HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

HashTable :HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。如下图

HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想

ConcurrentHashMap源码解析

ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。

Segment<K,V>[] segments = (Segment<K,V>[])
    new Segment<?,?>[DEFAULT_CONCURRENCY_LEVEL];

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)

所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。Segment类似于HashMap,一个Segment维护着一个HashEntry数组

transient volatile HashEntry<K,V>[] table;

HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。

static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
//其他省略
}

我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法

Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor= lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}

我们来看下ConcurrentHashMap的构造方法

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。

从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。

其实,put方法对segment也会有所体现

public V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

从源码看出,put的主要逻辑也就两步:

  1. 定位segment并确保定位的Segment已初始化

  2. 调用Segment的put方法。

ConcurrentLinkedQuere方法

  • Get操作

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

ConcurrentHashMap总结

ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable的全表锁在性能上的提升非常之大。

8、java.util.concurrent.atomic包

AtomicBoolean原子性布尔

AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。

AtomicBoolean,在这个Boolean值的变化的时候不允许在之间插入,保持操作的原子性。

下面将解释重点方法并举例:

boolean compareAndSet(expectedValue, updateValue),这个方法主要两个作用:

1. 比较AtomicBoolean和expect的值,如果一致,执行方法内的语句。其实就是一个if语句

2. 把AtomicBoolean的值设成update,比较最要的是这两件事是一气呵成的,这连个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行。为多线程的控制提供了解决的方案

下面我们简单介绍下AtomicBoolean的API

  • 创建一个AtomicBoolean

你可以这样创建一个AtomicBoolean:

AtomicBoolean atomicBoolean = new AtomicBoolean();

以上示例新建了一个默认值为false 的AtomicBoolean。如果你想要为AtomicBoolean 实例设置一个显式的初始值,那么你可以将初始值传给AtomicBoolean 的构造子:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
  • 获得AtomicBoolean的值

你可以通过使用get() 方法来获取一个AtomicBoolean 的值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();
  • 设置AtomicBoolean的值

你可以通过使用set() 方法来设置一个AtomicBoolean 的值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);

以上代码执行后AtomicBoolean 的值为false。

  • 交换AtomicBoolean的值

你可以通过getAndSet() 方法来交换一个AtomicBoolean 实例的值。getAndSet() 方法将返回AtomicBoolean 当前的值,并将为AtomicBoolean 设置一个新值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);

以上代码执行后oldValue 变量的值为true,atomicBoolean 实例将持有false 值。代码成功将AtomicBoolean 当前值ture 交换为false。

  • 比较并设置AtomicBoolean的值

compareAndSet() 方法允许你对AtomicBoolean 的当前值与一个期望值进行比较,如果当前值等于期望值的话,将会对AtomicBoolean 设定一个新值。compareAndSet() 方法是原子性的,因此在同一时间之内有单个线程执行它。因此compareAndSet() 方法可被用于一些类似于锁的同步的简单实现。以下是一个compareAndSet() 示例:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newValue= false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(
expectedValue, newValue);

本示例对AtomicBoolean 的当前值与true 值进行比较,如果相等,将AtomicBoolean 的值更新为false

AtomicInteger原子性整型

AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

我们先来看看AtomicInteger给我们提供了什么方法:

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

下面通过两个简单的例子来看一下AtomicInteger 的优势在哪:普通线程同步:

class Test2 {
private volatile int count = 0;
public synchronized void increment() {
count++; //若要线程安全执行执行count++,需要加锁
}
public int getCount() {
return count;
}
}

使用AtomicInteger:

class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}

从上面的例子中我们可以看出:使用AtomicInteger是非常的安全的.而且因为AtomicInteger由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。AtomicInteger是使用非阻塞算法来实现并发控制的。AtomicInteger的关键域只有以下3个:

// 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;

这里,unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。注意:value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。

优点:最大的好处就是可以避免多线程的优先级倒置和死锁情况的发生,提升在高并发处理下的性能。

AtomicIntegerArray原子性整型数组

java.util.concurrent.atomic.AtomicIntegerArray类提供了可以以原子方式读取和写入的底层int数组的操作,还包含高级原子操作。AtomicIntegerArray支持对底层int数组变量的原子操作。它具有获取和设置方法,如在变量上的读取和写入。也就是说,一个集合与同一变量上的任何后续get相关联。原子compareAndSet方法也具有这些内存一致性功能。

AtomicIntegerArray本质上是对int[]类型的封装。使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:

//获得数组第i个下标的元素
public final int get(int i)
//获得数组的长度
public final int length()
//将数组第i个下标设置为newValue,并返回旧的值
public final int getAndSet(int i, int newValue)
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//将第i个下标的元素加1
public final int getAndIncrement(int i)
//将第i个下标的元素减1
public final int getAndDecrement(int i)
//将第i个下标的元素增加delta(delta可以是负数)
public final int getAndAdd(int i, int delta)

下面给出一个简单的示例,展示AtomicIntegerArray使用:

public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
输出结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

上述代码第2行,申明了一个内含10个元素的数组。第3行定义的线程对数组内10个元素进行累加操作,每个元素各加1000次。第11行,开启10个这样的线程。因此,可以预测,如果线程安全,数组内10个元素的值必然都是10000。反之,如果线程不安全,则部分或者全部数值会小于10000

AtomicLong、AtomicLongArray原子性整型数组

AtomicLong、AtomicLongArray的API跟AtomicInteger、AtomicIntegerArray在使用方法都是差不多的。区别在于用前者是使用原子方式更新的long值和long数组,后者是使用原子方式更新的Integer值和Integer数组。两者的相同处在于它们此类确实扩展了Number,允许那些处理基于数字类的工具和实用工具进行统一访问。在实际开发中,它们分别用于不同的场景。这个就具体情况具体分析了,下面将举例说明AtomicLong的使用场景(使用AtomicLong生成自增长ID),其他就不在过多介绍。

importjava.util.ArrayList;
importjava.util.Collections;
importjava.util.HashSet;
importjava.util.List;
importjava.util.Set;
importjava.util.concurrent.atomic.AtomicLong;
publicclassAtomicLongTest{
/**
*@paramargs
*/
publicstaticvoidmain(String[]args){
finalAtomicLongorderIdGenerator=newAtomicLong(0);
finalList<Item>orders=Collections
.synchronizedList(newArrayList<Item>());
for(inti=0;i<10;i++){
ThreadorderCreationThread=newThread(newRunnable(){
publicvoidrun(){
for(inti=0;i<10;i++){
longorderId=orderIdGenerator.incrementAndGet();
Itemorder=newItem(Thread.currentThread().getName(),
orderId);
orders.add(order);
}
}
});
orderCreationThread.setName("OrderCreationThread"+i);
orderCreationThread.start();
}
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
Set<Long>orderIds=newHashSet<Long>();
for(Itemorder:orders){
orderIds.add(order.getID());
System.out.println("Order name:" + order.getItemName()
+"----"+"Order id:" + order.getID());
}
}
}
classItem{
StringitemName;
longid;
Item(Stringn,longid){
this.itemName=n;
this.id=id;
}
publicStringgetItemName(){
returnitemName;
}
publiclonggetID(){
returnid;
}
}
输出:
Order name:Order Creation Thread 0----Order id:1
Order name:Order Creation Thread 1----Order id:2
Order name:Order Creation Thread 0----Order id:4
Order name:Order Creation Thread 1----Order id:5
Order name:Order Creation Thread 3----Order id:3
Order name:Order Creation Thread 0----Order id:7
Order name:Order Creation Thread 1----Order id:6
........
Order name:Order Creation Thread 2----Order id:100

从运行结果我们看到,不管是哪个线程。它们获得的ID是不会重复的,保证的ID生成的原子性,避免了线程安全上的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值