从根本上理解Synchronized的加锁过程

news2024/11/23 9:15:23

伸手摘星,即使一无所获,亦不致满手污泥

请关注公众号:星河之码

作为一个Java开发,对于Synchronized这个关键字并不会陌生,无论是并发编程,还是与面试官对线,Synchronized可以说是必不可少。

在JDK1.6之前,都认为Synchronized是一个非常笨重的锁,就是在之前的《谈谈Java中的锁》中提到的重量级锁。但是在JDK1.6对Synchronized进行优化后,Synchronized的性能已经得到了巨大提升,也算是脱下了重量级锁这一包袱。本文就来看看Synchronized的使用与原理。

JDK1.6后优化点:

锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,synchronized的并发性能已经基本与J U C包提供的Lock持平

一、Synchronized的使用

在Java中,synchronized有:【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式,分别锁住不同的对象。这三种方式,获取不同的锁,锁定共享资源代码段,达到互斥(mutualexclusion)效果,以此保证线程安全。

共享资源代码段又被称之为临界区,锁的作用就是保证临界区互斥,即同一时间临界区的只能有一个线程执行,其他线程阻塞等待,排队等待前一个线程释放锁

1.1 修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

public synchronized void methodA() {
    System.out.println("作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁");
}

1.2 修饰静态方法

给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁

当被static修饰时,表明被修饰的代码块或者变量是整个类的一个静态资源,属于类成员,不属于任何一个实例对象,也就是说不管 new 了多少个对象,都只有一份,

public synchronized static void methodB() {
    System.out.println("给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 **当前 class 的锁**。");
}

如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

1.3 修饰代码块

指定加锁对象,对给定对象/类加锁

  • synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁

    public void methodc() {
        synchronized(this) {
            System.out.println("锁住的是当前对象,对象锁");
        }
    }
    
  • synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

    javapublic void methodd() {
        synchronized(SynchronizedDome.class) {
            System.out.println("给当前类加锁,SynchronizedDome class 的锁");
        }
    }
    

二、Synchronized案例说明

了解了Synchronized的使用方法以后,接下来结合案例的方式,来详细看看Synchronized的加锁,多线程下是怎么执行的,我这里将按照上面三个使用方法来分别使用案例描述

2.1 修饰实例方法案例

  • 案例说明

    两个线程同时对一个共享变量sum进行累加3000,输出其最终结果,我们期望的结果最终应该是6000,接下来看看不加Synchronized修饰和加Synchronized修饰的情况下分别输出什么。

2.1.1 案例

  • 不加Synchronized修饰

    package com.upup.edwin.sync;
    
    public  class SynchronizedDome {
    
        //定义来了一个共享变量
        public static int sum = 0;
    
        // 进行累加3000次
        public void add() {
            for (int i = 0; i < 3000; i++) {
                sum = sum + 1;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedDome dome = new SynchronizedDome();
    
            Thread thread1 = new Thread(() -> dome.add());
            Thread thread2 = new Thread(() -> dome.add());
            thread1.start();
            thread2.start();
            //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
            thread1.join();
            thread2.join();
            System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
        }
    
    }
    
    

    从结果上看,当我们不加synchronized修饰的时候,输出结果并不是我们锁期待的6000,这说明两个线程之间在执行的时候相互干扰了,也就是线程不安全

  • 加Synchronized修饰

    我们对上面的add方法进行改造,在方法上加上synchronized关键字,也就是加上了锁,来看看它的执行结果

    // 进行累加3000次
    public synchronized void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
    

    加上synchronized修饰后,发现输出结果与我们预期的是一致的,说明加上锁,两个线程是排队顺序执行的

2.1.2 案例执行过程

通过以上的两个案例对比,可以发现在synchronized修饰方法的时候,能够让结果正常输出,保证了线程安全,那么它是怎么做到的吗,两个线程的执行过程是怎么样的呢?

在前面我们提到了,当synchronized修饰实例方法的时候获取的是当前对象实例的锁,我们在代码中new出了一个SynchronizedDome对象,因此本质上锁住的是这个对象

SynchronizedDome dome = new SynchronizedDome();

所有线程要执行同步函数都要先获取锁(synchronized里面叫做监视器锁),获取到锁的线程才能执行同步函数,没有获取到的线程只能等待,抢到锁的线程执行完同步函数后会释放锁并通知唤醒其他等待的线程再次获取锁。流程如下

2.2 修饰静态方法案例

修饰静态方法与修饰实例方法基本一致,唯一的区别就是锁的不是当前对象,而是整个Class对象。我们只需要把上述案例中同步函数改成静态的就可以了

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public synchronized static void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
    }

}

