彻底理解synchronized

news2024/10/5 14:27:52

彻底理解synchronized

1. synchronized简介

在学习知识前,我们先来看一个现象:

public class SynchronizedDemo implements Runnable {
    private static int count = 0;
​
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new SynchronizedDemo());
            thread.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + count);
    }
​
    @Override
    public void run() {
        for (int i = 0; i < 1000000; i++)
            count++;
    }
}

开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。

我们已经已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的内存可见性问题,以及重排序导致的问题,进一步知道了happens-before规则。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。

2. synchronized实现原理

在java代码中使用synchronized可是使用在代码块和方法中,根据synchronized用的位置可以有如表3.1这些使用场景:

使用位置作用范围被锁的对象示例代码
方法实例方法类的实例对象public synchronized void method() { .......}
静态方法类对象public static synchronized void method1() { .......}
代码块实例对象类的实例对象synchronized (this) { .......}
class对象类对象synchronized (SynchronizedScopeDemo.class) { .......}
任意实例对象object实例对象objectfinal String lock = "";synchronized (lock) { .......}

如表所示synchronized可以用在方法上也可以使用在代码块中,方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中根据锁的目标对象

也可以分为三种,具体的可以看表数据。这里的需要注意的是如果锁的是类对象的话,尽管new多个实例对象,依然会被锁住。synchronized的使用起来很简单,那么背后的原理以及实现机制是怎样的呢?

1 对象锁(monitor)机制

现在来进一步分析synchronized的具体底层实现,有如下一个简单的示例代码:

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
            System.out.println("hello synchronized!");
        }
    }
}

上述代码通过synchronized“锁住”当前类对象来进行同步,将java代码进行编译之后通过javap -v SynchronizedDemo .class来查看对应的main方法字节码如下:

