并发编程——4.共享模型之内存

news2024/11/14 18:18:21

目录

  • 4.共享模型之内存
    • 4.1.Java 内存模型
    • 4.2.可见性
      • 4.2.1.退不出的循环
      • 4.2.2.解决办法
      • 4.2.3.可见性 vs 原子性
    • 4.3.终止模式之两阶段终止模式
      • 4.3.1.错误思路
      • 4.3.2.两阶段终止模式
    • 4.4.同步模式之 Balking
      • 4.4.1.定义
      • 4.4.2.实现
    • 4.5.有序性
      • 4.5.1.指令级并行原理
      • 4.5.2.案例
    • 4.6.原理之 volatile
      • 4.6.1.如何保证可见性
      • 4.6.2.如何保证有序性
      • 4.6.3.double-checked locking 问题
      • 4.6.4.double-checked locking 解决
    • 4.7.happens-before

本文笔记整理来自黑马视频https://www.bilibili.com/video/BV16J411h7Rd/?p=134,相关资料可在视频评论区进行获取。

4.共享模型之内存

上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。

4.1.Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响;
  • 可见性:保证指令不会受 cpu 缓存的影响;
  • 有序性:保证指令不会受 cpu 指令并行优化的影响;

4.2.可见性

4.2.1.退不出的循环

(1)先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

package cn.itcast.test;

import java.util.concurrent.TimeUnit;

public class Test24 {
    static boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ....
            }
        });
        t.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("暂停 t");
        run = false; // 线程 t 不会如预想的停下来
    }
}

(2)为什么呢?分析一下:
① 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

在这里插入图片描述

② 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。

在这里插入图片描述

③ 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

在这里插入图片描述

4.2.2.解决办法

(1)使用 volatile(易变关键字),它可以用来修饰成员变量静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

volatile static boolean run = true;

(2)使用 synchronized。

public class Test24 {
    static boolean run = true;
    
    final static Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    if (!run) {
                        break;
                    }
                }
                // ....
            }
        });
        t.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("暂停 t");
        synchronized (lock) {
            run = false;
        }
    }
}

4.2.3.可见性 vs 原子性

(1)前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况,上例从字节码理解是这样的:

getstatic run 	// 线程 t 获取 run true
getstatic run 	// 线程 t 获取 run true
getstatic run 	// 线程 t 获取 run true
getstatic run 	// 线程 t 获取 run true
putstatic run 	// 线程 main 修改 run 为 false, 仅此一次
getstatic run 	// 线程 t 获取 run false 

(2)比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错:

// 假设i的初始值为0
getstatic i 	// 线程2-获取静态变量i的值 线程内i=0
getstatic i 	// 线程1-获取静态变量i的值 线程内i=0
iconst_1 		// 线程1-准备常量1
iadd 			// 线程1-自增 线程内i=1
putstatic i 	// 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 		// 线程2-准备常量1
isub 			// 线程2-自减 线程内i=-1
putstatic i 	// 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是 synchronized 是属于重量级操作,性能相对更低。如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

4.3.终止模式之两阶段终止模式

Two Phase Termination:在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

4.3.1.错误思路

(1)使用线程对象的 stop() 方法停止线程:
stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
(2)使用 System.exit(int) 方法停止线程:
目的仅是停止一个线程,但这种做法会让整个程序都停止。

4.3.2.两阶段终止模式

在这里插入图片描述
(1) 利用 isInterrupted
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行。

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test13")
public class Test13 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        
        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;
    
    //启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread curThread = Thread.currentThread();
                if (curThread.isInterrupted()) {
                    log.debug("处理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //重新设置打算标记
                    curThread.interrupt();
                }
            }
        });
    
        monitor.start();
    }
    
    //停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}

输出结果如下:

11:00:42 [Thread-0] c.TwoPhaseTermination - 执行监控记录
11:00:43 [Thread-0] c.TwoPhaseTermination - 执行监控记录
11:00:44 [Thread-0] c.TwoPhaseTermination - 执行监控记录
11:00:44 [Thread-0] c.TwoPhaseTermination - 处理后事

Process finished with exit code 0

(2)利用停止标记

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test13")
public class Test13 {
    public static void main(String[] args) throws InterruptedException {
        TPTVolatile t = new TPTVolatile();
        t.start();
        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TPTVolatile {
    private Thread thread;
    private volatile boolean stop = false;
    
    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    
                }
                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }
    
