[Java并发编程] 一些并发编程的概念

此文章为笔记,为阅读其他文章的感受、补充、记录、练习、汇总,非原创,感谢每个知识分享者。
原文

同步(Synchronous)和异步(Asynchronous)

同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。

如图:
在这里插入图片描述
上图中显示了同步方法调用和异步方法调用的区别。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

打个比方,比如购物,如果你去商场买空调,当你到了商场看重了一款空调,你就向售货员下单。售货员去仓库帮你调配物品。这天你热的是在不行了,就催着商家赶紧给你送货,于是你就在商店里面候着他们,直到商家把你和空调一起送回家,一次愉快的购物就结束了。这就是同步调用。

不过,如果我们赶时髦,就坐在家里打开电脑,在电脑上订购了一台空调。当你完成网上支付的时候,对你来说购物过程已经结束了。虽然空调还没有送到家,但是你的任务已经完成了。商家接到你的订单后,就会加紧安平送货,当然这一切已经跟你无关了。你已经支付完成,想干什么就能去干什么,出去溜几圈都不成问题,等送货上门的时候,接到商家的电话,回家一趟签收就完事了。这就是异步调用。

并发(Concurrency)和并行(Parallelism)

并发和并行是两个非常容易被混淆的概念。他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”,下图很好地诠释了这点。
在这里插入图片描述
大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡,是并行。

从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程只是交替的,一会执行任务A,一会执行任务B,系统会不停地在两者之间切换。但对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间并行执行的错觉。

并发说的是在一个时间段内,多件事情在这个时间段内交替执行。

并行说的是多件事情在同一个时刻同事发生。

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

临界资源

概念:一次仅允许一个进程使用的共享资源。

临界区-解释1

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

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

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

临界区-解释2

  • 概念:每个进程中访问临界资源的那段程序称之为临界区。
  • 临界区不是内核对象,而是系统提供的一种数据结构,程序中可以声明一个该类型的变量,之后用它来实现对资源的互斥访问。当欲访问某一临界资源时,先将该临界区加锁(若临界区不空闲则等待),用完该资源后,将临界区释放。
  • 补充(待定):分类:临界区也是代码的称呼,所以一个进程可能有多个临界区,分别用来访问不同的临界资源。
    内核程序临界资源:系统时钟
    普通临界资源:普通I/O设备,如打印机(进程访问这些资源的时候,很慢,会自动阻塞,等待资源使用完成)

临界区-解释3

在计算机科学中,临界区(Critical Section)是指一段代码或一段程序片段,需要在同一时间只能被一个线程执行。临界区用于保护共享资源,以避免多个线程同时访问或修改造成的数据竞争和不确定性。

当多个线程并发执行时,如果它们都能够访问和修改共享资源,就会导致一些问题,例如:

竞态条件(Race Condition):多个线程同时读取和写入共享资源,导致结果的正确性依赖于执行的时序,从而引发不确定的结果。
数据不一致性:多个线程并发修改共享资源,使得资源的值在不同线程之间不一致,破坏了数据的一致性和正确性。
为了解决这些问题,需要使用临界区来确保在任意时刻只有一个线程可以访问共享资源。一旦一个线程进入了临界区,其他线程就必须等待,直到当前线程退出临界区后才能进入。

临界区的实现通常使用同步机制,例如互斥锁(Mutex)、信号量(Semaphore)或条件变量(Condition Variable)。这些机制可以确保在任意时刻只有一个线程能够获得对临界区的访问权限,从而保证了共享资源的安全性和一致性。

需要注意的是,合理设计和使用临界区是确保并发程序正确性的关键,过多的临界区可能导致性能问题和死锁,而过少的临界区可能导致数据不一致性。因此,在编写并发程序时,需要仔细考虑和设计临界区的范围和位置。

进程进入临界区的调度原则

  • 如果有若干进程请求进入空闲的临界区(空闲即0进程访问),一次仅允许一个进程进入。
  • 任何时候,处于临界区内的进程不可多于一个(0 或 1),若已有进程进入自己的临界区,则其它想进入自己临界区的进程必须等待。
  • 进行临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
  • 如果其它进程不能进入自己的临界区,则应让出 CPU,避免进程出现 “忙等” 现象。

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

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

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

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

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

死锁应该是最糟糕的一种情况了(当然,其他几种情况也好不到哪里去),如下图显示了一个死锁的发生:
在这里插入图片描述
A、B、C、D四辆小车都在这种情况下都无法继续行驶了。他们彼此之间相互占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状况将永远持续下去,谁都不可能通过,死锁是一个很严重的并且应该避免和实时小心的问题,后面的文章中会做更详细的讨论。

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

活锁是一种非常有趣的情况。不知道大家是否遇到过这么一种场景,当你要做电梯下楼时,电梯到了,门开了,这是你正准备出去。但很不巧的是,门外一个人当着你的去路,他想进来。于是,你很礼貌地靠左走,礼让对方。同时,对方也非常礼貌的靠右走,希望礼让你。结果,你们俩就又撞上了。于是乎,你们都意识到了问题,希望尽快避让对方,你立即向右边走,同时,他立即向左边走。结果,又撞上了!不过介于人类的智慧,我相信这个动作重复两三次后,你应该可以顺利解决这个问题。因为这个时候,大家都会本能地对视,进行交流,保证这种情况不再发生。但如果这种情况发生在两个线程之间可能就不那么幸运了。如果线程智力不够。且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么久会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

