JUC、多线程环境下使用ArrayList和HashMap

JUC

JUC全称:java.util.concurrent 我们熟悉的各种集合类都是在java.util下的,比如scanner,random等等。concurrernt就是java.util下的一个子包,翻译过来就是“并发的”,其中放了并发编程(多线程)相关的组件。

1.Callable接口

类似于Runnable,Runnable用来描述一个任务,描述的任务没有返回值。Callable也是用来描述一个任务,描述的任务是有返回值的。如果需要使用一个线程单独的计算出某个结果来,此时使用Callable是比较合适的。下面通过使用Callable来计算1+2+3+...+1000的值:

 需要注意的是,不能直接把callable传入到Thread的构造方法里,需要套上一层其他的辅助类!使用FutureTask来作为辅助类传入到Thread构造方法中。最后打印结果:

结果如图:

 2.ReentrantLock

ReentrantLock是标准库给我们提供的另一种锁。顾名思义,是“可重入的”。synchronized是直接基于代码块的方式来加锁解锁的,而ReentrantLock更加传统,使用了lock和unlock的方法加锁解锁。如图:

 这种写法带来的最大问题就是,unlock可能会执行不到,代码中间如果存在ruturn或者一些异常,都可能会导致unlock不能顺利执行,如图:

 建议的用法就是:把unlock代码块放到finally中,无论如何finally代码块都会被执行到,解决了上述问题。

 上面讲的是ReentrantLock的劣势,但是也是有优势的。

1)ReentrantLock提供了公平锁版本的实现

 2)对于synchronized来说,提供的加锁操作就是“死等”。只要获取不到锁,就一直阻塞等待。ReentrantLock提供了更灵活的等待方式:tryLock。

 无参数版本,意思是能加锁就加,加不上就放弃。有参数版本意思是,指定了一个超过时间,加不上锁就等待一会,如果等一会时间到了也没加上,就放弃。

3)ReentrantLock提供了一个更强大,更方便的等待通知机制。

synchronized搭配的是wait,notify。notify的时候是随机唤醒一个wait的线程。

ReentrantLock搭配一个Condition类,进行唤醒的时候可以唤醒指定的线程。

虽然ReentrantLock有一定的优势,但是在实际开发中,大部分情况还是使用的synchronized。

3.原子类

原子类内部用的是CAS实现,所以性能要比加锁实现i++高很多。原子类有以下几个:

4.信号量Semaphore

信号量在生活中经常可以见到,比如停车场的车位是有固定上限的,很多停车场会在入口显示一个牌子:牌子上写,当前空闲车位有xx个。每次有车从入口进去,计数器就-1;每次有车从入口出来,计数器就+1;如果当前停车场里面的车满了,计数器就是0了。这个时候如果还有车想停进去,就面临两个选择:1.在这里等。2.放弃这里,去找别的停车场。所以,信号量Semaphore本身就是一个计数器,描述了“可用资源的个数”。信号量提供了两种操作:

1.P操作:申请一个可用资源,计数器就要-1

2.V操作:释放一个可用资源,计数器就要+1

P操作如果要是计数器为0了,继续P操作,就会出现阻塞等待的情况。提出信号量概念的大佬,叫做 迪杰斯特拉,PV是出自于荷兰语的单词首字母。

现在考虑一个计数初始值为1的信号量,针对这个信号量的值,就只有1和0两种取值(信号量不能是负的),执行一次P操作,1->0;执行一次V操作,0->1。如果已经进行了一次P操作了,继续进行P操作,就会阻塞等待。锁可以视为是 计数器为1的信号量,也就是二元信号量。锁是信号量的一种特殊情况,信号量就是锁的一般表达。

在实际开发中,虽然锁是最常用的,但是信号量也是会偶尔用到的,主要还是看实际的需求场景。比如在图书馆借书,图书馆的某一本书有20本,就可以使用一个初始值为20的信号量来表示。每次同学借书,就进行P操作,每次有同学还书,就进行V操作。如果已经计数器为0了,还有同学想继续借这本书,此时就得等。代码中也是可以用Semaphore来实现类似于锁的效果的,来保证线程安全的。如图:

执行结果如图:

 可以发现,当信号量被使用完后,就会进入阻塞状态,直到被release释放后才可以继续执行。

多线程环境使用ArrayList

1.自己加锁。自己使用synchronized或者ReentrantLock(常见)。

2.Collections.synchroniezdList这里会提供一些ArrayList相关的方法,同时是带锁的。使用这个方法把集合类套一层。