public static void main(java.lang.String[]);
​
•    descriptor: ([Ljava/lang/String;)V
​
•    flags: ACC_PUBLIC, ACC_STATIC
​
•    Code:
​
•      stack=2, locals=3, args_size=1
​
•         0: ldc           #2                  // class com/codercc/chapter3/SynchronizedDemo
​
•         2: dup
​
•         3: astore_1
​
•         4: **monitorenter**
​
•         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
​
•         8: ldc           #4                  // String hello synchronized!
​
•        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
​
•        13: aload_1
​
•        14: monitorexit
​
•        15: **goto**          23
​
•        18: astore_2
​
•        19: aload_1
​
•        20: **monitorexit**
​
•        21: aload_2
​
•        22: **athrow**
​
•        23: **return

重要的字节码已经在原字节码文件中进行了标注,再进入到synchronized同步块中,需要通过monitorenter指令获取到对象的monitor(也通常称之为对象锁)后才能往下进行执行,在处理完对应的方法内部逻辑之后通过monitorexit指令来释放所持有的monitor,以供其他并发实体进行获取。代码后续执行到第15行goto语句进而继续到第23行return指令,方法成功执行退出。另外当方法异常的情况下,如果monitor不进行释放,对其他阻塞对待的并发实体来说就一直没有机会获取到了,系统会形成死锁状态很显然这样是不合理。

因此针对异常的情况,会执行到第20行指令通过monitorexit释放monitor锁,进一步通过第22行字节码athrow抛出对应的异常。从字节码指令分析也可以看出在使用synchronized是具备隐式加锁和释放锁的操作便利性的,并且针对异常情况也做了释放锁的处理。

每个对象都存在一个与之关联的monitor,线程对monitor持有的方式以及持有时机决定了synchronized的锁状态以及synchronized的状态升级方式。monitor是通过C++中ObjectMonitor实现,代码可以通过openjdk hotspot进行下载openjdk中hotspot版本的源码,具体文件路径在src\share\vm\runtime\objectMonitor.hpp,具体源码为:

  // initialize the monitor, exception the semaphore, all other fields
​
  // are simple integers or pointers
​
  ObjectMonitor() {
​
•    _header       = NULL;
​
•    _count        = 0;
​
•    _waiters      = 0,
​
•    _recursions   = 0;
​
•    _object       = NULL;
​
•    _owner        = NULL;
​
•    **_WaitSet**      = NULL;
​
•    _WaitSetLock  = 0 ;
​
•    _Responsible  = NULL ;
​
•    _succ         = NULL ;
​
•    _cxq          = NULL ;
​
•    FreeNext      = NULL ;
​
•    **_EntryList**    = NULL ;
​
•    _SpinFreq     = 0 ;
​
•    _SpinClock    = 0 ;
​
•    OwnerIsThread = 0 ;
​
•    _previous_owner_tid = 0;
​
  }

从ObjectMonitor的结构中可以看出主要维护WaitSet以及EntryList两个队列来保存ObjectWaiter 对象,当每个阻塞等待获取锁的线程都会被封装成ObjectWaiter对象来进行入队,与此同时如果获取到锁资源的话就会出队操作。另外_owner则指向当前持有ObjectMonitor对象的线程。等待获取锁以及获取锁出队的示意图如下图所示:

 

当多个线程进行获取锁的时候,首先都会进行_EntryList队列,其中一个线程获取到对象的monitor后,对monitor而言就会将_owner变量设置为当前线程,并且monitor维护的计数器就会加1。如果当前线程执行完逻辑并退出后,monitor中_owner变量就会清空并且计数器减1,这样就能让其他线程能够竞争到monitor。另外,如果调用了wait()方法后,当前线程就会进入到_WaitSet中等待被唤醒,如果被唤醒并且执行退出后,也会对状态量进行重置,也便于其他线程能够获取到monitor。

从线程状态变化的角度来看,如果要想进入到同步块或者执行同步方法,都需要先获取到对象的monitor,如果获取不到则会变更为BLOCKED状态,具体过程如下图所示:

 

从上图可以看出任意线程对Object的访问,首先要获得Object的monitor,如果获取失败,该线程就会进入到同步队列中,线程状态变为BLOCKED。当monitor持有者释放后,在同步队列中的线程才会有机会重新获取monitor,才能继续执行。

2 synchronized的happens-before关系

在第2章中分析过happens-before规则,其中有一条就是监视器锁规则:对同一个监视器的解锁happens-before于对该监视器的加锁。为了进一步了解synchronized的并发语义,通过示例代码分析这条happens-before规则,示例代码如下:

public class MonitorDemo {
    private int a = 0;
•
    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3
•
    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

在并发时,第5步操作中读取到的变量a的值是多少呢?这就需要通过happens-before规则来进行分析,示例代码的happens-before关系如下图所示:

 

上图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,通过监视器锁规则可以推导出线程A释放锁happens-before线程B加锁,即红色线表示。蓝色的线则是通过传递性规则进一步推导的happens-before关系。最终得到的结论就是操作2 happens-before 5,通过这个关系可以得出什么?

根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见。那么在该示例代码中,线程A先对共享变量A进行加1,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。

3 锁获取和锁释放的内存语义

在第2章中总结果JMM核心为两个部分:happens-before规则以及内存抽象模型。在分析完synchronized的happens-before关系后还是不太完整的,接下来看看基于java内存抽象模型的synchronized的内存语义,具体过程如下图所示:

针对线程A的操作而言,从上图可以看出线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到线程本地内存。然后基于该值进行数据操作后变量a变为1,然后会将值写入到主内存中。

 

对线程B而言执行流程如上图所示。线程B获取锁的时候会强制从主内存中共享变量a的值,而此时变量a已经是最新值了。接下来线程B会将该值拷贝到工作内存中进行操作,同样的执行完操作后也会重新写入到主内存中。

从横向来看,线程A和线程线程都是基于主内存中的共享变量互相感知到对方的数据操作,并基于共享变量来完成并发实体中的协同工作,整个过程就好像线程A给线程B发送了一个数据变更的“通知”,这种通信机制就是基于共享内存的并发模型结构导致。

通过上面的讨论对synchronized应该有一定了解,它最大的特征就是在同一时刻只有一个线程能够获得对象monitor,从而确保当前线程能够执行到相应的同步逻辑,对线程之间而言表现为互斥性(排它性) 。自然而然这种同步方式会有效率相对低下的弊端,既然同步流程不能发生改变,那么能不能让每次获取锁的速度更快或者降低阻塞等待的概率呢?也就是通过局部的优化来提升系统整体的并发同步的效率。比如去收银台付款的场景,之前的方式是大家都去排队,然后去纸币付款收银员找零。甚至有的时候付款的时候还需要在包里拿出钱包拿出钱,这个过程是比较耗时的。针对付款的流程,就可以通过线上化的手段来进行优化,在现在只需要通过支付宝扫描二维码就可以完成付款了,也省去了收银员找零的时间。尽管整个付款场景还是需要排队,但是因为付款(类似于获取锁释放锁)这个环节的优化导致耗时大大缩短,对收银台(系统整体并发效率)而言操作效率就极大的带来提升。如此类比,如果能对锁操作过程进行优化的话,也会对并发效率带来极大的提升。

那么,针对synchronized的优化是怎样做的呢?在进一步分析之前,需要先了解这两个概念:1. CAS操作;2.Java对象头。

3.1 CAS操作

3.1.1 什么是CAS?

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

3.1.2 CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

Synchronized VS CAS

元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

3.1.3 CAS的应用场景

在J.U.C包中利用CAS实现类有很多,可以说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现,关于这些具体的实现场景在之后会详细聊聊,现在有个印象就好了(微笑脸)。

3.1.4 CAS的问题

1. ABA问题 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

2. 自旋时间过长

使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

3. 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

3.2 Java对象头

在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为(注:java对象头以及下面的锁状态变化摘自《java并发编程的艺术》一书,该书我认为写的足够好,就没在自己组织语言班门弄斧了):

 

如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图:

 

3.2 偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

 

如图,偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。

 

如何关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

3.3 轻量级锁

加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

3.5 各种锁的比较

 

4. 一个例子

经过上面的理解,我们现在应该知道了该怎样解决了。更正后的代码为:

public class SynchronizedDemo implements Runnable {
    private static int count = 0;
​
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new SynchronizedDemo());
            thread.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + count);
    }