可以看到当我们改成静态方法之后,就不需要在main方法中new SynchronizedDome()了,直接调用add即可,这也说明锁的不是当前对象了,

我们知道在Java中静态资源是属于Class的,不属于任何一个实例对象,而每个Class对象在Jvm中都是唯一的,所以我们锁住Class对象后,其他线程无法获取其静态资源了,从而进入等待阶段,本质上,锁住静态资源的执行过程与锁住实例方法的执行过程是一致的,只是锁的对象不一样而已

2.3 修饰代码块案例

静态资源锁Class,实例方法锁对象,还有一种就是锁住一个方法的某一段代码,也就是代码块。比如我们在上述的add 方法中调用了一个print方法

 public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }

如上,print就是睡眠了5秒钟后输出一句话,不涉及到线程安全问题,如果使用synchronized修饰整个Add方法,并且在add中调用 print(),如下

public synchronized static void add() {
    print();
    for (int i = 0; i < 3000; i++) {
        sum = sum + 1;
    }
}

这种方式synchronized就会把add()整个包裹,使整个程序执行时间变长,完整案例如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public synchronized static void add() {
        print();
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
    // 可以异步执行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);

    }

}

以上案例执行结果如下

很明显,两个线程在排队执行Add方法时,连print方法一起等待,但是实际上print是一个线程安全的方法,不需要获取锁,并且print方法还比较耗时,这就拖慢了整个程序的执行总时长,其执行过程如下

这种方式会将线程安全的方法也锁住,导致排队执行代码变多,时间变长,其本质就是synchronized锁住的是整个Add方法,粒度比较大,我们可以对add进行改造一下,让它只锁累计的那一段代码

public static void add() {
    print();
    synchronized(SynchronizedDome.class){
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
}

如上,synchronized只锁了这for循环段代码,print()是可以并行执行的,这样就可以提升整个方法的执行效率,完整代码如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public static void add() {
        print();
        synchronized(SynchronizedDome.class){
            for (int i = 0; i < 3000; i++) {
                sum = sum + 1;
            }
        }
    }
    // 可以异步执行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
    }

}

修改之后,整个方法的执行时间只有5秒多,我们休眠的时间也是5秒,说明两个线程是一起进入的休眠,并不是排队的,其执行过程如下

在上述案例中我使用的是锁住整个class的方法:‘synchronized(SynchronizedDome.class)’,如果要改成锁住对象只需要改成’synchronized(this)'即可。其他执行流程都是一样的,只是获取的锁不一样

三、Synchronized原理剖析

以Hotspot(是Jvm的一种实现)为例,在Jvm中每个Class都有一个对象,对象又由 【对象头 + 实例数据 + 对齐填充(java对象必须是8byte的倍数)】三部分组成,每个对象都有一个对象头,synchronized的锁就是存在对象头中的

3.1 对象头

既然synchronized的锁是存在对象头中的,那就先来了解一下对象头,Hotspot 有两种对象头:

  • 数组类型:如果对象是数组类型,则虚拟机用3字节存储对象头
  • 非数组类型:如果对象是非数组类型,则用2字节存储对象头

一般对象头由两部分组成

  • Mark Word

    存储自身的运行时数据,比如:对象的HashCode,分代年龄和锁标志位信息

    Mark Word存储的信息与对象自身定义无关,所以Mark Word是一个一个非固定的数据结构,Mark Word里存储的数据会在运行期间随着锁标志位的变化而变化。

  • Klass Pointer

    类型指针指向它的类元数据的指针。

Mark Word在不同的虚拟机下的bit位不一样,以下是32位与64位虚拟机的对比图


3.2 Monitor

在了解Monitor之前,先思考一个问题,前面我们说synchronized修饰方法和代码块的时候,加锁业务流程的执行过程是一样的,那么他们内部加锁实现是不是一样的呢?

