4 集合的线程安全
4.1 集合操作 Demo
创建集合使用string的泛型
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
查看源码,主要牵扯这个添加的方法是因为该方法在列表中是不安全的,没有synchronized声明
boolean add(E e);
报错
java.util.ConcurrentModificationException
为并发修改问题
4.2 vector
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList,实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能**。 Vector 实现了 RandmoAccess 接口,即**提供了随机访问功能。RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。
和 ArrayList 不同,Vector 中的操作是线程安全的。
通过list下的实现类Vector
因为在Vector下的add普遍都是线程安全
查看源代码
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
在改变代码时,只需要将其修改为List<String> list = new Vector<>();
但此方法用的比较少,因为在jdk 1.0的版本适用
4.3 Collections
Collections类中的很多方法都是static静态
其中有一个方法是返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list)
具体代码为
List<String> list = Collections.synchronizedList(new ArrayList<>());
此方法也比较古老,很少使用
4.4 CopyOnWriteArrayList
首先我们对 CopyOnWriteArrayList 进行学习,其特点如下:
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和ArrayList 不同的时,它具有以下特性:
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
- 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
- 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
1. 独占锁效率低:采用读写分离思想解决
2. 写线程获取到锁,其他写线程阻塞
3. 复制思想:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来 得及写会内存,其他的线程就会读到了脏数据
这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。
将其代码修改为
List<String> list = new CopyOnWriteArrayList<>();
涉及的底层原理为写时复制技术
-
读的时候并发(多个线程操作)
-
写的时候独立,先复制相同的空间到某个区域,将其写到新区域,旧新合并,并且读新区域(每次加新内容都写到新区域,覆盖合并之前旧区域,读取新区域添加的内容)
-
public boolean add(E e) { synchronized (lock) { Object[] es = getArray(); int len = es.length; es = Arrays.copyOf(es, len + 1); es[len] = e; setArray(es); return true; } }
原因分析(重点):动态数组与线程安全
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明。
- “动态数组”机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的 操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话, 效率比较高。
- “线程安全”机制
- 通过 volatile 和互斥锁来实现的。
- 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
- 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。
4.5 CopyOnWriteArraySet
该类是HashSet的实现类
同样使用HashSet类,也会出现线程不安全
java Set<String> set = new HashSet<>();
需要将上面的代码改为
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
4.6 ConcurrentHashMap
ConcurrentHashMap
类是HashMap的实现类
先讲述其线程不安全实列
HashMap不安全线程也同理Map<String,String> map = new HashMap<>();
具体实现代码是
for (int i = 0; i <30; i++) {
String key = String.valueOf(i);
new Thread(()->{
//向集合添加内容
map.put(key,UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(map);
},String.valueOf(i)).start();
}
将其代码修改为
Map<String,String> map = new ConcurrentHashMap<>();
通过这行代码可以编程线程安全
4.7 volatile关键字的作用
1、保证可见性;
2、防止指令重排;
3、但是不保证原子性;
4.7.1 可见性是什么?
在JMM(java memory model)java内存模型中,其他线程从主内存空间把值拷贝到自己的工作空间,线程修改之后的值会返回给主内存,主内存会通知其他线程,此为可见性。
4.7.2 指令重排
CPU为了执行效率会并发执行操作指令,volatile可以使指令一个一个的执行。
4.7.3 如何解决原子性问题
1、通过synchronized关键字。
2、通过使用AtomicXX,不加锁,采用CAS(compareAndSet)解决。其本质是使用UnSafe本地方法(CPU原语)。
3、使用LongAdder:最快(在线程多的情况下,使用分段锁)
4.8 写时复制(Copy-on-write)
简称COW)是一种计算机程序设计领域的优化策略。
核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。
此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
JDK的CopyOnWriteArrayList/CopyOnWriteArraySet容器就采用了COW思想。
4.8.1 vector的缺陷
容器是线程安全的,并不意味着就可以在多线程环境下放心大胆地随便。
在一个线程中使用Iterator迭代器遍历vector,同时另一个线程对vector作修改时,会抛出java.util.ConcurrentModificationException异常。
4.8.2 实现原理
Copy-on-write是解决并发的的一种思路,指的是实行读写分离,如果执行的是写操作,则复制一个新集合,在新集合内添加或者删除元素。待一切修改完成之后,再将原集合的引用指向新的集合。好处就是,可以高并发地对COW进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素。
CopyOnWriteArrayList的核心理念就是读写分离,写操作在一个复制的数组上进行,读操作还是在原始数组上进行,读写分离,互不影响。写操作需要加锁,防止并发写入时导致数据丢失。写操作结束之后需要让数组指针指向新的复制数组。
4.8.3 CopyOnWriteArrayList优缺点总结
优点
- 对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
- CopyOnWriteArrayList并发安全且性能比Vector好。Vector是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector。
缺点
- 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了,但是线程A迭代出来的是旧数据。
- 内存占用问题。如果CopyOnWriteArrayList经常要增删改里面的数据,并且对象比较大,频繁地写会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。
4.9 小结(重点)
1.线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:
ArrayList ----- Vector
HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低
2.Collections 构建的线程安全集合
3.java.util.concurrent 并发包下
CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全
5 多线程锁
某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象
synchronized锁的是方法,则是对象锁
同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
加了static则为class锁而不是对象锁
5.1 锁的八个问题演示
class Phone {
public synchronized void sendSMS() throws Exception {
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
通过调用锁的对象机制
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
// phone.sendEmail();
// phone.getHello();
phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
具体八种情况为
1 标准访问,先打印短信还是邮件
------sendSMS
------sendEmail
2 停4秒在短信方法内,先打印短信还是邮件
------sendSMS
------sendEmail
3 新增普通的hello方法,是先打短信还是hello
------getHello
------sendSMS
4 现在有两部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5 两个静态同步方法,1部手机,先打印短信还是邮件
------sendSMS
------sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5.2 公平锁和非公平锁
- 公平锁:效率相对低
- 非公平锁:效率高,但是线程容易饿死
通过查看源码
带有参数的ReentrantLock(true)
为公平锁
ReentrantLock(false)
为非公平锁
主要是调用NonfairSync()
与FairSync()
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
具体其非公平锁与公平锁的源码
查看公平锁的源码
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* Acquires only if reentrant or queue is empty.
*/
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
private final ReentrantLock lock = new ReentrantLock();
默认是初始化一个非公平锁,如果要实现公平锁,private final ReentrantLock lock = new ReentrantLock(true);
5.3 可重入锁
synchronized和lock都是可重入锁(也叫递归锁)
- sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
- 可重入锁也叫递归锁
而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+" 外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 内层");
}
}
}
},"t1").start();
synchronized (o)
代表锁住当前{ }
内的代码块
以上都是synchronized锁机制
下面讲解lock锁机制
public class SyncLockDemo {
public synchronized void add() {
add();
}
public static void main(String[] args) {
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
//创建新线程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
}
}
在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁
要把握上锁与解锁的概念,都要写上
5.4 死锁(面试重点)
两个或以上的进程因为争夺资源而造成互相等待资源的现象称为死锁
产生死锁的原因:
- 系统资源不足
- 系统资源分配不当
- 进程运行顺序不当
具体死锁的操作代码实列
可理解背下来,大厂面试可考,死锁的简单案例
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName()+" 获取锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b) {
System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" 获取锁a");
}
}
},"B").start();
}
}
验证是否是死锁(要求掌握)
- jps 类似于linux中的
ps -ef
查看进程号 - jstack 自带的堆栈跟踪工具
通过用idea自带的命令行输入 jps -l
查看其编译代码的进程号后jstack 进程号
5.5 总结
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法。
锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。
synchronized实现同步的基础: Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Clas对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。