• JUC并发编程系列详解篇一(基础)


    同步和异步

    同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。如图所示:
    在这里插入图片描述
    图中显示了同步方法调用和异步方法调用的区别。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

    举个例子,假如你要去商城买个东西,当你到达商城挑选完东西后,你需要去找售货员付款,还要将东西带回家,那么这个过程就是同步调用,但是假如你需要的那个东西在网上有售卖,那你可以在网上进行购买,当你完成付款后,这个网购的过程其实已经是结束了,那么剩下的就是快递小哥将你购买的东西送给你,这就是异步调用。

    并发和并行

    并发

    并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。如图所示:
    在这里插入图片描述
    特点

    1、程序与计算不再一一对应,一个程序副本可以有多个计算。
    2、并发程序之间有相互制约关系,直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争某一资源,如处理机、缓冲等。
    3、并发程序在执行中是走走停停,断续推进的。

    程序并发的过程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。

    并行

    在操作系统中,若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,无论从微观还是宏观,程序都是一起执行的。是顺序执行)。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。如图所示:
    在这里插入图片描述
    当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。

    小结

    并发说的是在一个时间段内,多件事情在这个时间段内交替执行。并行说的是多件事情在同一个时刻同时发生。实际上,如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换多任务)。真实的并行也只可能出现在拥有多个CPU的系统中(比如多核CPU)。

    临界区

    临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。

    比如,一个办公室里有一台打印机,打印机一次只能执行一个任务。如果小王和小明同时需要打印文件,很明显,如果小王先发了打印任务,打印机就开始打印小王的文件,小明的任务就只能等待小王打印结束后才能打印,这里的打印机就是一个临界区的例子。

    在并行程序中,临界区资源是保护的对象,如果意外出现打印机同时执行两个任务的情况,那么最有可能的结果就是打印出来的文件是损坏的文件,它既不是小王想要的,也不是小明想要的。

    阻塞(Blocking)和非阻塞(Non-Blocking)

    阻塞和非阻塞通常用来形容很多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他线程阻塞在这个临界区上的线程都不能工作。

    非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。

    死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

    死锁、饥饿和活锁都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程就不再活跃,也就是说它可能很难再继续往下执行了。

    死锁

    两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。
    例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。
    例如:

    /**
     * @author: 随风飘的云
     * @describe:死锁例子
     * @date 2022/09/10 0:57
     */
    public class DeadLockDemo {
        private static String A = "A";
        private static String B = "B";
        public static void main(String[] args) {
            new DeadLockDemo().deadLock();
        }
        private void deadLock() {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (A) {
                        try {
                            Thread.currentThread().sleep(2000);
                        }catch (InterruptedException e) {
                            e.printStackTrace();
                        }synchronized (B) {
                            System.out.println("1");
                        }
                    }
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (B) {
                        synchronized (A) {
                            System.out.println("2");
                        }
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    使用jstack查看线程执行可得如下图:(使用方法,首先运行死锁程序,然后再cmd下输入jps获取死锁程序的pid,接着使用jstack pid可查看程序的运行)
    在这里插入图片描述

    饥饿

    饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。在自然界中,母鸟给雏鸟喂食很容易出现这种情况:由于雏鸟很多,食物有限,雏鸟之间的事务竞争可能非常厉害,经常抢不到事务的雏鸟有可能被饿死。线程的饥饿非常类似这种情况。此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。于死锁想必,饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。
    实例:

    import java.util.concurrent.*;
    
    public class ExecutorLock {
        private static ExecutorService single = Executors.newSingleThreadExecutor();
    
        public static class AnotherCallable implements Callable<String>{
            @Override
            public String call() throws Exception {
                System.out.println("随风飘的云");
                return "annother success";
            }
        }
    
        public static class MyCallable implements Callable<String>{
            @Override
            public String call() throws Exception {
                System.out.println("随风飘的云");
                Future<String> submit = single.submit(new AnotherCallable());
                return "success" + submit.get();
            }
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            MyCallable callable = new MyCallable();
            Future<String> future = single.submit(callable);
            System.out.println(future.get());
            System.out.println("over");
            single.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    结果:
    在这里插入图片描述
    使用jstack查询可得:

    "pool-1-thread-1" #12 prio=5 os_prio=0 tid=0x000001e144b0a000 nid=0x53ec waiting on condition [0x00000069768ff000]
       java.lang.Thread.State: WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            - parking to wait for  <0x000000076bc18780> (a java.util.concurrent.FutureTask)
            at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
            at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
            at java.util.concurrent.FutureTask.get(FutureTask.java:191)
            at ThreadTest.ExecutorLock$MyCallable.call(ExecutorLock.java:26)
            at ThreadTest.ExecutorLock$MyCallable.call(ExecutorLock.java:21)
            at java.util.concurrent.FutureTask.run(FutureTask.java:266)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
            at java.lang.Thread.run(Thread.java:748)
    "main" #1 prio=5 os_prio=0 tid=0x000001e127f7a800 nid=0x4c1c waiting on condition [0x00000069753ff000]
       java.lang.Thread.State: WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            - parking to wait for  <0x000000076b9c1560> (a java.util.concurrent.FutureTask)
            at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
            at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
            at java.util.concurrent.FutureTask.get(FutureTask.java:191)
            at ThreadTest.ExecutorLock.main(ExecutorLock.java:33)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    代码分析: 堆栈信息结合图中的代码,可以看出主线程在29行处于等待中,线程池中的工作线程在22行处于等待中,等待获取结果。由于线程池是一个线程,AnotherCallable得不到执行,而被饿死,最终导致了程序死锁的现象。在这里插入图片描述

  • 相关阅读:
    【计算机网络】HTTP协议
    kubeKey部署k8s与kubeSphere
    OA系统都能为企业带来什么
    Redis的安装及启动
    【网络】Burpsuite学习笔记
    操作系统的结构设计怎么搞?带你理解理解
    Arcgis横向图例设置
    vue中插槽slot
    java集合概述:ArrayList[67]
    【C++】STL:map/set,使用红黑树模拟实现
  • 原文地址:https://blog.csdn.net/m0_46198325/article/details/126459396