其实加锁过程肯定是不一样的,不然加锁过程一样,锁一样,加锁业务流程的执行过程是一样,那就没必要分成方法和代码块了

我们可以找到上述案例中的SynchronizedDome类的Class文件,然后再命令行中执行javap -c -s -v -l SynchronizedDome.class就可以看到编译后指令集。可以分别查看synchronized修饰方法和代码块指令集的区别

  • synchronized代码块

    上图中的指令集中有monitorenter、monitorexit两个指令,当synchronized修饰代码块时,JVM就是使用monitorenter和monitorexit两个指令实现同步的,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执到monitorexit的时候则要释放锁

  • synchronized修饰方法

上图中的指令集中有一个ACCSYNCHRONIZED标记,当synchronized修饰方法时,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能,当线程执行有ACCSYNCHRONIZED标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor


3.1什么是Monitor锁

从上面的描述无论是修饰代码块还是修饰方法,都要获取一个Monitor锁,那么什么是Monitor锁呢?

Monitor即监视器,可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁

Monitor锁与对象的关系图:

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM中基于进入和退出Monitor对象,通过成对的MonitorEnter和MonitorExit指令来实现方法同步和代码块同步。

  • MonitorEnter

    插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor锁;

  • MonitorExit

    插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit,释放Monitor锁;

3.2 Monitor锁的工作原理

每一个对象都会有一个monitor锁,Monitor锁的MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,ObjectMonitor中维护了一个锁池(EntryList)和等待池(WaitSet)

ObjectMonitor工作模型图如下:

ObjectMonitor工作模型图大致描述了以下几个步骤

  • 所有新的线程都会进入(①号入口)EntryList中去竞争锁

  • 当有线程通过CAS把monitor的owner字段设置为自己时,说明这个线程获取到了锁,也就是进入图中的(②号入口)owner区域,其他线程进入阻塞状态

    如果当前线程是第一次进入该monitor,将recursions由0设置为1,_owner为当前线程,该线程成功获得锁并返回

    如果当前线程不是第一次进入该monitor,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数

  • 如果获取到锁的线程(owner)执行了wait等方法,就会释放锁,并进入(③号入口)waitset中,于此同时通知waitset中其他线程重新竞争锁,获取到锁(④号入口)进入owner区域

  • 当线程执行完同步代码,会释放锁(由⑤号口出),于此同时通知waitset和EntryList中其他线程重新竞争锁

    释放锁线程执行monitorexitmonitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit

四、Synchronized锁优化

在之前的《谈谈Java中的锁》中介绍了锁优化的一些基本原理, 接下来看看Synchronized的锁优化。锁优化主要包含:锁粗化、锁消除、锁升级三部分。

4.1 锁粗化

同步代码块要求我们将同步代码的范围尽量缩小,这样可以使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁

比如上述案例add的循环中,如果将Synchronized防止for循环里面不是范围更小吗?

for (int i = 0; i < 3000; i++) {
    synchronized(SynchronizedDome.class){
        sum = sum + 1;
    }
}

这样虽然缩小了范围,但是未必缩短了时间,因为在加锁过程中也会消耗资源,如果频繁的加锁释放锁,可能会导致性能损耗

基于此,JVM会对这种情况进行锁粗化,锁粗化就是将【多个连续的加锁、解锁操作连接在一起】,扩展成一个范围更大的锁,避免频繁的加锁解锁操作

J V M在检测到上述for循环再频繁获取同一把锁的到时候,就会将加锁的范围粗化到循环操作的外部,使其只需要获取一次锁就可以,减小加锁释放锁的开销。

4.2 锁消除

Java虚拟机在JIT编译时,会进行逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),通过对运行上下文的扫描,分析synchronized锁对象是不是只被一个线程加锁,不存在其他线程来竞争加锁的情况。这样就可以消除该锁了,提升执行效率。

锁消除的经典案例就是StringBuffer 了,StringBuffer 是线程安全的,其内部的append方法就是通过synchronized加锁的,源码如下

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

当我们调用StringBuffer 的append时,就会加锁,但是当我们使用的对象经过逃逸分析后,认为该对象不会被其他线程共享的时候,就会将append方法的synchronized去掉,编译不加入monitorenter和monitorexit指令。比如下面这个方法

