基于对多线程的使用和理解,对多线程使用的的类做了一个归类,并对相关内容进行一个简单的分解,如果需要详细了解,请自己查询相关资料。
![](https://i-blog.csdnimg.cn/blog_migrate/f0948734f726811dd676277824d6153e.png)
上图是大家经常在多线程中或者编程中使用的类
变量
一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个
对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存
变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量
值有任何关系,而是直接修改副本变量的值。
Atomic
包含多个原子操作类,比较常用的到的AtomicInteger,AtomicLong。
处理器基本都支持CAS,借助硬件的相关指令,CAS的基本思路就是,如果这个地址上的值
和期望 的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。
采用UnSafe的compareAndSwap方法
对于Atomic能实现线程同步,其实是CPU和内存,利用CPU的多处理能力,实现硬件层面的
阻塞, 再加上
类似volatile变量的特性即可实现基于原子操作的线程安全。
volatile
volatile不是原子操作,只是定义的变量存储在主内存中,
1.每次获取数据都是从主内存中重新读取。
2.修改数据后,需要刷新到主内存中。
使用场景
1.写入的变量值不依赖读取的变量值。
2.该变量不是另外变量表达式中的值。
transient
常用来对该变量不进行序列化,对于存储在硬盘或者其他的数据流传输中,这个字段值为空。
简单来说就是 当对象存储时,它的值不需要维持。
ThreadLocal
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地
改变自己的副本,而不会影响其它线程所对应的副本。副本的实现是采用
ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,
Map中元素的键为线程对象,而值对应线程的变量副本。
锁的类型
synchronized
Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。
如果需要对其原理进行分析,需要对监视器锁(monitor)
指令(monitorenter,monitorexit) 机制进行了解。
Lock
ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”,可以被单个线程多次占有,
分为“公平锁”和“非公平锁”。通过一个FIFO的等待队列来管理获取该锁所有线程的。
ReadWriteLock :读写锁,分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥。
Condition
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在
lock.lock()和 lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
concurrent
这个架包下有很多工具类,先从
BlockingQueue
阻塞队列开始,队列一般采用的是先进先出
![](https://i-blog.csdnimg.cn/blog_migrate/a38e80cbf6fae3a1e2a6cb9104642559.jpeg)
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
1. ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行。
2. LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列。LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
3. DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:
DelayQueue使用场景较少,但都相当巧妙。
4. PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
需要注意的是,如果按poll()/take()方法获取出来的数据是排序的,因为每次获取的数据吧保证优先级最高的放在前面,其他的元素不排序。每次消费一个,马上排序一个优先级最高的到头部。
5. SynchronousQueue
是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的
移除操作take,反过来也一样。
线程池
在多线程中,线程池是非常常用的工具。
Java
里面线程池的顶级接口是
Executor
,但是严格
意义上讲
Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口
是
ExecutorService。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,
很有可能配置的线程池不是较优的,因此在
Executors
类里面提供了一些静态工厂,
生成一些常用的线程池。
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程,
在内部是有一个queue存储提交的线程,这个是无固定大小的
。
3. newCachedThreadPool
ExecutorService pool = Executors.
newCachedThreadPool
();
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(
60
秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
此线程池不会对线程池大小做限制
,线程池大小完全依赖于操作系统(或者说
JVM
)能够创建的最大线程大小。
4.newScheduledThreadPool
ScheduledThreadPoolExecutor exec =
new
ScheduledThreadPoolExecutor(1);
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求,
输入参数为线程池保持的活跃线程数量,但是最大的线程数量为无限大小。
ThreadPoolExecutor
是
Executors
类的底层实现。
在
JDK
帮助文档中,有如此一段话:
“
强烈建议程序员使用较为方便的
Executors
工厂方法
Executors.newCachedThreadPool()
(无界线程池,可以进行自动线程回收)、
Executors.newFixedThreadPool(int)
(固定大小线程池)
Executors.newSingleThreadExecutor()
(单个后台线程)。