再入锁,线程安全队列与线程池串想

题中这三者是有一环扣一环的联系的,在此做一个总结加深理解。

再入锁Reentrantlock主要是和synchronized关键字作区别,都是加锁但是调度单位不同。synchronized是以调用次数为单位,即被synchronized修饰的方法或者代码块每被线程执行一次,都有一个获取锁释放锁的过程,哪怕是同一个线程多次调用,所以在递归方法中最好不要用synchronized。如上所述,synchronized底层也有获取锁和释放锁的方法,其获取锁和释放锁的方法会考虑到对象是否有偏斜锁,开启偏斜所即用fast_enter()/fast_exit()方法获取锁和释放锁。若关闭了偏斜锁就是slow_enter()/slow_exit()方法。补充一点,偏斜锁的撤销操作是很重的开销,而且偏斜锁仅仅在synchronized代码块并发度不高的情况下才会体现出效率优势,很好理解,因为并发高了不断地撤销偏斜锁开销很大效率当然低了。既然用了synchronized关键字说明并发度不低,所以个人感觉偏斜锁意义不大。

有点扯远了,再看下再入锁Reentrantlock,它是以线程执行为调度单位的,即如果线程已经lock()获取该对象的锁,还没有unlock()释放掉,那么当再次尝试获取锁的时候就可以直接获取成功,即线程执行一次在释放该对象的锁之前只会获取一次锁,这就比较适合递归方法中使用了。再入锁需要注意的是其条件变量condition的应用,这也和后面将要串想的线程安全队列有关。

条件变量是通过再入锁的newCondition()方法获得,首先它也是一个对象,有两个重要的方法await()/signal()。一个线程虽然获取了再入锁,但是condition.await()方法可以让该线程进入等待状态,condition.signal()相应的唤醒因同一个条件变量的await()方法进入等待状态的所有线程,这点很重要,和后面将要串想的线程安全队列有关。

再来看线程安全队列,先了解一下基本介绍。JAVA并发包下常用的两种线程安全队列分别是ConcurrentLinkedQueue和LinkedBlockingQueue。这两者主要是锁机制不同。ConcurrentLinkedQueue采用了CAS无锁机制,而后者就是采用了前文介绍的再入锁Reentrantlock。准确的说BlockingQueue下所有的线程安全队列都是利用再入锁机制。当然JDK1.6之后把synchronousQueue的加锁机制换为CAS机制,synchronousQueue线程安全队列也是newCachedThreadPool()线程池的默认队列,这个暂时先不了解,先知道线程池的实现离不开线程安全队列即可,后文会详细介绍。

除synchronousQueue之外,BlockingQueue下其他线程安全队列:

1.ArrayBlockingQueue

它的特点就是是有界的,创建ArrayBlockingQueue要指定容量,还涉及到公平性。

public ArrayBlockingQueue(int Capacity,boolean fair)

2.PriorityBlockingQueue

它的特点是有优先级概念,虽然是无边界的,但是毕竟会受到系统资源限制,其实它的边界是Integer的Max值

3.DelayedQueue和LinkedTransferQueue

这两个是无边界的,暂时不了解。

4.LinkedBlockingQueue

这个线程安全队列无疑是最重要的,也是无边界的,是很多线程池的默认安全队列。

其实我感觉BlockingQueue不能称之为"线程安全队列",因为我理解的线程安全加锁为了避免多线程下的数据安全,而BlockingQueue加锁是为了在队列非空情况下才让take(),在队列没满的情况下才让put()。本质上只是为了在编程中省了一个队列判断非空判断长度的过程。通过再入锁实现的,简单总结一下原理。

LinkedBlockingQueue的take()和put()用得是再入锁的两个不同的条件变量,而ArrayBlockingQueue用的是同一个条件变量,所以LinkedBlockingQueue比ArrayBlockingQueue的锁颗粒度要细。但是原理大致一样。在take()方法中有一个while()循环去判断队列是否为空,如果是空,就notEmpty.await()将该线程处于等待状态,如下:

