Java 并发编程知识总结【一】

JUC 是什么?

java.util.concurrent 在并发编程中使用的工具类

concurrent:并发

image-20221231111506601

1. 线程基础知识复习

1.1 进程(process)

进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程(生命周期)。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

  • 如:运行中的QQ,运行中的MP3播放器
  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

1.2 线程(thread)

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间—>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

1.3 管程

Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象。

Object o = new Object();

new Thread(() -> {
    
    
    synchronized (o)
    {
    
    

    }
},"t1").start();

Monitor 对象会和 Java 对象一同创建并销毁,它底层是由 C++ 语言来实现的。

在 JVM 第3版中

image-20221231113143834

1.4 用户线程和守护线程

Java 线程分为用户线程和守护线程,线程的 daemon 属性为 true 表示是守护线程,false 表示是用户线程。

守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程

用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作

public static void main(String[] args) {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
                           (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
        while (true) {
    
    

        }
    }, "t1");
    t1.start();
    // 3秒钟后主线程再运行
    try {
    
    
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    System.out.println("----------main线程运行完毕");
}
 运行结果
t1	 开始运行,用户线程
----------main线程运行完毕
此时程序还在运行
public static void main(String[] args) {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
                           (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
        while (true) {
    
    

        }
    }, "t1");
    // 线程的 daemon 属性为 true 表示是守护线程,false 表示是用户线程
    t1.setDaemon(true);
    t1.start();
    // 3秒钟后主线程再运行
    try {
    
    
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    System.out.println("----------main线程运行完毕");
}
 运行结果
t1	 开始运行,守护线程
----------main线程运行完毕
此时程序不再运行

结论(重点):

当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出;如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。

设置守护线程,需要在start()方法之前进行。

1.5 线程状态

public enum State {
    
    
    New,//新建
    Runnable,//就绪
    BLOCKED,//阻塞
    WAITING,//不见不散
    TIMED_WAITING,//过时不候
    TERMINATED//终结
}

线程的生命周期:

  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

1.6 sleep/wait的区别

一句话就是都是将当前线程暂停,但是 wait 放开手去睡,放开手里的锁,sleep 握紧手去睡,醒了手里还有锁。

  • sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)
  • wait方法
    • 是Object 的方法
    • 在当前线程中调用方法: 对象名.wait()
    • 使当前线程进入等待(某对象)状态,直到另一线程对该对象发出notify (或notifyAll)为止
    • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
    • 调用此方法后,当前线程将释放锁 ,然后进入等待
    • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行

1.7 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。(一个CPU(采用时间片)同时执行多个任务—秒杀、多个人做同一件事)
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。(多个CPU同时执行多个任务—多个人同时做不同的事)

2. JUC 的辅助类

2.1 CountDownLatch 减少次数

案例:6个同学陆续离开教室后值班同学才可以关门。main主线程必须要等前面6个线程完成全部工作后,自己才能开干

for (int i = 1; i <= 6; i++) {
    
    
    new Thread(() -> SmallTool.printTimeAndThread("离开教室"), String.valueOf(i)).start();
}
SmallTool.printTimeAndThread("班长关门走人");
// 结果
1672477117075	|	24	|	1	|	离开教室
1672477117075	|	29	|	6	|	离开教室
1672477117075	|	25	|	2	|	离开教室
1672477117075	|	28	|	5	|	离开教室
1672477117075	|	1	|	main	|	班长关门走人
1672477117075	|	26	|	3	|	离开教室
1672477117075	|	27	|	4	|	离开教室

从上述代码中我们发现问题所在,开启的线程还未执行关闭,主线程就已经关闭了。

这个时候可以使用我们的 CountWownLatch 来进行解决。

是什么:允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助(就是减少到0时,开始执行某个任务)

CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。

其它线程调用 countDown 方法会将计数器减1(调用 countDown 方法的线程不会阻塞),

当计数器的值变为0时,因 await 方法阻塞的线程会被唤醒,继续执行。

改进代码:

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch countDownLatch = new CountDownLatch(6);
    for (int i = 1; i <= 6; i++) {
    
    
        new Thread(() -> {
    
    
            SmallTool.printTimeAndThread("离开教室");
            // 离开一个减少一个
            countDownLatch.countDown();
        }, String.valueOf(i)).start();
    }
    // 主线程进行等待 阻塞
    countDownLatch.await();
    SmallTool.printTimeAndThread("班长关门走人");
}
// 结果
1672477600285	|	25	|	2	|	离开教室
1672477600285	|	26	|	3	|	离开教室
1672477600285	|	27	|	4	|	离开教室
1672477600285	|	28	|	5	|	离开教室
1672477600285	|	29	|	6	|	离开教室
1672477600285	|	24	|	1	|	离开教室
1672477600285	|	1	|	main	|	班长关门走人

2.2 CyclicBarrier 循环栅栏

是什么:允许一组线程全部等待彼此达到共同屏障点的同步辅助(就是增加到一个值时候,开始执行某个任务)

原理:CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrierawait() 方法

public static void main(String[] args) {
    
    
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> SmallTool.printTimeAndThread("*****召唤神龙"));

    for (int i = 1; i <= 7; i++) {
    
    
        final int tempInt = i;
        new Thread(() -> {
    
    
            SmallTool.printTimeAndThread("收集到" + tempInt + "星龙珠");
            try {
    
    
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
    
    
                throw new RuntimeException(e);
            }
        }, String.valueOf(i)).start();
    }
}
// 结果
1672478173433	|	30	|	7	|	收集到7星龙珠
1672478173433	|	25	|	2	|	收集到2星龙珠
1672478173433	|	29	|	6	|	收集到6星龙珠
1672478173433	|	28	|	5	|	收集到5星龙珠
1672478173433	|	27	|	4	|	收集到4星龙珠
1672478173433	|	26	|	3	|	收集到3星龙珠
1672478173433	|	24	|	1	|	收集到1星龙珠
1672478173433	|	27	|	4	|	*****召唤神龙

2.3 Semaphore 信号灯

是什么:一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个 acquire() 都会阻塞,直到许可证可用,然后才能使用它。 每个 release() 添加许可证,潜在地释放阻塞获取方法。 但是,没有使用实际的许可证对象; Semaphore 只保留可用数量的计数,并相应地执行。(就是当前的资源不够时,需要轮着获取,只能等前一个释放后,后一个才能获取)

原理:在信号量上我们定义两种操作:

  • acquire(获取)当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
  • release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制

更多文章在我的语雀平台:https://www.yuque.com/ambition-bcpii/muziteng

猜你喜欢

转载自blog.csdn.net/m0_52781902/article/details/128513151