public static String appendStr(String str, int i) {
    StringBuffer sb= new StringBuffer();
    sb.append(str);
    sb.append(i);
    return sb.toString();
}

StringBuffer的append虽然是同步方法。但appendStr中的sb对象没有传递到方法外,不会被其他线程引用,不存在锁竞争的情况,因此可以进行锁消除

五、Synchronized锁升级

在之前的《谈谈Java中的锁》中介绍了锁的类型,其中介绍了无锁、偏向锁 、轻量级锁、 重量级锁等几种锁的实现。而我们常说的锁升级其实就是这几种锁的升级跃迁。锁升级过程:【无锁】—>【偏向锁】—>【轻量级锁】—>【 重量级锁】。

而锁的变化其实就是一个标志位的变化,在前面提到的对象头中Mark World时有提到它存储的就是对象的HashCode,分代年龄和锁标志位信息。因此锁的升级变化,本质上就是Mark World中锁标志位的变化。以上几种锁的标志位信息如下

锁状态存储内容存储内容
无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

注意:

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

5.1 无锁升级为偏向锁

  • 为啥要有偏向锁

    大多数情况下是一个线程多次获得同一个锁,不存在锁竞争的,而竞争锁会增大资源消耗,,为了降低获取锁的代价,才引入的偏向锁


当线程第一次执行到同步代码块的时候,锁对象变成就会偏向锁(通过CAS修改对象头里的锁标志位),其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,可以通过JVM配置成没有延迟

-XX:BiasedLockingStartUpDelay=0

可以通过J V M参数关闭偏向锁,关闭之后程序默认会进入轻量级锁状态

-XX:-UseBiasedLocking=false

无锁升级为偏向锁,其本质是判断对象头的Mark Word中线程ID与当前线程ID是否一致以及偏向锁标识,如果一致直接执行同步代码或方法,具体流程如下

  • 无锁状态,存储内容「是否为偏向锁(0)」,锁标识位01

    • CAS设置当前线程ID到Mark Word存储内容中,并且将是否为偏向锁0 修改为 是否为偏向锁1
    • Mark Word和栈帧中记录获取到偏向的锁的threadID
    • 执行同步代码或方法
  • 偏向锁状态,存储内容「是否为偏向锁(1)、线程ID」,锁标识位01

    • 对比线程ID是否一致,如果一致无需使用CAS来加锁、解锁,直接执行同步代码或方法

      因为偏向锁不会自动释放锁,因此后续线程A再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致

    • 如果不一致,CASMark Word的线程ID设置为当前线程ID,设置成功,执行同步代码或方法

      其他线程,如线程B要竞争锁对象,而偏向锁不会主动释放,因此Mark Word还是存储的线程A的threadID

      此时会检查Mark Word的线程A是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程B)可以竞争将其设置为偏向锁;

    • CAS设置失败,证明存在多线程竞争情况,触发撤销偏向锁,当到达全局安全点,偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后在安全点的位置恢复继续往下执行。

      如果Mark Word的线程A是存活,则线程B的CAS会失败,此时会暂停线程A,撤销偏向锁,升级为轻量级锁,

5.2 偏向锁升级为轻量级锁

轻量级锁又称自旋锁,一般在竞争锁对象的线程比较少,持有锁时间也不长的场景中,由于阻塞线程、唤醒线程需要C P U从用户态转到内核态,时间比较长,如果同步代码块执行的时间比这更时间短,那就本末倒置了,所以这种情况一般不阻塞线程,让其自旋一段时间等待锁其他线程释放锁,通过自旋换取线程在用户态和内核态之间切换的开销

  • 锁竞争

    如果多个线程轮流获取一个锁,但是每次获取锁的时候没有发生阻塞,就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。


当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

升级为轻量级锁有两种情况:

  • 当关闭偏向锁功能时,会由无锁直接升级为轻量级锁
  • 多个线程竞争偏向锁导致偏向锁升级为轻量级锁