    public void stop() {
        stop = true;
        thread.interrupt();
    }
}

输出结果如下:

11:13:13 [监控线程] c.TwoPhaseTermination - 将结果保存
11:13:14 [监控线程] c.TwoPhaseTermination - 将结果保存
11:13:15 [监控线程] c.TwoPhaseTermination - 将结果保存
11:13:15 [main] c.Test13 - stop
11:13:15 [监控线程] c.TwoPhaseTermination - 料理后事

Process finished with exit code 0

4.4.同步模式之 Balking

4.4.1.定义

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。

4.4.2.实现

@Slf4j(topic = "c.TwoPhaseTermination")
class TPTVolatile {
    private Thread thread;
    //停止标记
    private volatile boolean stop = false;
    //判断是否执行过 start 方法
    private boolean starting = false;
    
    public void start() {
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                
                }
                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }
    
    public void stop() {
        stop = true;
        thread.interrupt();
    }
}

它还经常用来实现线程安全的单例:

public final class Singleton {
    private Singleton() {
    }
    
    private static Singleton INSTANCE = null;
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

4.5.有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码:

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是:

i = ...;
j = ...;

也可以是:

j = ...;
i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下。

4.5.1.指令级并行原理

(1)事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段。

在这里插入图片描述

术语参考:
instruction fetch (IF)
instruction decode (ID)
execute (EX)
memory access (MEM)
register write back (WB)

(2)在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位。

提示:分阶段,分工是提升效率的关键!

(3)指令重排的前提是,重排指令不能影响结果,例如:

// 可以重排的例子
int a = 10; 		// 指令1
int b = 20; 		// 指令2
System.out.println(a + b);

// 不能重排的例子
int a = 10; 		// 指令1
int b = a - 5; 		// 指令2

4.5.2.案例

int num = 0;
boolean ready = false;
    
// 线程1 执行此方法
public void actor1(I_Result r) {
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?
有同学这么分析
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1;
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1;
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了);
但事实上,结果还有可能是 0。这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2。这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现。借助 java 并发压测工具 jcstress。

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -
DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -
DartifactId=ordering -Dversion=1.0 

创建 maven 项目,提供如下测试类:

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

执行

mvn clean install
java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

在这里插入图片描述

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。

解决方法:volatile 修饰的变量,可以禁用指令重排。

volatile boolean ready = false;

结果如下:

*** INTERESTING tests
 Some interesting behaviors observed. This is for the plain curiosity.
 0 matching test results. 

4.6.原理之 volatile

volatile 的底层实现原理是内存屏障,即 Memory Barrier (Memory Fence)。