​
    @Override
    public void run() {
        synchronized (SynchronizedDemo.class) {
            for (int i = 0; i < 1000000; i++)
                count++;
        }
    }
}

开启十个线程,每个线程在原值上累加1000000次,最终正确的结果为10X1000000=10000000,这里能够计算出正确的结果是因为在做累加操作时使用了同步代码块,这样就能保证每个线程所获得共享变量的值都是当前最新的值,如果不使用同步的话,就可能会出现A线程累加后,而B线程做累加操作有可能是使用原来的就值,即“脏值”。这样,就导致最终的计算结果不是正确的。而使用Syncnized就可能保证内存可见性,保证每个线程都是操作的最新值。这里只是一个示例性的demo,聪明的你,还有其他办法吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/81783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSC7136D

CSC7136D是一款高效率低待机功耗原边反馈小功率电源AC/DC驱动电路&#xff0c;无需光耦、TL431及相关器件。CSC7136D采用开关频率调制和初级电流峰值振幅&#xff08;FM和AM&#xff09;多模式工作技术&#xff0c;保证了全负载和线性范围内的较高的转换效率。恒压模式下的线缆…

Redis -- 缓存穿透和雪崩

文章目录一、缓存穿透1.1 概念1.2 解决方案1.3 布隆过滤器的工作原理二、缓存击穿2.1 概念2.2 解决方案三、缓存雪崩3.1 概念3.2 解决方案用户的数据一般是存储于数据库&#xff0c;数据库的数据是落在磁盘上的&#xff0c;磁盘的读写速度可以说是计算机里最慢的硬件了。当用户…

新手小白入门必看教程,js中的迭代器和生成器到底是什么

内容预警&#xff0c;低级内容&#xff0c;菜鸟必看&#xff0c;大佬请绕道 在学习es6的新语法的时候&#xff0c;我相信不管你买什么书&#xff0c;里面一定有很长很长的章节在介绍【迭代器和生成器】&#xff0c;新手对于这两个名词真的非常陌生&#xff0c;即便看过所有的章…

Grafana监控大屏配置参数介绍(一)

Grafana 系列文章&#xff0c;版本&#xff1a;OOS v9.3.1 Grafana 的介绍和安装 在上篇文章中&#xff0c;我们已经安装了Grafana&#xff0c;并且看到了它的初步面貌。在这篇文章&#xff0c;我们以一个简单的大屏为例&#xff0c;来了解Grafana的大屏配置参数。 创建第一个…

TeXstudio配置

目录&#xff1a;TeXstudio配置一、前言二、下载和安装 TeXstudio三、下载和安装 TeXlive四、配置 TeXstudio五、测试一、前言 众所周知&#xff0c;TeX Live 自带的编辑器 TeXworks 是越更新 bug 越多&#xff0c;作为新手上路的工具刚刚好&#xff0c;但是对于有比较强的使用…

【圣诞节特辑】会呼吸的玫瑰爱心代码 -李峋爱心续 动画演示思路 代码开源 一起浪漫吧

源码下载地址&#xff1a;会呼吸、带有玫瑰花的爱心告白程序-Java文档类资源-CSDN下载 粉丝可直接私信我领取。 前言 之前有部电视剧《点燃我温暖你》没火&#xff0c;但是其中李峋的爱心代码却在程序圈超级火&#xff0c;这圣诞节快到了了&#xff0c;给大家来一波爱心代码…

非零基础自学Golang 第1章 走进Go 1.2 Go语言官方文档 1.3 学好Go 的建议

非零基础自学Golang 文章目录非零基础自学Golang第1章 走进Go1.2 Go语言官方文档1.3 学好Go 的建议1.3.1 了解语言特性及自身需求1.3.2 动手写代码1.3.3 参与项目1.3.4 阅读英文文档第1章 走进Go 1.2 Go语言官方文档 Go语言官方文档网址&#xff1a;https://go.dev/doc/。 官…

系统迁移的重点步骤及注意事项