while(0 == count){
    notEmpty.await();
}
......
notEmpty.signal();

如果不是空,线程往下走,take()到值,最后还要通过notEmpty.signal()去唤醒之前因为队列为空而等待的所有线程,这里是一个很巧妙的设计,可以体会一下,也就是说这些因队列为空take()造成等待的线程是靠一个成功take()到值得线程去唤醒的。put()方法也是同理,只不过用notFull条件变量作区分。

前面说的LinkedBlockingQueue的take()和put()用得是再入锁的两个不同的条件变量,而ArrayBlockingQueue用的是同一个条件变量,所以LinkedBlockingQueue比ArrayBlockingQueue的锁颗粒度要细。这里可以通过大致代码对比一下。

//在LinkedBlockingQueue中
//take()中使用的条件变量
private final Reentrantlock takelock = new Reentrantlock();
private final Condition notEmpty = takelock.newCondition();
//put()中使用的条件变量
private final Reentrantlock putlock = new Reentrantlock();
private final Condition notFull = putlock .newCondition();

当然takelock、putlock、notEmpty以及notFull都是定义在take()和put方法外面的 。

//在ArrayBlockingQueue中
//take()和put()中使用同一个条件变量
private final Reentrantlock lock = new Reentrantlock();
private final Condition notEmpty = lock .newCondition();
private final Condition notFull= lock .newCondition();

同上lock、notEmpty以及notFull也是定义在take()和put方法外面的 。

所以,我觉得与其称之为线程安全队列,不如叫线程高效队列,因为这并不涉及理解中的线程安全概念。只是为了take()和put()队列的时候更加高效,退一万步讲,加锁操作是在take()和put()方法内加锁的,是对局部变量加锁,局部变量是线程私有的,不存在线程安全问题。如果不加锁就要有一个判断队列是否为空或者判断长度的过程,省掉了这个过程,如果没有也是报错,这也和线程安全没有关系。

再来说说线程池,先了解一下线程池有三个作用:

1.管理线程的创建与销毁

2.线程复用

3.控制线程并发量

关于第一点,因为线程的创建与销毁是很重的开销,将这些交给线程池来管理可以提高资源利用率。而第二点和第三点就是利用线程池中的工作队列和threadFactoy来实现的,这里的工作队列其实就是一种前面所介绍的线程安全队列,我还是喜欢称之为线程高效队列,里面放的是一个个待执行的任务,也可以理解为任务队列。这么理解,类实现了Runable接口并重写了run()方法,这可以理解为一个任务,只有将其start()后才能叫线程。用线程池管理线程其实就是将一个个任务加入任务队列即工作队列,将start()启动任务变为线程这个工作交给线程池来做。这里就可以理解为什么工作队列要使用线程高效队列了,总不能想启动的时候找不到任务吧,也不能工作队列都满了还往里面塞任务吧。而重要的是这个threadFactoy,顾名思义线程工厂,它是负责创建线程去执行工作队列中的任务的,之所有有第三点可以控制线程并发量就是通过控制threadFactoy创建的线程数。线程创建出来了就会去工作队列中take()任务,这些逻辑是在一个addwork()方法中实现的,当一个线程完成了take()出来的一个任务会再去工作队列中take(0下一个任务,如果工作队列为空即没有任务了,threadFactoy就会销毁这个线程。这就是将线程和任务分开,达到了线程复用的目的。我们平时写一个线程实现Runable并重写run()方法建好任务后,通过start()方法启动线程,执行完这个任务这个线程也就会销毁了,没有复用,而线程的创建和销毁是很重的开销,所以线程池是一个不错的选择。不单单线程池,对象池,内存池以及连接池这些池化技术都值得了解一下。

猜你喜欢

转载自blog.csdn.net/weixin_42447959/article/details/83830504