  • 对 volatile 变量的写指令后会加入写屏障;
  • 对 volatile 变量的读指令前会加入读屏障;

4.6.1.如何保证可见性

(1)写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。

public void actor2(I_Result r) {
	num = 2;
	ready = true; // ready 是 volatile 赋值带写屏障
	// 写屏障
}

(2)而读屏障 (lfence) 保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。

public void actor1(I_Result r) {
	// 读屏障
	// ready 是 volatile 读取值带读屏障
	if(ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
}

在这里插入图片描述

4.6.2.如何保证有序性

(1)写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。

public void actor2(I_Result r) {
	num = 2;
	ready = true; // ready 是 volatile 赋值带写屏障
	// 写屏障
}

(2)读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。

public void actor1(I_Result r) {
	// 读屏障
	// ready 是 volatile 读取值带读屏障
	if(ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
}

在这里插入图片描述

(3)还是那句话,不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去;
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序;
    在这里插入图片描述

4.6.3.double-checked locking 问题

(1)以著名的 double-checked locking 单例模式为例:

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化;
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁;
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外;

(2)但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:

在这里插入图片描述

关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值,这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例。对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效。

4.6.4.double-checked locking 解决

public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // t2
                // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

字节码上看不出来 volatile 指令的效果。

// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中;
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据;
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后;
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前;
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性;

在这里插入图片描述

4.7.happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

(1)线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见。

static int x;
static Object m = new Object();
new Thread(()->{
    synchronized(m) {
        x = 10;
    }},"t1").start();
new Thread(()->{
    synchronized(m) {
        System.out.println(x);
    }
},"t2").start();

(2)线程对 volatile 变量的写,对接下来其它线程对该变量的读可见。

volatile static int x;
new Thread(()->{
    x = 10;
},"t1").start();
new Thread(()->{
    System.out.println(x);
},"t2").start();

(3)线程 start 前对变量的写,对该线程开始后对该变量的读可见。

static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();

(4)线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)。

static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();

(5)线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)。

static int x;
public static void main(String[] args) {
    Thread t2 = new Thread(()->{
        while(true) {
            if(Thread.currentThread().isInterrupted()) {
                System.out.println(x);
                break;
            }
        }
    },"t2");
    t2.start();
    new Thread(()->{
        sleep(1);
        x = 10;
        t2.interrupt();
    },"t1").start();
    while(!t2.isInterrupted()) {
        Thread.yield();
    }
    System.out.println(x);
}

(6)对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。

(7)具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子:

volatile static int x;
static int y;
new Thread(()->{
    y = 10;
    x = 20;
},"t1").start();
new Thread(()->{
    // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
    System.out.println(x);
},"t2").start();

注意:这里的变量都是指成员变量或静态成员变量。

在这里插入图片描述

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

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

相关文章

LVS详解

一、负载均衡&#xff1a;必不可少的基础手段 1.1 找更多的牛来拉车吧 当前大多数的互联网系统都使用了服务器集群技术&#xff0c;集群即将相同服务部署在多台服务器上构成一个集群整体对外提供服务&#xff0c;这些集群可以是Web应用服务器集群&#xff0c;也可以是数据库服务…

新冠“照妖镜”,体质弱点现原形。你是啥症状?2023年,请好好善待你的身体!

新冠“照妖镜”&#xff0c;体质弱点现原形。你是啥症状&#xff1f; 阳性之后的不同症状&#xff0c;是我们身体发出的【预警信号】。 病毒进入时&#xff0c;最先攻击我们自身最薄弱的地方。 2023年&#xff0c;请好好【善待】你的身体&#xff01; 症状1 、头疼 出现头痛…

Kurganov-Tadmor二阶中心格式:理论介绍

简介 CFD的核心问题是求解双曲偏微分方程 ∂∂tu(x,t)∂∂xf(u(x,t))0\frac{\partial}{\partial t} u(x, t)\frac{\partial}{\partial x} f(u(x, t))0 ∂t∂​u(x,t)∂x∂​f(u(x,t))0在CFD中&#xff0c;双曲偏微分方程一般使用Godunov型迎风格式求解。但是这种迎风格式往往实…

2022年度学习总结

2022年有焦虑也有成长&#xff0c;记录和总结也是成长的一部分。这一年&#xff0c;我也努力在不确定性中做一些确定的事情&#xff0c;感恩被保护的三年&#xff0c;三年清零抗疫结束&#xff0c;做好自己健康的第一责任人。研一半个学期在网课或者封校中度过&#xff0c;我们…

1.0、Linux-入门概述

1.0、Linux-入门概述 我们为什么要学习 Linux &#xff1f; Linux诞生了这么多年&#xff0c;以前还喊着如何能取代 Windows 系统&#xff0c;现在这个口号已经小多了&#xff0c;任何事物发展都有其局限性&#xff1b;如同现在国内在搞一个社交软件取代 QQ 、微信 一样&#x…

已解决(Python语法报错)SyntaxError: invalid syntax

已解决&#xff08;Python语法报错&#xff09;SyntaxError: invalid syntax 文章目录报错信息报错翻译报错原因解决方法帮忙解决报错信息 粉丝群里面一个小伙伴运行Python代码&#xff0c;但是发生了报错&#xff08;当时他心里瞬间凉了一大截&#xff0c;跑来找我求助&…

IDEA安装与配置教程

一、下载并安装IDEA 1、下载官网&#xff1a; 下载 IntelliJ IDEA &#xff08;这里以Windows系统为例&#xff0c;其他系统类似&#xff09; 2、安装 1、下载完成后&#xff0c;直接点击安装包安装&#xff0c;即可。 2、开始安装&#xff0c;然后下一步 3、可以在此处自定…

算法 - 蓝桥杯并查集题型

目录 合并集合 连通块中点的数量 蓝桥杯2017年第八届真题-合根植物 [蓝桥杯][2019年第十届真题] 修改数组 蓝桥幼儿园 刷了好多题&#xff0c;发现并查集这种思想挺妙的&#xff0c;是时候总结一下了&#xff1b; 作用与基本原理&#xff1a; 套路问题&#xff1a; 用一道…

Mac M1 Pro下载node.js

Mac M1 Pro下载node.js基本信息 Mac M1 Pronode版本&#xff1a;14.20.0npm版本&#xff1a;8.19.3cnpm版本&#xff1a;8.5.1 注意&#xff1a; 我之前是去官网下了最新版本node&#xff0c;但cnpm就是下载不下来&#xff0c;于是查了其他博主的文章&#xff0c;这边通过nvm…

aws codepipeline 配置 ecs 蓝绿部署

参考资料 CI/CD workshop for Amazon ECSTutorial: Create a pipeline with an Amazon ECR source and ECS-to-CodeDeploy deploymentAmazon ECS 计算平台上的部署CodeDeploy AppSpec 文件引用 之前的文章介绍了通过codepipeline对ecs服务进行滚动更新&#xff0c;本文主要介…

Docker 学习总结(78)—— WebAssembly 入门简介

什么是 WebAssembly? WebAssembly 是一种定义二进制指令格式的开放标准&#xff0c;它支持从不同的源语言创建可移植的二进制可执行文件。这些二进制文件可以在各种环境中运行。它起源于 Web&#xff0c;并得到各大主流浏览器的支持。 Wasm 如何在浏览器中工作&#xff1f; …

RCTF-pwn-diary

RCTF-pwn-diary 赛后看了一眼发现给出了源码&#xff0c;https://github.com/ruan777/RCTF2022/blob/main/diary/main.cpp 漏洞是erase的问题 解释一下 add(0) add(1) add(2) delete(1)这样子的话&#xff0c;其实就是把2给删除&#xff0c;把2的内容复制到1中&#xff0c;所…

Base64自定义编码表及破解

什么是Base64 Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一&#xff0c;Base64并不是安全领域的加密算法&#xff0c;其实Base64只能算是一个编码算法&#xff0c;对数据内容进行编码来适合传输。标准Base64编码解码无需额外信息即完全可逆&#xff0c;即使你自…

基于Python + Django 开发一款学生管理系统(附源码)

文章目录1.开发环境源码分享&技术交流2.项目实战1&#xff09;创建Django项目2&#xff09;创建应用3&#xff09;配置MySQL4&#xff09;数据模型层创建5&#xff09;路由配置6&#xff09;增删改查视图函数7&#xff09;模板页面创建8&#xff09;启动web服务1.开发环境 …

最大似然和贝叶斯参数估计

统计生成模型的参数估计 – Maximum Likelihood(ML) 假设参数是某个确定的值&#xff0c;通过使似然度最大求出参数 – Bayesian estimation 假设参数是随机变量&#xff0c;估计参数分布的参数 – 最大似然求出具体的参数&#xff0c;贝叶斯求的是参数的分布 最大似然估计 假…

献给自己技术成长的第三年

年度总结词语&#xff1a;幸运 献给自己技术成长的第三年一、五州一都二、if else量产三、学技术四、用真心五、设计精产六、感恩幸运一、五州一都 1.1-1.12成都、2.23-3.19广州、4.12-7.23苏州、8.12-8.20兰州、8.23-9.20湖州、其余杭州 成都。去年年底出差到成都&#xff0c;…

HTTP_day01

在互联网世界里&#xff0c;HTTP 通常跑在 TCP/IP 协议栈之上&#xff0c;依靠 IP 协议实现寻址和路由、TCP 协议实现可靠数据传输、DNS 协议实现域名查找、SSL/TLS 协议实现安全通信。此外&#xff0c;还有一些协议依赖于 HTTP&#xff0c;例如 WebSocket、HTTPDNS 等。这些协…

【数据结构】冒泡排序、快速排序(递归,非递归)、归并排序(递归,非递归),七大排序比较,

文章目录冒泡排序快速排序归并排序七大排序之间的对比冒泡排序 基本思想&#xff1a;所谓交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置&#xff0c;交换排序的特点是&#xff1a;将键值较大的记录向序列的尾部移动&#xff0c;键值较小…

高性能web网关之Openresty相关基础知识

高性能web网关之Openresty一、Openresty 简介二、Openresty 应用场景三、lua-nginx-module3.1、Lua 模块指令顺序3.2、Lua嵌入nginx四、责任链五、cosocket后言一、Openresty 简介 openresty 是一个基于 nginx 与 lua 的高性能 web 平台&#xff0c;其内部集成了大量精良的 lu…

148.排序链表

148.排序链表 题目&#xff1a; 给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4]示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;…