文章目录
- 一、JUC的常见类
- 1.Callable接口
- 2.ReentranrLock
- 1.ReentranLock的优势
- 1.两种加锁方法
- 2.提供了公平锁的实现
- 3.提供了更强大的等待通知机制。
- 二、线程安全的集合类
- 1.多线程环境使用ArraList
- 1.synchronizedList
- 2.CopyOnWriteArrayList
- 写时拷贝。
- 局限性:
- 2.多线程环境使用队列
- 3.多线程环境使用哈希表
- 1.ConcurrentHashMap
JUC_5">一、JUC的常见类
JUC :Java.util.concurrent
并发(这个包里的内容,主要就是一些多线程相关的组件)
Callable_11">1.Callable接口
- 也是一种创建线程的方式,配合FutureTask使用,FutureTask保存任务执行的结果
- 适用于想让某个线程执行一个逻辑,并且返回结果的时候
相比之下,Runnable不关注结果。
java"> public static void main(String[] args) throws ExecutionException, InterruptedException {//定义任务Callable<Integer> callable = new Callable<Integer>() {//泛型的参数类型就是希望返回值的参数类型@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 1001; i++) {sum += i;}return sum;}};//把任务放进线程中去执行//callable不能直接传进线程当中,需要FutureTask类进行包装FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t1 = new Thread(futureTask);t1.start();//get方法来获取到callable的返回结果//由于线程是并发执行的,执行到get的时候,t线程可能还没执行完,get就会进行阻塞。System.out.println(futureTask.get());}
- callable不能直接传进线程当中,需要FutureTask类进行包装
- t线程还没执行完时,get就会进行阻塞
就类似于食堂点餐,点餐完成之后会给你一个取餐牌,后厨就相当于一个线程,开始执行,这个过程需要进行等待。直到饭做好了,就可以凭餐牌取餐。futureTask就是小票,拿着小票来换取线程执行的结果。
创建线程的方式:
1.直接继承Thread,重写run(创建一个类/匿名内部类)
2.实现Runnable,重现run
3.实现Callable,重写call
4.Lambda表达式
5.ThreadFactory线程工厂
6.线程池
2.ReentranrLock
- 可重入锁,效果和synchronized类似。
java"> ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try { // working
} finally { lock.unlock()
}
要记得调用unlock(),进行解锁
ReentranLock_85">1.ReentranLock的优势
1.两种加锁方法
lock() :加锁后,如果遇到锁冲突,就会一直阻塞等待。
tryLock() : 尝试去加锁,如果没加上锁,就放弃了。
2.提供了公平锁的实现
默认情况下是非公平锁,可以通过构造方法传入一个 true 开启公平锁模式
公平锁(队列-实现加锁顺序)
3.提供了更强大的等待通知机制。
synchronized 是通过 Object 的 wait / notify 实现等待-唤醒,每次唤醒的线程是随机的
ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程
二、线程安全的集合类
数据结构中的集合类大部分都是线程不安全的
Vector.Stack,HashTable的线程安全的。(都带有Synchronized)
Stack继承自Vector。Vector和HashTable都是历史遗留,是早期Java引入的集合类。在方法上都加上了synchronized.
针对其他线程不安全的集合类,如果在多线程环境中使用,就需要考虑线程安全问题。
可以自行进行加锁,同时Java标准库也提供了一些搭配的组件,来保证线程安全。
1.多线程环境使用ArraList
1.synchronizedList
java">Collections.synchronizedList(new ArrayList)
- 会返回一个新的对象,相当于给ArrayList套了层壳。在方法上直接使用synchronized。
就相当于ArrayList和加锁的外壳是可拆分的,吸取了Vector的教训,把本体和外壳分离开。在单线程环境下用本体,在多线程环境下用套外壳的本体。
2.CopyOnWriteArrayList
写时拷贝。
比如:多个线程同时使用一个ArrayList。可能会读,也可能会修改。
-
如果两个线程都是读操作,不会涉及到线程安全问题。
-
如果某个线程要进行修改,就把ArrayList复制出一份副本。修改线程就去修改这个副本,与此同时,另一个线程仍然可以读取数据(从原来的数据上进行读取),并不会相互干扰。
一旦修改完毕,就会使用修改好的数据,替换掉原来的数据(往往是引用赋值)
这样的修改过程,就不需要进行加锁。
局限性:
1.当前操作的ArrayList不能太大(拷贝成本不能太高)
2.更适合一个线程来修改,而不能是多个线程同时修改。(多个线程读,一个线程修改的场景)
针对特定场景:适用于服务器的配置跟新。可以通过配置文件来描述配置的详细内容(文件本身不是很大),配置的内容就会被读到内存中,再由其他的线程,来读取这里的内容。而修改这个配置内容,往往只有一个线程来修改。使用某个命令让服务器重新加载配置,就可以使用写时拷贝的方式。
2.多线程环境使用队列
ArrayBlockingQueue 基于数组实现的阻塞队列
LinkedBlockingQueue 基于链表实现的阻塞队列
PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
TransferQueue 最多只包含一个元素的阻塞队列
3.多线程环境使用哈希表
Hashtable保证线程安全的方式,主要就是给关键方法加上synchromized。只要两个线程,操作同一个Hashtable,就会出现锁冲突。
实际上,如果不考虑触发扩容的前提下。对于链地址法的哈希表来说,操作不同链表上的内容,此时是线性安全的。只有在操作同一个链表上的内容时,才会发生线程安全问题。但是整个Hashtable只有一把锁。
1.ConcurrentHashMap
- 1.调整了锁的粒度
ConcurrentHashMap最核心的改进,就是把一个全局的大锁,改进成每个链表独立的小锁。从而大幅度的降低锁冲突的概率。
把每个链表的头结点,作为锁对象
分段锁:Java8之前,concurrentHashMap就是基于分段锁的方式实现的(多个链表共用一把锁)。从Java8之后,就成了直接在链表头结点加锁。
- 2.充分利用到了CAS的特性,把一些不必要加锁的环节省略了。
比如使用一个变量来记录哈希表的中的元素个数,此时就没必要对变量来进行加锁,直接通过原子操作。用CAS来维护元素个数。
- 3.针对读操作没有加锁。(激进)
读和读之间,读和写之间,都没有锁竞争。写和写还需要加锁。
底层修改的时候,避免了使用++这种非原子操作。而是使用 赋值“=”进行修改,保证写操作本身就是原子的。读的时候要么是写之前的旧值,要么是写之后的新值,不会出现读一半的情况。
- 4.针对扩容操作,做了单独的优化。
本身Hashtable或者HashMap在扩容的时候,都需要把所有元素都拷贝一遍。如果元素很多,拷贝就比较耗时。在极端情况下,用户访问了1000次,999次很流畅。而第1000次触发扩容,就会造成严重的卡顿问题。
- 用化整为零的方法来解决:一旦需要进行扩容,确实需要搬运。但是不是一次搬运完,而是分为了多次进行搬运。每次只搬运一部分数据来避免单次操作过于卡顿的问题。
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去. 扩容期间, 新老数组同时存在. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素. 搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加. 这个期间, 查找需要同时查新数组和老数组。
点击移步博客主页,欢迎光临~