3.CopyOnWriteArrayList,简称为COW,也叫做“写时拷贝”。如果针对这个ArrayList进行读操作,不做任何额外的工作。如果进行写操作,则拷贝一份新的ArrayList,针对新的进行修改,修改过程中如果有读操作,就继续读这份旧的数据,当修改完毕了,使用新的替换旧的(本质上就是引用一个新的赋值,原子的)。很明显,这种方案优点是不需要加锁;缺点则是要求这个ArrayList不能太大。什么样的情况下可以用到这种写时拷贝的方式呢?一个典型的场景就是服务器程序的配置与维护。服务器程序的配置文件,可能会需要进行修改,而修改配置文件可能就需要重启服务器才能生效。但是重启操作可能成本比较高。假设一个服务器重启需要花费5min,假设有20台这样的服务器,总的重启时间就有100min。为了避免经济损失,很多服务器都提供了“热加载”(reload)这样的功能,通过这样的功能就可以不重启服务器,实现配置的更新。热加载的实现,就可以使用刚才所说的 写时拷贝的思路。将新的配置放到新的对象中,加载过程中,请求仍然基于旧的配置进行工作,当新的对象加载完毕,使用新对象替代旧对象,替换完成之后,旧的对象就可以释放了。

多线程使用哈希表

HashMap是线程不安全的,而HashTable是线程安全的,HashTable给关键方法加上了synchronized关键字,更推荐使用的是ConcurrentHashMap,是优化后的线程安全的哈希表。那么,ConcurrentHashMap进行了哪些优化,比HashTable好在哪里,和HashTable之间的区别又是啥?

1.最大的优化之处:ConcurrentHashMap相比于HashTable大大缩小了锁冲突的概率,把一把大锁,转换成多把小锁了。HashTable的做法是直接在方法上加synchronized,等于是给this加锁。只要操作哈希表上的任意元素,都会产生加锁,也就都有可能发生锁冲突。但是实际上,其实基于哈希表的结构特点,有些元素在进行并发操作的时候,是不会产生线程安全问题的,也就不需要用锁控制!举个例子:

此时,元素1,2在同一个链表上,如果线程A修改元素1,线程B修改元素2,就会产生线程安全问题,因为两个元素相邻,此时并发的插入或删除,就需要修改这俩节点相邻的节点的next的指向。但是此时如果线程A修改元素3,线程B修改元素4,就不需要加锁了,这个情况就相当于多个线程修改不同的变量。HashTable不管任何情况都会加锁,导致性能下降。所以HashTable的锁冲突概率就太大了,任何两个元素的操作都会有锁冲突,即使是在不同的链表上。

而ConcurrentHashMap的做法是,每个链表有各自的锁(不是大家共用一个锁了),具体来说,就是使用每个链表的头结点作为锁对象,这时两个线程针对同一个锁对象加锁,才会有锁竞争,针对不同对象加锁时,就没有锁竞争,如图:

 此时把锁的粒度变小了,针对1,2这种情况就是针对同一把锁进行加锁,保证了线程安全。针对3,4这种情况,是针对不同的锁进行加锁,就不会有锁竞争了,程序效率就会更高。上述谈到的情况是针对JDK1.8及以后的情况,在JDK1.7以及之前,ConcurrentHashMap使用的是“分段锁”,如图:

 分段锁的本质上也是缩小锁的范围,从而降低锁冲突的概率,但是这种做法粒度切分的还不够细,另一方面,代码实现也更繁琐。

2.ConcurruntHashMap做了一个激进的操作,针对读操作不加锁,只针对写操作加锁。

3.ConcurrentHashMap内部充分的使用了CAS,通过这个也来进一步地削减加锁操作的数目。

4.针对扩容,采取了“化整为零”的方式

HashMap/HashTable扩容:

创建于一个更大的数组空间,把旧的数组上的链表上的每一个元素搬运到新的数组上。这个扩容会在某一次put操作的时候触发,如果元素个数特别多,就会导致这样的搬运操作非常耗时。

ConcurrentHashMap扩容:

扩容采取的是每次搬运一小部分元素的方式,创建新的数组,旧的数组也保留,每次put操作,都往新数组上添加,同时进行一部分搬运(把一小部分旧的元素搬运到新的数组上),每次get的时候,则旧数组和新数组都查询,每次remove的时候,把元素删了就可以了。经过一段时间后,所有的元素都搬运好了,最终再释放旧数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚报大街-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值