🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (91平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 2.2 手动锁ReentrantLock
- 2.2.1 用法
- 2.2.2 ReentrantLock与synchronized的区别
- 2.3 信号量Semaphore
- 2.4 CountDownLatch
- 2.5 线程池
- 2.6 原子类
- 3. 线程安全的集合类
- 3.1 多线程情况下使用ArrayList
- 3.2 多线程使用队列
- 3.3 多线程环境下使用哈希表
- 3.3.1 Hashtable
- 3.3.2 ConcurrentHashMap(高频面试题)
2.2 手动锁ReentrantLock
可重入互斥锁,和synchronized功能类似,都是用来实现互斥效果,保证线程安全.
2.2.1 用法
- lock(),加锁,如果获取不到锁就会死等.
- tryLock(),枷锁,如果一定时间内获取不到锁就放弃加锁.
- unlock(),解锁.
在进行加锁解锁操作的时候,为了防止加锁之后未解锁的情况,我们在使用ReentrantLock的时候,一般使用try-finally结构来完成.未解锁操作并不是程序员忘记写unlock造成的,而是中间出现了一些例如在执行到某个时候直接return,或者在某些时候抛出异常,使得程序终止的操作.这时候unlock操作就执行不到了.而finally可以完美地解决这个问题.
代码如下:
public class Demo29 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();//公平
reentrantLock.lock();
try {
//...
}finally {
reentrantLock.unlock();
}
}
}
2.2.2 ReentrantLock与synchronized的区别
- ReentrantLock需要手动加锁和手动解锁,而synchronized自动加锁解锁.
- synchronized在申请失败的时候,会死等,而ReentrantLock可以使用tryLock方法来限制等待的时间.
- synchronized是非公平锁,而ReentrantLock可以通过构造方法来规定这把锁是否是公平锁.
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock reentrantLock = new ReentrantLock(true);//公平
ReentrantLock reentrantLock1 = new ReentrantLock(false);//非公平
- ReentrantLock拥有强大的唤醒机制,他可以指定线程唤醒,而synchronized如果有多个线程在wait的时候,notify只能唤醒随机的其中一个线程.
2.3 信号量Semaphore
信号量,就是一个表示资源剩余个数的量,本质上就是一个计数器.
举例说明:停车场自动化停车
在每一个停车场的起杆的地方,都会显示当前剩余车位:xxx,只要有一辆车进入停车场,显示器上的停车位就会-1(称为p操作),有一辆车出来,就会+1(称为v操作),当车位已满的时候,停车场的杆就不会自动抬起.想要进来的车就要阻塞等待,等待有车从停车场出来才可以进去.
Semaphore的操作都是原子的,可以在多线程环境下直接使用.
代码实例:
- acquire用来申请资源,release用来释放资源.
class demo{
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
for (int i = 0; i < 4; i++) {
semaphore.acquire();
System.out.println("申请第"+(i+1)+"个资源");
}
for (int i = 0; i < 4; i++) {
semaphore.release();
System.out.println("释放第"+(i+1)+"个资源");
}
}
}
运行结果:
- 如果在资源使用完之后,再去申请资源,就会阻塞等待.
class demo{
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
for (int i = 0; i < 4; i++) {
semaphore.acquire();
System.out.println("申请第"+(i+1)+"个资源");
}
semaphore.acquire();
}
}
运行结果:
我们看到,进行并未结束.
2.4 CountDownLatch
这个类是一个比较实用的工具类,他的主要功能就是:当一个任务被拆分成许多部分让多个线程执行的时候,就可以通过这个类来判断任务是否全部执行完毕.
就比如我们经常用到的一个下载工具:IDM下载器,这个下载工具的下载速度非常快,就是因为这个下载工具会把下载任务拆分成多个任务,等待所有下载任务全部结束之后,载合并下载结果,这时候就需要有一个工具来判断所有任务是否全部下载完毕.
举例说明:跑步比赛
10个选手同时起泡,有的跑的慢,有的跑的快,但是裁判必须等到所有运动员全部通过终点之后才可以结束比赛.
代码示例:
- 使用countDown方法领取任务.
- 使用await方法判断所有线程是否执行完毕.
import java.util.concurrent.CountDownLatch;
public class Demo30 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);//任务被拆分成了5个部分
for (int i = 0; i < 5; i++) {//把任务分配给5个线程
int finalI = i;
Thread thread = new Thread(()->{
try {
Thread.sleep((finalI +1)*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();//获取任务
System.out.println("任务"+(finalI+1)+"结束");
});
thread.start();
}
latch.await();//等待所有线程结束任务
System.out.println("所有线程任务结束");
}
}
运行结果:
2.5 线程池
前面叙述过,不再赘述
https://blog.csdn.net/2301_80050796/article/details/138388735?spm=1001.2014.3001.5501
2.6 原子类
前面叙述过,不再赘述
https://blog.csdn.net/2301_80050796/article/details/138506092?spm=1001.2014.3001.5501
3. 线程安全的集合类
原来的集合类,大多数都是线程不安全的.但是其中有几个线程是安全的.比如:Vector,Stack,HashTable.这些集合的方法都自带synchronized,都是被synchronized修饰的.但是加了锁也不一定安全,需要具体问题具体分析.
3.1 多线程情况下使用ArrayList
- 自己使用同步机制,synchronized或者ReentrantLock.
Collections.synchronizedList(new ArrayList)
这个操作相当于给ArrayList套了一层壳.相当于之后对ArrayList的操作都带上了synchronized修饰.
上面两种操作都是有加锁的操作,下面我们介绍一种不加锁的操作.- 使用CopyOnWriteArrayList.
CopyOnWrite即写时拷贝容器.
如果我们想要修改一个容器中的值的时候,如果直接进行修改,比如想要修改两个数据,一个线程刚好修改完第一个数据的时候,有第二个线程想要来读取修改后的数据,这时候就读到的是一种"中间结果",不够准确.
这时候就需要引入写时拷贝容器:
- 当我们往一个容器中添加或者修改数据的时候,不直接修改当前容器,而是先拷贝当前容器,之后在复制出的容器中进行修改.
- 在修改完成之后,将原容器的引用指向修改后的容器.
这样如果在有线程去读取数据的时候,如果修改未完成的时候,读取的就是原容器的数据,修改完成之后,就是读取新容器的数据了.所以CopyOnWrite容器采用的便是读写分离思想.
举例说明:不停机更新
在我们玩一个游戏,比如王者荣耀的时候,经常会出现不停机更新这样的现象.在更新的时候,并不会影响用户的游戏体验,在一场游戏结束之后,自动获取游戏更新内容.
3.2 多线程使用队列
- ArrayBlockingQueue 基于数组实现的阻塞队列
- LinkedBlockingQueue 基于链表实现的阻塞队列
- PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
- TransferQueue 最多只包含⼀个元素的阻塞队列
3.3 多线程环境下使用哈希表
HashMap本身是线程不安全的.在多线程环境下使用哈希表可以使用:
- Hashtable
- ConcurrentHashMap
3.3.1 Hashtable
只是简单地把关键方法加上了synchronized.这就相当于直接对Hashtable对象本身直接加锁.
如果一个哈希表只有一把锁,别说在操作同一个哈希桶,即使在不同的线程操作不同的哈希桶的时候,也会产生阻塞,这样的效率是非常低的.而且一旦触发扩容,就会有大量拷贝的操作,这样的效率是非常低的.
3.3.2 ConcurrentHashMap(高频面试题)
相比于Hashtable做出了一些优化.
-
优化1:ConcurrentHashMap对每一个哈希桶都使用了synchronized进行加锁,这就大大降低了锁冲突的概率.或许有的人会想,每个哈希桶都加锁,不是加大了加锁开销吗,其实不是,当没有线程与当前哈希桶进行锁竞争的时候,加锁的锁只是一个偏向锁,开销也没有大多少.反而降低锁冲突的概率收益会很明显.
-
优化2:充分利用了CAS特性.比如size属性就通过CAS来更新.避免出现重量级锁的情况.
-
优化3: 优化了扩容方式:化整为零
- 当发现需要扩容的时候,就会创建一个新的数组出来,同时只拷贝几个元素过去.
- 扩容期间,新数组和老数组同时存在.
- 后续的每个对ConcurrentHashMap的操作都会拷贝几个元素过去.总的来说不是一次性全部拷贝完成,而是分多次拷贝.
- 之后每次插入新元素的时候都直接插入新数组中.
- 当拷贝完成最后一个元素的时候,老数组就会被删除.