这两种情况下偏向锁升级为轻量级锁过程如下

  • 无锁状态:存储内容「是否为偏向锁(0)」,锁标识位01

    关闭偏向锁功能时

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容
    • 锁标识位设置为00
    • 执行同步代码或方法
  • 轻量级锁状态:存储内容「线程栈中锁记录的指针」,锁标识位00

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容,设置成功获取轻量级锁,执行同步块代码或方法
    • 设置失败,证明多线程存在一定竞争,线程自旋上一步的操作,自旋一定次数后还是失败,轻量级锁升级为重量级锁
    • Mark Word存储内容替换成重量级锁指针,锁标记位10

5.3 轻量级锁升级为重量级锁

轻量级锁在自旋一定次数之后还没获取到锁,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,成本非常高,等待锁的线程都会进入阻塞状态,防止CPU空转

计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改

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

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

相关文章

顶刊实证复现:金融投资行为与企业技术创新 -动机分析与经验证据(思路梳理+全数据源+python代码)

标题&#xff1a;金融投资行为与企业技术创新 ——动机分析与经验证据 参考文献 作者&#xff1a;段军山 庄旭东 数据概况 1、数据来源&#xff1a; 第三方数据库 2、样本&#xff1a;2009-2018 中国 A 股市场的上市公司科技数据和财务数据 3、数据处理&#xff1a; (1)由…

Filter和Listener学习笔记

1.什么是Filter 2.定义过滤器 &#xff08;注意导的Filter的包来源是Javax.servlet&#xff09; /* 定义一个过滤器的步骤&#xff1a;1. 自定义一个类实现Filter接口2. 把过滤器规则定义在doFilter方法里面3. 使用WebFilter注解配置过滤的路径*///过滤的路径 //WebFilter(&qu…

用拙劣的Python技术画出的圣诞树(勿喷)

2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 一、前言 今天晚上,准备睡觉的我看见了CSDN有活动了,我就立马飞奔到电脑前面 这不,CSDN又举办了一场活动,看着令人心动的奖励和丰富的勋章,我的口水都从眼睛里流出来了(夸张),我准备创造一颗圣诞树,但是又想到我…

「设计模式」责任链模式

「设计模式」责任链模式 文章目录「设计模式」责任链模式一、概述二、结构三、案例实现四、优缺点五、应用场景六、模拟过滤器机制七、拓展八、小结一、概述 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;为请求创建了一个接收者对象的链。这种模式给予…

高频时序数据的储存与统计方案

文章目录问题背景解决办法第一步&#xff0c;梳理数据和计算要求第二步&#xff0c;确定存储和计算方案第三步&#xff0c;确定技术选型和方案第四步&#xff0c;实施优化方案后记问题背景 发电设备中常常会放置传感器&#xff08;DCS&#xff09;来采集数据以监控设备运转的状…

河海大学软件工程学硕考研复试经验贴

一、写在前面 想必看到这篇文章的学弟学妹都已经考完初试了&#xff0c;考得如何每个人心中各有千秋。无论如何&#xff0c;坚持将考研整个过程走下来的你们就已经是最棒的了&#xff0c;现在可以好好休息一下&#xff0c;静待考研成绩的公布了。 我写下这篇文章的目的主要是…

4.3.1、IPv4 地址概述

1、基本介绍 在 TCP/IP 体系中&#xff0c;IP 地址是一个最基本的概念&#xff0c;我们必须把它弄清楚。 IPv4 地址就是给因特网&#xff08;Internet&#xff09;上的每一台主机(或路由器)的每一个接口分配一个在全世界范围内是唯一的 32 比特的标识符。 IP 地址由因特网名…

5.8 什么是学习博主?看两个博主案例【玩赚小红书】

先看大家看两个博主案例 ​ 学习博主&#xff0c;就是专门为用户提供学习方法的人。 学习方法在小红书的内容中属于干货价值&#xff0c;也就是用户们需要的东西&#xff0c;能为他们解决问题的内容&#xff0c;所以是比较受欢迎的&#xff0c;换言之&#xff0c;就是笔记数据…

Spark 3.0 - 15.ML PIC 快速迭代聚类理论与实战

目录 一.引言 二.PIC 理论 1.谱聚类 2.快速迭代聚类 三.PIC 实战 1.数据准备 2.构建 PIC 3.预测与展示 四.总结 一.引言 前面介绍了 K-means 聚类与高斯混合聚类&#xff0c;本文介绍另外一种聚类方法 Power Iteration Cluster 快速迭代聚类&#xff0c;简称 PIC。快…