在实际项目中会有迁移合并正在使用中的环境的需求&#xff0c;本文将以把B环境迁移合并到A环境为例&#xff0c;介绍如何迁移合并两个环境。 迁移前准备工作如下&#xff1a; 防止迁移过程中出现不可控的错误&#xff0c;迁移之前请备份目标环境default数据源库确保两个环境的…

GCSE英语语言考试-虚构小说考试问题和答案案例​

Analysing Fiction - Question and extract 虚构小说分析--问题和摘要 The extracts your exam questions are based on might be longer than the one here but you should still be able to write a close analysis. First of all, have a read, and see what ideas you can …

[附源码]Python计算机毕业设计大学生运动员健康管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

27-Vue之ECharts-通用配置

ECharts-通用配置前言标题提示框工具按钮图例完整代码前言 本篇来学习下ECharts图表中的通用配置 标题 title&#xff1a;标题 title: {text: "分数", // 标题文字textStyle: {color: red // 文字颜色},borderWidth: 2, // 标题边框宽度borderColor: blue, // 标…

[ Linux ] 可重入函数,volatile 关键字,SIGCHLD信号

目录 1.可重入函数 2.volatile 2.1从信号角度理解volatile的作用 2.2volatile的作用 3.SIGCHLD信号 3.1SIGCHLD信号的验证 1.可重入函数 在数据结构初阶时我们学习过链表&#xff0c;其中当然也学习过链表头插。在此我们复习一下链表头插&#xff0c;我们使用画图来演示…

Koa初体验

前面我们已经学习了express&#xff0c;另外一个非常流行的Node Web服务器框架就是Koa。 Koa官方的介绍&#xff1a; koa&#xff1a;next generation web framework for node.js&#xff1b; koa&#xff1a;node.js的下一代web框架&#xff1b; 事实上&#xff0c;koa是ex…

【云计算与大数据技术】Google、亚马逊、IBM、阿里云等云计算应用平台介绍讲解(超详细)

云应用跟云计算最大的不同在于&#xff0c;云计算作为一种宏观技术发展概念而存在&#xff0c;而云应用则是直接面对客户解决实际问题的产品。 “云应用”的工作原理是把传统软件“本地安装、本地运算”的使用方式变为”即取即用”的服务&#xff0c;通过互联网或局域网连接并操…

VUE学习(五)VUE项目发布启动

在项目目录下运行 npm run build 2.下载nginx nginx: download 双击nginx.exe运行 3.将打包好的vue项目下dist目录所有文件拷贝 4.粘贴至nginx目录html下 5.此时在浏览器中输入localhost 即可看到发布的网站了

【云计算与大数据技术】大数据概念和发展背景讲解(图文解释 超详细)

一、什么是大数据 大数据是一个不断发展的概念&#xff0c;可以指任何体量或负载下那个超出常规数据处理方法和处理能力的数据&#xff0c;数据本身可以是结构化&#xff0c;半结构化甚至是非结构化的&#xff0c;随着物联网技术与可穿戴设备的飞速发展&#xff0c;数据规模变…

首篇!BEV-Locator:多目端到端视觉语义定位网络(清华轻舟智航)

点击下方卡片&#xff0c;关注“自动驾驶之心”公众号ADAS巨卷干货&#xff0c;即可获取点击进入→自动驾驶之心【SLAM】技术交流群后台回复【BEV定位】获取本文论文&#xff01;摘要准确的定位能力是自动驾驶的基础。传统的视觉定位框架通过几何模型来解决语义地图匹配问题&am…

【C语言经典面试题】这样的char * 定义怎么回事

作者简介 *架构师李肯&#xff08;全网同名&#xff09;**&#xff0c;一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验&#xff0c;深耕IoT领域多年&#xff0c;熟知IoT领域的业务发展&#xff0c;深度掌握IoT领域的相关技术栈&#xff0c;包括但不限于主流…

Cookie | Cookie的理论基础、Cookie中常用的方法

目录 一&#xff1a;Cookie的理论基础 二&#xff1a;Cookie中常用的方法 一&#xff1a;Cookie的理论基础 &#xff08;1&#xff09;cookie怎么生成&#xff1f; ①session的实现原理中&#xff0c;每一个session对象都会关联一个sessionid&#xff0c;例如&#xff1a;JS…

中创软件递交上会稿:年营收不到2亿 税收优惠占利润比高

雷递网 雷建平 12月10日山东中创软件商用中间件股份有限公司&#xff08;简称&#xff1a;“中创软件”&#xff09;日前递交上会稿&#xff0c;准备在科创板上市。中创软件计划募资6亿元&#xff0c;其中&#xff0c;2.3亿元用于应用基础设施及中间件研发项目&#xff0c;1.3亿…