写在最前
JUC(Java Util Concurrent
)学习内容框架:
1、JUC概述
1.1 JUC概念
JUC是Java Util Concurrent的简称,是Java 5及以后版本中新增的用于支持高并发编程的包。JUC提供了一些线程安全的集合类、原子类、锁、信号量、倒计数器、栅栏等用于并发编程的类和接口,以帮助Java开发者编写高效、安全、稳定的多线程程序。
在传统的Java多线程编程中,开发者使用synchronized关键字来保证线程的同步和协作,但是synchronized存在一些问题,比如只能使用在方法或代码块中、一旦持有锁就无法被其他线程获取等。JUC提供了更加灵活、功能更加强大的同步机制,如Lock接口、Condition接口、Semaphore类、CountDownLatch类、CyclicBarrier类等,可以更好地帮助开发者解决线程同步和协作的问题。
除此之外,JUC还提供了一些并发集合类和原子类,这些类可以在多线程环境下安全地进行操作,从而避免了多线程同时修改数据时的竞争问题,提高了并发编程的效率和稳定性。
总的来说,JUC提供了一组强大的并发编程工具,可以帮助Java开发者更好地利用多核CPU的优势,提高应用程序的并发性能,从而更好地满足现代应用对高并发处理的需求。
1.2 JUC 内容
JUC(Java Util Concurrent)是Java 5及以后版本中新增的用于支持高并发编程的包,其中包含了很多用于并发编程的类和接口,主要包括以下内容:
并发集合类:JUC中提供了一些线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等,这些类可以在多线程环境下安全地进行操作,从而避免了多线程同时修改数据时的竞争问题。
原子类:JUC中提供了一些原子类,如AtomicInteger、AtomicLong、AtomicBoolean等,这些类可以保证在多线程环境下的原子性操作,避免了多线程同时修改数据时的竞争问题。
Lock接口:JUC中提供了一组Lock接口及其实现类,如ReentrantLock、ReentrantReadWriteLock等,Lock接口提供了比synchronized更加灵活的锁操作,可以更精确地控制线程的并发访问。
Condition接口:JUC中提供了Condition接口,可以与Lock接口配合使用,实现更加灵活的线程等待和唤醒操作。
Semaphore类:Semaphore是一种计数信号量,可以用来限制同时访问某个资源的线程数。
CountDownLatch类:CountDownLatch是一种倒计数器,可以用来实现等待多个线程完成后再执行某个操作。
CyclicBarrier类:CyclicBarrier是一种栅栏,可以用来等待多个线程都到达某个状态后再一起执行。
JUC提供的这些类和接口,可以帮助Java开发者更好地编写高效、安全、稳定的多线程程序,从而充分利用多核CPU的优势,提高应用程序的并发性能。
1.3 进程和线程
进程
进程是指在系统中正在运行的一个程序,一个进程可以包含多个线程。每个进程都有自己独立的内存空间和系统资源,不同的进程之间相互独立,互相之间不能访问彼此的内存空间。进程通过操作系统提供的进程调度器来实现进程的调度和管理。
线程
线程是进程中的一个执行单元,是操作系统进行运算调度的最小单位。在一个进程中,多个线程可以共享相同的内存空间和系统资源,可以方便地进行通信和数据共享。线程之间的切换开销较小,可以更加高效地实现并发任务。
由于进程之间的切换开销较大,所以多线程的应用比多进程的应用更加高效,可以更好地利用计算机的资源,提高应用程序的性能。同时,线程之间的共享内存和数据的操作需要进行同步和互斥,否则会出现数据竞争和死锁等问题,因此在多线程编程中需要注意线程的同步和互斥问题。
1.4 线程状态
1.4.1 线程状态枚举类
NEW:(新建)
RUNNABLE:(准备就绪)
BLOCKED:(阻塞)
WAITING:(不见不散)
TIMED_WAITING:(过时不候)
TERMINATED:(终结)
通过一个小案例说明上面的枚举值
你和你朋友越好一起出去玩(NEW新建状态),一切准备就绪(RUNNABLE:准备就绪状态),出发之后,出现了意外你在开车的路上堵车了(BLOCKED阻塞),你们越好的是晚上的八点见面,由于阻塞了,你的朋友有还在等你(WAITING不见不散),也有可能你的朋友已经走了你那么久还没有来(TIMED_WAITING过时不候),最后可能是愉快结束见面,也有可能是不愉快的结束这次出行。(TERMINATED终结)
1.4.2 wait、sleep 的区别
sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。 (在哪里睡就在那里醒来)
它们都可以被 interrupted 方法中断。
1.5 并发与并行
1.5.1 串行模式
串行表示所有任务都一一按先后顺序进行。
串行意味着必须先装完一车柴才能 运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。 串行是一次只能取得一个任务,并执行这个任务。
1.5.2 并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。
并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。
1.5.3 并发
并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象, 并发描述 的是多进程同时运行的现象。但实际上,对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,这里的 “同时运行” 表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一 会停一会。 要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:
可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果
可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或 异步的方式,比如只有准备好产生了事件通知才执行某个任务。
可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这 些小任务,这时很可能要配合多路复用才能达到较高的效率
1.5.4 小结(重点)
并发:
同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:春运抢票、电商秒杀…
并行:
多项工作一起执行,之后再汇总
例子:泡方便面,一边电水壶烧水,一边撕调料倒入桶中
1.6 管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)。但是这样并不能保证进程以设计的顺序执行。
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程 (monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁 执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方 法在执行时候会持有管程,其他线程无法再获取同一个管程
1.7 用户线程和守护线程
1.7.1 用户线程
平时用到的普通线程,自定义线程
1.7.2 守护线程
运行在后台,是一种特殊的线程,比如垃圾回收 当主线程结束后,用户线程还在运行,JVM 存活 如果没有用户线程,都是守护线程,JVM 结束
/** * @author Shier 2023/2/21 12:38 * 用户线程 */ public class UserThread { public static void main(String[] args) { Thread thread = new Thread(() -> { //isDaemon 怕判断是否为用户线程 System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon()); // 来一个死循环 while (true) { } }, "test"); // 设置为守护线程 thread.setDaemon(true);// JVM将会结束进程,进行后台运行 thread.start(); System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon()); } }
2、Lock 接口
2.1 Synchroniezd 锁
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用 的对象是调用这个方法的对象;
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的 所有对象;
修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主 的对象是这个类的所有对象。
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
获取锁的线程执行完了该代码块,然后线程释放对锁的占有
线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一 下,这多么影响程序执行效率。 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等 待一定的时间或者能够响应中断),通过 Lock 就可以办到。
2.1.1 多线程编程步骤
- 创建资源类,在资源类中创建属性和操作方法(上部)
- 在资源列中操作方法
例子售票员买票
package com.shier.sync; /** * @author Shier 2023/2/21 13:07 */ // 1 创建资源类 class Ticket { // 创建属性 private int number = 30; // 同步方法 public synchronized void sale() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (number--) + "张票,剩余:" + number); } } } public class SaleTicket { public static void main(String[] args) { // 调用资源类 Ticket ticket = new Ticket(); // 创建三个线程 new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 40; i++) { ticket.sale(); } } }, "1号").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 40; i++) { ticket.sale(); } } }, "2号").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 40; i++) { ticket.sale(); } } }, "3号").start(); } }
2.2 什么是Lock
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允 许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对 象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。
Lock 是一个类,通过这个类可以实现同步访问;
Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
就好比如上厕所,如果你要去上厕所,就得先判断厕所是否有人,如果有人就得申请(叫他),别人出来了就会把门打开就是释放锁,你进去了就会把门关上(上锁),等你完事之后,就会开门(主动释放锁)。
2.2.1 lock 方法
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他 线程获取,则进行等待。 采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一 般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。
Lock lock = ....; // 上锁 lock.lock(); try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (number--) + "张票,剩余:" + number); } } finally { // 解锁 lock.lock(); }
2.2.2 ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。 ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。下面通过一些实例看具体看一下如何使了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。下面通过一些实例看具体看一下如何使用。
/** * @author Shier 2023/2/21 13:07 */ // 1 创建资源类 class Ticket { // 创建属性,票的数量 private int number = 30; // 创建可重入锁 private final ReentrantLock lock = new ReentrantLock(); // 同步方法 public void sale() { // 上锁 lock.lock(); try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (number--) + "张票,剩余:" + number); } } finally { // 解锁 lock.lock(); } } } public class LockSaleTicket { public static void main(String[] args) { // 调用资源类, Ticket ticket = new Ticket(); // 创建三个线程 new Thread(()->{ for (int i = 0; i < 30; i++) { ticket.sale(); } },"1号").start(); new Thread(()->{ for (int i = 0; i < 30; i++) { ticket.sale(); } },"2号").start(); new Thread(()->{ for (int i = 0; i < 30; i++) { ticket.sale(); } },"3号").start(); } }
2.2.3 newCondition (条件唤醒线程)
关键字 synchronized 与 wait() / notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重 新获得锁并继续执行。
signal()用于唤醒一个等待的线程。
2.2.4 ReadWriteLock
ReadWriteLock 也是一个接口,在它里面只定义了两个方法
Lock readLock(); // 只读锁 Lock writeLock(); // 写锁
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。 ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。
PS
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
2.3 Lock 总结
Lock 和 synchronized 有以下几点不同:
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内 置的语言实现;
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现 象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很 可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。
3、 线程间通信
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析
3.1 多线程编程步骤
创建资源类,在资源类中创建属性和操作方法(上部)
在资源列中操作方法(中部)
判断(是否符合当前的状态)
干活 (具体内容)
通知(告诉另外的一个线程,值已经改变)
创建多个线程,调用资源类的操作方法(下部)
防止虚假唤醒问题
例子:有两个线程,实现对一个初始值是0的变量的操作,一个线程对值 + 1 ,另一个线程对值 -1
使用synchronization实现:
package com.shier.sync; /** * @author Shier 2023/2/21 13:07 */ // 第一步创建资源类 class Share { // 创建一个属性 默认值为 0 private int num = 0; // 对属性加一的方法 public synchronized void increase() throws InterruptedException { // 判断 if (num == 1) { // 等待 this.wait(); } // 干活 num++; System.out.println(Thread.currentThread().getName() + "当前值:" + num); // 通知 this.notifyAll(); } // 对属性减一的方法 public synchronized void decrease() throws InterruptedException { // 判断 if (num == 0) { // 等待 this.wait(); } // 干活 num--; System.out.println(Thread.currentThread().getName() + "当前值:" + num); // 通知 this.notifyAll(); } } public class ThreadSingleCommunication { public static void main(String[] args) { // 创建多个线程, 调用资源类的线程 Share share = new Share(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { share.decrease();// 减一 } catch (InterruptedException e) { e.printStackTrace(); } } }, "减一线程").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { share.increase();// 加一 } catch (InterruptedException e) { e.printStackTrace(); } } }, "加一线程").start(); } }
以上的代码存在着一定的问题:如果是多个线程,比如四个线程就不能出现 1010的效果了,也就是出现了虚假唤醒,所以说得去指定线程执行
// 对属性加一的方法 public synchronized void increase() throws InterruptedException { // 使用while循环中避免虚假唤醒 while (num == 1) { // 等待 this.wait(); } // 干活 num++; System.out.println(Thread.currentThread().getName() + "当前值:" + num); // 通知 this.notifyAll(); }
使用Lock 实现
/** * @author Shier 2023/2/21 16:10 */ // 定义一个资源类 class Share { // 属性 private int number = 0; // 创建锁 private Lock lock = new ReentrantLock(); // 创建钥匙 private Condition condition = lock.newCondition(); // 加一方法 public void increase() throws InterruptedException { //上锁 lock.lock(); try { // 避免虚假唤醒 while (number != 0) { condition.await(); } number++; System.out.println(Thread.currentThread().getName() + "当前值:" + number); condition.signalAll(); } finally { lock.unlock(); } } // 减一方法 public void decrease() throws InterruptedException { //上锁 lock.lock(); try { // 避免虚假唤醒 while (number != 1) { condition.await(); } // 减一操作 number--; System.out.println(Thread.currentThread().getName() + "当前值:" + number); // 唤醒其他线程 condition.signalAll(); } finally { lock.unlock(); } } }
3.2 线程间定制化通信
问题引入:启动三个线程A、B、C,A 线程打印 5 次,B 线程打印 10 次,C 线程打印 15 次,按照此顺序循环 10 轮
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vC7eFyX9-1677050697833)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/2767/04-%E7%BA%BF%E7%A8%8B%E5%AE%9A%E5%88%B6%E5%8C%96%E9%80%9A%E4%BF%A1.png)]
/** * @author Shier 2023/2/21 17:10 */ // 资源类 class ShareResource { // 标志位 private int flag = 1; // 创建lock锁 private Lock lock = new ReentrantLock();= // 创建condition private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); private Condition c3 = lock.newCondition(); // 创建打印五次的方法 public void printFive(int loop) throws InterruptedException { // 上锁 lock.lock(); try { // 避免虚假唤醒 while (flag != 1) { c1.await(); } for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "当前值:" + i + "" + "第" + loop + "次"); } flag = 2; // 唤醒c2 c2.signal(); } finally { lock.unlock(); } } // 创建打印10次的方法 public void printTen(int loop) throws InterruptedException { // 上锁 lock.lock(); try { // 避免虚假唤醒 while (flag != 2) { c2.await(); } for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "当前值:" + i + "" + "第" + loop + "次"); } flag = 3; // 唤醒c3 c3.signal(); } finally { lock.unlock(); } } // 创建打印15次的方法 public void printFifth(int loop) throws InterruptedException { // 上锁 lock.lock(); try { // 避免虚假唤醒 while (flag != 3) { c3.await(); } for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + "当前值:" + i + "" + ",第" + loop + "次"); } flag = 1; // 唤醒 c1 c1.signal(); } finally { lock.unlock(); } } } public class ThreadDemo { public static void main(String[] args) { ShareResource shareResource = new ShareResource(); // 调用几次 int loop = 5; new Thread(() -> { for (int i = 1; i <= loop; i++) { try { shareResource.printFive(i); } catch (InterruptedException e) { e.printStackTrace(); } } }, "1号线程").start(); new Thread(() -> { for (int i = 1; i <= loop; i++) { try { shareResource.printTen(i); } catch (InterruptedException e) { e.printStackTrace(); } } }, "2号线程").start(); new Thread(() -> { for (int i = 1; i <= loop; i++) { try { shareResource.printFifth(i); } catch (InterruptedException e) { e.printStackTrace(); } } }, "3号线程").start(); } }
4、集合的线程安全
ArrayList 线程不安全问题引出
/** * @author Shier 2023/2/21 17:59 */ public class ThreadSecurity { public static void main(String[] args) { // 多个线程对集合进行修改会出现异常 ArrayList<String> arrayList = new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(() -> { arrayList.add(UUID.randomUUID().toString().substring(0, 3)); System.out.println(arrayList); }, String.valueOf(i)).start(); } } }
运行会出现一下的报错异常:并发修改异常
解决线程的并发修改异常有基本的三种
- Vertor
- Collections
- CopyOnWriteArrayList
首先看第一种
4.1 Vertor 和 Collections
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList, 实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功 能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。 RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆
和 ArrayList 不同,Vector 中的操作是线程安全的。1、将上面的代码的ArrayList 改成 Vertor
List<String> list = new Vertor<>();
就不会出现并发修改异常
因为Vertor中的add 是使用 synchronization 同步锁关键字修饰,所以说不会出现异常
2、同样也还可以使用Collections 工具类来进行避免异常,返回的是一个同步的集合
List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
但是以上两种在实际开发过程中并不常用。
4.2 CopyOnWriteArrayList
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:
它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多 于可变操作,需要在遍历期间防止线程间的冲突。
它是线程安全的。
因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
最主要的特点:
独占锁效率低:采用读写分离思想解决
写线程获取到锁,其他写线程阻塞
CopyOnWriteArrayList 的 复制思想:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容 器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素 之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来 得及写会内存,其他的线程就会读到了脏数据。 这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。
List<String> arrayList = new CopyOnWriteArrayList()
这样创建也不会出现线程安装问题。
原因分析(重点):动态数组与线程安全 下面从“动态数组”和“线程安全”两个方面进一步对 CopyOnWriteArrayList 的原理进行说明。
动态数组机制
它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据 时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该 数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的 操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话, 效率比较高。
线程安全机制
通过 volatile 和互斥锁来实现的。
通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看 到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读 取到的数据总是最新的”这个机制的保证。
通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”, 再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥 锁”,就达到了保护数据的目的。
4.3 HashSet、HashMap集合解决并发异常(线程不安全)
Set集合
Set<String> arrayList = new CopyOnWriteArraySet<String>();
Map集合
Map<String, String> hashMap = new ConcurrentHashMap<>();
4.4 小结
线程安全与线程不安全集合 集合类型中存在线程安全与线程不安全的两种,
常见例如: ArrayList ----- Vector HashMap -----HashTable 但是以上都是通过 synchronized 关键字实现,效率较低
2.Collections 构建的线程安全集合
java.util.concurrent 并发包下 CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安 全个方面保证线程安全
5、多线程锁
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的 一个 synchronized 方法, 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法。
锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法
加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁了,情况立刻变化。
synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。 具体表现为以下 3 种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的 Class 对象。
对于同步方法块,锁是 Synchonized 括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。 也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方 法必须等待获取锁的方法释放锁后才能获取锁, 可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁, 所以不需要等待该实例对象已获取到锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才 能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同 步方法之间,只要它们同一个类的实例对象!
5.1 公平锁和非公平锁
源码:
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ 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(); }
5.1.1 非公平锁
非公平锁是指一个线程将任务都执行完毕,导致其他的线程处于一个饿死的状态。
缺点:
- 导致线程饿死
优点:
- 效率高
5.1.2 非公平锁
公平锁是指每一个线程都能得到任务执行,不会导致其他的线程处于一个饿死的状态。
缺点:
- 效率较低
优点:
- 线程不会出现饿死
5.2 可重入锁(递归锁)
synchronization(隐式锁)、lock(显示锁)
相当于回家开门,也就是你只要打开了大门的锁,就可以自由的(无障碍)进入里面的任何一个门(锁)
public class EnableInLock { public static void main(String[] args) { // synchronization 可重入锁 Object o = new Object(); new Thread(()->{ synchronized (o) { System.out.println("外层"); synchronized (o) { System.out.println("中层"); synchronized (o) { System.out.println("内层"); } } } }).start(); } }
Lock 可重入锁
上锁和释放锁都必须主动完成,不能缺少释放锁
// lock 可重入锁 ReentrantLock lock = new ReentrantLock(); new Thread(() -> { lock.lock(); try { System.out.println("lock可重入锁外层"); lock.lock(); try { System.out.println("lock可重入锁内层"); }finally { lock.unlock(); } }finally { lock.unlock(); } }).start();
5.3 死锁
死锁:两个或者两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们将一致处于死锁状态。
产生死锁的原因:
- 系统资源不足
- 进程运行顺序不合适
- 资源分配不当
/** * @author Shier 2023/2/21 20:27 */ public class DeadLock { public static void main(String[] args) { Object a = new Object(); Object b = new Object(); new Thread(() -> { synchronized (a) { System.out.println(Thread.currentThread().getName() + "试图获取锁b"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println(Thread.currentThread().getName() + "a获取到锁b"); } } }).start(); new Thread(() -> { synchronized (b) { System.out.println(Thread.currentThread().getName() + "试图获取锁a"); System.out.println(Thread.currentThread().getName() + "试图获取锁b"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a) { System.out.println(Thread.currentThread().getName() + "b获取到锁a"); } } }).start(); } }
验证死锁
- jps -l :查看当前进程
- jstack 进程号