【架构设计】你的类足够“专一”吗

前言 软件设计SOLID原则中有一个最基础的原则就是单一职责原则&#xff0c;我想绝大部分的程序员都知道&#xff0c;而且都理解它的意思&#xff0c;甚至觉得很简单。但是往往“看懂”和“会用”是两回事&#xff0c;而“用好”更是难上加难。好比我们项目&#xff0c;一开始一…

取代OpenFeign:Spring Framework 6全新声明式客户端@HttpExchange

本文已被https://yourbatman.cn收录&#xff1b;女娲Knife-Initializr工程可公开访问啦&#xff1b;程序员专用网盘https://wangpan.yourbatman.cn&#xff1b;技术专栏源代码大本营&#xff1a;https://github.com/yourbatman/tech-column-learning&#xff1b;公号后台回复“…

【几种可调动对象,Function和bind;线程的调动方式举例】

1.可调动对象的调动方式 方法 1、函数指针调动 方法2 、类类型的括号的重载 调动可调动对象 #include<iostream> #include<functional> using namespace std; struct Foo {void operator()(int x){cout<<"Foo operator "<<x<<endl;}…

CSS3【定位的基本使用[静态定位\相对定位\绝对定位]、子绝父相、固定定位、元素的层级关系】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、定位1.1定位的基本介绍1.1.1 网页常见布局方式1.1.2 定位的常见应用场景1.2 定位的基本使用1.2.1 定位初体验1.2.2 使用定位的步骤1.2.3 静态定位1.2.4 小结1.2.…

使用 Docker Hub 完美地存储 Helm 图表实战

使用 Docker Hub 完美地存储 Helm 图表实战 Helm 是 Kubernetes 的包管理器。它是一个开源容器编排系统。它通过提供一种简单的方法来定义、安装和升级复杂的 Kubernetes 应用程序&#xff0c;帮助您管理 Kubernetes 应用程序。 使用 Helm&#xff0c;您可以将您的应用程序打包…

git教程

教程目录Git 教程Git 与 SVN 区别Git 与 SVN 区别点&#xff1a;Git 安装配置Linux 平台上安装Debian/UbuntuCentos/RedHatWindows 平台上安装Mac 平台上安装Git 配置用户信息文本编辑器##差异分析工具查看配置信息Git 工作流程Git 工作区、暂存区和版本库基本概念Git 创建仓库…

semargl 软件使用方法简介

文章目录前言一、semargl 软件使用简介1.semargl 软件简介2.准备演示软件操作所需的数据3.使用 semargl 获取频谱关系4.使用 semargl 获取特定频率模式的空间分布5.使用 semargl 获取自旋波的色散关系二、笔记05第三节内容的补充1.优化多进程读取磁化数据文件的代码2.新增获取特…

【JavaSE】 常用类(447~515)

String 447.常用类-每天一考 1.画图说明线程的生命周期&#xff0c;以及各状态切换使用到的方法等 状态&#xff0c;方法 2.同步代码块中涉及到同步监视器和共享数据&#xff0c;谈谈你对同步监视器和共享数据的理解&#xff0c;以及注意点。 synchronized(同步监视器){//操…

Python手势识别与追踪

程序示例精选 Python手势识别与追踪 如需安装运行环境或远程调试&#xff0c;见文章底部微信名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Python手势识别与追踪>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 应…

【日常】圣诞节、颜色⛄

2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 关于圣诞节&#x1f384;&#xff0c;大家想到什么颜色&#xff1f;⛄&#x1f98c;&#x1f381;&#x1f385;&#x1f525; demo online - https://codepen.io/adamlindqvist/pen/EaPeJg html <!-- Christ…

详细介绍关于自定义类型:结构体、枚举、联合【c语言】

文章目录结构体结构体的声名特殊的声明结构成员的类型结构的自引用结构体变量的定义和初始化结构体内存对齐修改默认对齐数结构体变量访问成员结构体传参结构体实现位段&#xff08;位段的填充&可移植性&#xff09;位段的内存分配位段的跨平台问题枚举枚举类型的定义枚举的…