JUC 是 Java并发编程的缩写,指的是 Java.util.concurrent 即Java工具集下的并发编程库 【说白了就是处理线程的工具包】
JUC提供了一套并发编程工具,这些工具是Java 5以后引入的,使得Java开发者可以更加方便地编写高效的并发程序
JUC包含许多有用的类和接口,如线程池、阻塞队列、同步器、原子变量、并发集合等,它们能够帮助Java开发者编写更加高效和可靠的并发程序
使用JUC可以充分发挥多核CPU的并发优势,提高程序的响应速度和吞吐量,从而提升应用程序的性能和用户体验。
JUC的相关类和接口非常多,使用时需要根据实际需求进行选择和组合,同时需要注意其复杂性和易出现的并发问题,如死锁、竞态条件等。因此,在使用JUC进行并发编程时需要具有一定的经验和技能
1️⃣ 进程与线程
(1)什么是进程?
进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
(2)什么是线程?
线程(thread) 是操作系统能够进行运算调度的最小单位
总结来说:
2️⃣ 线程的状态
在操作系统层面,任何进程都有五种状态
新建状态:新创建了一个线程对象,但是还没有开始执行,此时线程处于新建状态。
就绪状态:线程创建后,通过start()方法启动线程,此时线程进入线程调度器的就绪队列中,等待调度器的调度。
运行状态:线程调度器从就绪队列中选取一个线程,并将其转移到运行态,开始执行run()方法中的代码。
阻塞状态:线程正在执行run()方法中的代码,但被某些原因阻塞了,如等待某个资源、睡眠(Thread.sleep())、等待输入输出完成(IO)等。
死亡状态:线程运行完了run()方法中的代码,或者调用了stop()方法,导致线程结束。
补充: 线程的状态是由操作系统控制的,Java程序只能感知线程状态的改变但无法直接控制。
在Java的线程生命周期中有六种状态
需要注意的是,阻塞状态、等待状态和计时等待状态都属于非可执行状态,因为这些状态下线程无法直接执行其 run() 方法,只有当条件满足之后才能进入运行状态。
3️⃣ wait 和 sleep 有什么区别:
4️⃣ 并发和并行
首先我们需要了解什么是串行模式、什么是并行模式?
(1)串行模式
串行表示所有任务都按先后顺序进行
串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤
串行是一次只能取得一个任务,并执行这个任务
(2)并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务
并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度
并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU
那么什么又是并发呢?
并发(concurrent) 是指在单个处理器上同时处理多个任务的能力,通过CPU时间片轮转的调度方式来实现多个任务交替执行的效果,使得它们在同一个时间段内都获得了执行的机会
由于时间片的切换速度非常快,所以对于用户来说,多个任务看起来像是同时执行的
那么并行和并发相比有什么区别呢?
并行则是指同时使用多个处理器或多核处理器来处理多个不同的任务,每个处理器或核心处理一部分任务,各自互不干扰,最终集中处理器或核心的结果形成总体处理结果
并行通常比并发更具有性能优势,因为它可以利用CPU的多个核心同时进行计算。
综上所述,虽然并发和并行都是指多任务同时执行的情况,但是:
5️⃣ 管程
什么是管程?
管程(Moniotor)是一个并发编程中用于实现互斥和同步的一种工具或模式,它是由荷兰计算机科学家 Edsger W. Dijkstra 首次提出的。主要用于解决共享资源的互斥问题,简化多线程之间的同步操作
通俗地说,管程是一个数据结构和一组操作共同组成的一个抽象,它可以包含一个或多个共享变量以及对这些变量的操作。通过对管程的操作,保证了同一时刻只能有一个线程访问共享变量,从而避免了竞争条件和死锁等并发问题。
管程的基本操作通常包括进入管程(monitor enter)、退出管程(monitor exit)以及条件变量(condition variable) 等。当一个线程进入管程时,它会获得管程的监视权,只有当它退出管程或者调用等待操作时才会放弃监视权。而条件变量则是用于在管程中对线程进行等待和唤醒操作的一种机制。
管程的优点
补充:
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
6️⃣ 用户线程和守护线程
基本概念:
案例演示:
(1)当我们主线程结束,用户线程没有结束,那么JVM还会继续进行下去 【当主线程结束后,用户线程还在运行,JVM 存活】
package com.atguigu.part1;
/**
* @author Bonbons
* @version 1.0
* 演示普通线程和守护线程的区别
*/
public class DaemonThread {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
//获取当前线程
String s = Thread.currentThread().getName() + " ::" + Thread.currentThread().isDaemon();
System.out.println(s);
//设置永真while循环
while(true){
}
}, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + " over");
}
}
(2)我们将用户线程设置为守护线程,当JVM里面没有了运行用户线程,那么守护线程自动结束 【如果没有用户线程,都是守护线程,JVM 结束】
package com.atguigu.part1;
/**
* @author Bonbons
* @version 1.0
* 演示普通线程和守护线程的区别
*/
public class DaemonThread {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
//获取当前线程
String s = Thread.currentThread().getName() + " ::" + Thread.currentThread().isDaemon();
System.out.println(s);
//设置永真while循环
while(true){
}
}, "t1");
//将t1线程设置为守护线程 [在线程启动之前设置]
t1.setDaemon(true);
t1.start();
System.out.println(Thread.currentThread().getName() + " over");
}
}
1️⃣ Synchronized作用范围
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
修饰一个静态的方法 ,其作用的范围是整个静态方法,作用的对象是 这个类的所有对象
修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象
2️⃣ Synchronized实现卖票例子
多线程编程的步骤(上):
通过代码演示如何实现基础版的卖票案例
package com.atguigu.sync;
/**
* @author Bonbons
* @version 1.0
* 使用Synchronized实现卖票,30张票、3个售票员
*/
public class SellTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}, "t2").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}, "t3").start();
}
}
//定义资源类、属性、操作方法
class Ticket{
private int ticket_num = 30;
public synchronized void sell(){
if(ticket_num <= 0){
return;
}
System.out.println(Thread.currentThread().getName() + "卖了: " + ticket_num + ";剩余: " + (--ticket_num));
}
}
3️⃣ 总结:
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到
1️⃣ Lock接口介绍
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
1️⃣ 继承Thread类
创建一个线程时,可以定义一个新的类,继承自 Thread 类,并重写 run() 方法。在新定义的类中,可以调用 start() 方法来启动线程
class MyThread extends Thread{
@Override
public void run(){
System.out.println("This is a new Thread!");
}
}
MyThread mt = new MyThread();
mt.start();
2️⃣ 实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("This is a new Thread!");
}
}
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
3️⃣ 使用Callable接口
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//...
return result;
}
}
// 使用 ExecutorService 提交 Callable 任务
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(new MyCallable());
// 获取 Callable 的执行结果
System.out.println(future.get());
4️⃣ 使用线程池
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个线程池
executor.execute(new Runnable() {
public void run() {
System.out.println("This is a new thread.");
}
});
package com.atguigu.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Bonbons
* @version 1.0
* 使用Lock锁实现售票
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for(int i = 1; i <= 20; i++){
ticket.sale();
}
}, "AA").start();
new Thread(() -> {
for(int i = 1; i <= 20; i++){
ticket.sale();
}
}, "BB").start();
new Thread(() -> {
for(int i = 1; i <= 20; i++){
ticket.sale();
}
}, "CC").start();
}
}
//资源类
class Ticket{
private int num = 30;
//创建Lock锁的对象
private final ReentrantLock lock = new ReentrantLock();
public void sale(){
//上锁
lock.lock();
try{
//判断是否有余票
if(num > 0){
//打印
System.out.println(Thread.currentThread().getName() + "售票: " + (num--) +
" ;剩余: " + (num));
}
}finally {
//释放
lock.unlock();
}
}
}
上面采用的是Lock接口的一个实现类, ReentrantLock(可重入锁)实现的
1️⃣ 先看一下 Lock 接口包含哪些内容:
public interface Lock {
//获得锁,如果锁已被占用,则等待直到获取到锁
void lock();
//获得锁,但如果当前线程被中断,则抛出InterruptedException异常
void lockInterruptibly() throws InterruptedException;
//尝试获得锁,如果锁未被占用,则获取到锁并返回true,否则立即返回false,不会等待
boolean tryLock();
//尝试获得锁,在指定的时间内如果未获得锁,则返回false,否则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁,如果当前线程持有该锁,则释放,并通知等待的线程
void unlock();
//返回一个与当前 Lock 绑定的 Condition 对象,可以用于唤醒等待在该 Condition 上的线程
Condition newCondition();
}
2️⃣ Lock 接口的实现类介绍:
除了上述常见的Lock实现类外,还有一些其他的实现类,例如:
总的来说,在选择使用Java提供的不同Lock实现类时,需要根据具体的应用场景和需求,选择不同的实现方式和策略,以达到最优的多线程同步效果
在Java中,synchronized关键字可用于保证线程安全,实现了线程的互斥访问。在synchronized块中,线程可以等待某些条件的到来,或者通知其他线程已经满足了条件。wait()和notify() 就是用于实现线程之间相互通信的两个重要方法
wait()方法是Object类中的一种,它将当前线程置于睡眠状态,并且释放对象的锁。在等待期间,线程不会占用CPU
wait()方法有三种重载形式:
notify() 和 notifyAll() 被用于唤醒等待的线程
notify() 和 notifyAll()的主要区别:
1️⃣ 通过 Synchronized 实现
package com.atguigu.sync;
/**
* @author Bonbons
* @version 1.0
* 进程间通信:
* 两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
* 使用Synchronized关键字实现
*/
public class TestVolatile {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "BB").start();
}
}
//资源类
class Share{
//共享资源
private int num = 0;
//操作方法
public synchronized void incr() throws InterruptedException {
//判断
if(num != 0){
this.wait();
}
//干活
num++;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
if(num != 1){
this.wait();
}
//干活
num--;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//通知
this.notifyAll();
}
}
Condition 是 Java 并发包中的一种线程通信机制,可以用于协调线程之间的唤醒和等待
Condition 接口提供了一个 newCondition() 方法,该方法返回一个与当前 Lock 绑定的 Condition 对象,可以用于唤醒等待在该 Condition 上的线程
具体来说,newCondition() 方法会返回一个 Condition 对象,它与当前 Lock 相关联
通过调用 condition.await() 方法,一个线程可以将自己挂起等待,并释放锁,直到其他线程通过调用 condition.signal() 或 condition.signalAll() 方法来通知它继续执行
当有一个或多个线程处于等待状态时,它们可以通过 condition.signal() 方法来唤醒其中一个线程,或通过 condition.signalAll() 方法来唤醒所有等待线程
使用 Condition 对象可以有效地避免线程竞争和死锁的问题,提高程序的运行效率和可靠性
它常常被用于多线程协作的场景中,例如生产者消费者模型或者读写锁的实现中
2️⃣ 通过 Lock 实现
package com.atguigu.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Bonbons
* @version 1.0
* 进程间通信:
* 两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
* 使用Lock接口实现类,此处使用重入锁ReentrantLock实现
*/
public class LTestVolatile {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "BB").start();
}
}
class Share{
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try{
if(num != 0){
condition.await();
}
System.out.println(Thread.currentThread().getName() + " :: " + (++num));
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try{
if (num != 1){
condition.await();
}
System.out.println(Thread.currentThread().getName() + " :: " + (--num));
}finally {
condition.signalAll();
lock.unlock();
}
}
}
上面的这两个案例其实设置了一个坑,存在虚假唤醒问题,当我们设置多个线程(超过两个)来完成这部分的功能,这个问题就会浮出水面
多线程通信是指在多个线程间共享数据,一个线程需要等待其它线程的操作结果才能继续执行的情况
而虚假唤醒(Spurious Wakeup)问题,是指一个线程在没有被通知或者没有被条件满足的情况下被唤醒,这种唤醒是不符合预期的,称为虚假唤醒
虚假唤醒问题在多线程编程中是比较常见的,它可能会导致程序出现逻辑错误、数据异常等问题
因此在多线程编程中,需要对虚假唤醒进行处理,以保证程序的正确性和可靠性。
通常解决虚假唤醒问题的方法,是在等待前判断条件是否满足,如果不满足则等待,这样可以避免虚假唤醒引起的问题
在Java中,可以使用while循环来进行条件判断和等待,例如:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 执行需要等待的操作
}
上面的代码在等待之前使用while循环判断条件是否满足,如果条件不满足则一直等待,直到条件满足才会执行需要等待的操作。这样可以有效地避免虚假唤醒引起的问题。
另外,还可以使用Java提供的Condition对象来实现精准的线程等待和唤醒,Condition对象可以理解为是一个等待队列,它能够使得指定的线程等待并放弃锁,同时通知其它线程进行通知(signal)
在使用Condition对象时,需要先通过Lock对象创建一个Condition实例,然后在等待前使用await()方法进行等待,等待时会释放Lock对象的锁,直到其它线程通过signal()方法进行通知,Condition对象再次获取Lock对象的锁,继续执行下面的代码。例如:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionSatisfied) {
condition.await();
}
// 执行需要等待的操作
} catch (InterruptedException e) {
// 处理中断异常
} finally {
lock.unlock();
}
上面的代码使用Lock对象创建了一个Condition实例,然后在等待前使用await()方法进行等待,等待时会释放Lock对象的锁,直到其它线程通过signal()方法进行通知,Condition对象再次获取Lock对象的锁,继续执行下面的代码。
总之,对于多线程通信时的虚假唤醒问题,我们需要在进行等待操作前先进行条件判断,确保只有在条件满足时才会进行等待。此外,使用Condition对象能够更加精确地进行线程等待和唤醒,从而避免虚假唤醒引起的问题。
要实现线程定制化通信,可以使用以下方式:
在选择线程间通信的方式时,应该根据特定的应用程序需求和环境条件来选择最优的实现方式
案例分析:A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
package com.atguigu.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Bonbons
* @version 1.0
* 使用ReentrantLock定制化多线程通信
* A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
*/
public class ThreadCommunication {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(() -> {
try {
threadDemo.print5();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "c1").start();
new Thread(() -> {
try {
threadDemo.print10();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "c2").start();
new Thread(() -> {
try {
threadDemo.print15();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "c3").start();
}
}
class ThreadDemo{
private int num = 0;
private ReentrantLock lock = new ReentrantLock();
//三个通信钥匙
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
//三个方法
public void print5() throws InterruptedException {
lock.lock();
try{
if(num != 0){
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + num);
}
num = 1;
c1.signalAll();
}finally {
lock.unlock();
}
}
public void print10() throws InterruptedException {
lock.lock();
try{
if(num != 1){
c1.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + num);
}
num = 2;
c1.signalAll();
}finally {
lock.unlock();
}
}
public void print15() throws InterruptedException {
lock.lock();
try{
if(num != 2){
c1.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + num);
}
num = 0;
c1.signalAll();
}finally {
lock.unlock();
}
}
}
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。对于普通同步方法,锁是当前实例对象。对于静态同步方法,锁是当前类的class.对象。对于同步方法块,锁是Synchonized括号里配置的对象