死锁的例子

public class DeadLock {
    public static void main(String[] args) {
        Obj1 obj1 = new Obj1();
        Obj2 obj2 = new Obj2();
        Thread thread1 = new Thread(new SynAddRunalbe(obj1, obj2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(obj1, obj2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }
    /**
     * 线程死锁等待演示
     */
    public static class SynAddRunalbe implements Runnable {
        Obj1 obj1;
        Obj2 obj2;
        int a, b;
        boolean flag;
        public SynAddRunalbe(Obj1 obj1, Obj2 obj2, int a, int b, boolean flag) {
            this.obj1 = obj1;
            this.obj2 = obj2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (obj1) {
                        Thread.sleep(100);
                        synchronized (obj2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (obj2) {
                        Thread.sleep(100);
                        synchronized (obj1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static class Obj1 {
    }
    public static class Obj2 {
    }
}

运行上面代码,可以通过jstack查看到死锁信息:

使用jps查看进程(cmd中即可)

在这里插入图片描述

使用jstack pid查看死锁堆栈情况

jstack 13424
Java stack information for the threads listed above:
===================================================
"thread2":
        at com.xin.demo.threaddemo.lockdemo.DeadLock$SynAddRunalbe.run(DeadLock.java:43)
        - waiting to lock <0x00000000db5b8280> (a com.xin.demo.threaddemo.lockdemo.DeadLock$Obj1)
        - locked <0x00000000db5babe8> (a com.xin.demo.threaddemo.lockdemo.DeadLock$Obj2)
        at java.lang.Thread.run(Thread.java:748)
"thread1":
        at com.xin.demo.threaddemo.lockdemo.DeadLock$SynAddRunalbe.run(DeadLock.java:36)
        - waiting to lock <0x00000000db5babe8> (a com.xin.demo.threaddemo.lockdemo.DeadLock$Obj2)
        - locked <0x00000000db5b8280> (a com.xin.demo.threaddemo.lockdemo.DeadLock$Obj1)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

thread1持有com.jvm.visualvm.Demo4 O b j 1 的锁,等待获取 c o m . j v m . v i s u a l v m . D e m o 4 Obj1的锁,等待获取com.jvm.visualvm.Demo4 Obj1的锁,等待获取com.jvm.visualvm.Demo4Obj2的锁
thread2持有com.jvm.visualvm.Demo4 O b j 2 的锁,等待获取 c o m . j v m . v i s u a l v m . D e m o 4 Obj2的锁,等待获取com.jvm.visualvm.Demo4 Obj2的锁,等待获取com.jvm.visualvm.Demo4Obj1的锁,两个线程相互等待获取对方持有的锁,出现死锁。

饥饿死锁的例子

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

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("in AnotherCallable");
            return "annother success";
        }
    }
    public static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("in MyCallable");
            Future<String> submit = single.submit(new AnotherCallable());
            return "success:" + submit.get();
        }
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable task = new MyCallable();
        Future<String> submit = single.submit(task);
        System.out.println(submit.get());
        System.out.println("over");
        single.shutdown();
    }
}

执行代码,输出:

in MyCallable
使用jstack命令查看线程堆栈信息:

jps
13424 DeadLock
4704
15396 Jps
16744 Launcher
10156 ExecutorLock
11772 RemoteMavenServer36
jstack 10156
2023-07-31 22:50:24
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.271-b09 mixed mode):

"pool-1-thread-1" #12 prio=5 os_prio=0 tid=0x0000019b622db800 nid=0x3b50 waiting on condition [0x00000012b6aff000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000db6f58b0> (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 com.xin.demo.threaddemo.lockdemo.ExecutorLock$MyCallable.call(ExecutorLock.java:23)
        at com.xin.demo.threaddemo.lockdemo.ExecutorLock$MyCallable.call(ExecutorLock.java:18)
        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)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000019b621d2000 nid=0x25f0 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x0000019b6210a000 nid=0x478 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000019b62106800 nid=0x1ef4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000019b62102000 nid=0x774 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000019b62100800 nid=0x40a8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000019b620ef800 nid=0x19a8 runnable [0x00000012b63fe000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x00000000db61dd90> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x00000000db61dd90> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000019b60010000 nid=0x43f0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000019b5ffbd000 nid=0x1a0 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000019b5ff87800 nid=0x2dc in Object.wait() [0x00000012b60ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000db308ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x00000000db308ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000019b5ff7f800 nid=0x2cd8 in Object.wait() [0x00000012b5fff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000db306c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000db306c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" #1 prio=5 os_prio=0 tid=0x0000019b4a6ae800 nid=0x2e8c waiting on condition [0x00000012b55ff000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000db5d2ca8> (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 com.xin.demo.threaddemo.lockdemo.ExecutorLock.main(ExecutorLock.java:29)

"VM Thread" os_prio=2 tid=0x0000019b5ff57800 nid=0x2108 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000019b4a6c8000 nid=0x28ec runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000019b4a6ca000 nid=0x3e14 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000019b4a6cb800 nid=0x1fc8 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000019b4a6cd000 nid=0x3a0c runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000019b4a6cf000 nid=0x2590 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000019b4a6d0000 nid=0x3504 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000019b4a6d3000 nid=0x1d2c runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000019b4a6d3800 nid=0x19b8 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x0000019b6229e000 nid=0x658 waiting on condition

JNI global references: 12

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值