Java 并发编程面试题——synchronized 与 volatile

news2024/11/27 14:43:46

目录

  • 1.synchronized
    • 1.1.synchronized 是什么?有什么作用?
    • 1.2.如何使用 synchronized?
    • 1.3.synchronized 的底层原理是什么?
      • 1.3.1.synchronized 同步语句块
      • 1.3.2.synchronized 修饰方法
      • 13.3.总结
    • 1.4.JDK1.6 之后的 synchronized 底层做了哪些优化?
    • 1.5.早期的 synchronized 为什么被称为重量级锁?请从操作系统层面来说明。
    • 1.6.synchronized 和 volatile 有什么区别?
  • 2.volatile
    • 2.1.volatile 是什么?有什么作用?
    • 2.2.volatile 的内存语义是什么样的?
      • 2.2.1.内存可见性
      • 2.2.2.禁止重排序
    • 2.3.volatile 有哪些用途?

前置知识:
Java 内存模型基础知识
重排序与 happens-before

1.synchronized

1.1.synchronized 是什么?有什么作用?

(1)synchronized 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

(2)在 Java 早期版本中,synchronized 属于重量级锁,效率低下。这是因为监视器锁 (monitor) 是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

(3)不过,在 Java 6 之后, synchronized 引入了大量的优化,例如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此,synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized。

1.2.如何使用 synchronized?

synchronized 关键字的使用方式主要有下面 3 种:

(1)修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。

synchronized void method() {
    //业务代码
}

(2)修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

思考:静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?
答:不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

(3)修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:

  • synchronized(object):表示进入同步代码库前要获得给定对象的锁;
  • synchronized(类.class):表示进入同步代码前要获得给定 Class 的锁;
synchronized(this) {
    //业务代码
}

总结:

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

注意:构造方法不能使用 synchronized 关键字修饰,其原因在于构造方法本身就属于线程安全的,不存在同步的构造方法一说。

在这里插入图片描述

1.3.synchronized 的底层原理是什么?

public class Synchronized {
    public static void main(String[] args) {
        // 对Synchronized Class对象进行加锁
        synchronized (Synchronized.class) {
        }
        
        // 静态同步方法,对Synchronized Class对象进行加锁
        m();
    }
    
    public static synchronized void m() {
    }
}

在 Synchronized.class 同级目录执行 javap–v Synchronized.class,部分相关输出如下所示:

public static void main(java.lang.String[]);
		// 方法修饰符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
        Code:
        stack=2, locals=1, args_size=1
        0: ldc #1 // class com/murdock/books/multithread/book/Synchronized
        2: dup
        3: monitorenter // monitorenter:监视器进入,获取锁
        4: monitorexit // monitorexit:监视器退出,释放锁
        5: invokestatic #16 // Method m:()V
        8: return
	public static synchronized void m();
	// 方法修饰符,表示: public static synchronized
        flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
        stack=0, locals=0, args_size=0
        0: return

1.3.1.synchronized 同步语句块

由上述信息可知,synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权。在执行 monitorenter 时,会尝试获取对象的锁:

  • 如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁
  • 如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

1.3.2.synchronized 修饰方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

13.3.总结

(1)无论采用哪种方式,对于同步块的实现的本质是对一个对象的监视器 (monitor) 进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器

(2)任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用
时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入 BLOCKED 状态。

(3)下图描述了对象、对象的监视器、同步队列和执行线程之间的关系:

在这里插入图片描述

从上图中可以看到,任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

有关抽象队列同步器的相关知识可以参考Java 并发编程面试题——Lock 与 AbstractQueuedSynchronizer (AQS)这篇文章。

1.4.JDK1.6 之后的 synchronized 底层做了哪些优化?

(1)JDK1.6 对锁的实现引入了大量的优化,如偏向锁轻量级锁自旋锁适应性自旋锁锁消除锁粗化等技术来减少锁操作的开销:

  • 偏向锁 (Biased Locking):偏向锁是 JVM 对 synchronized 关键字的一种优化,用于减少无竞争情况下的锁操作开销。偏向锁的核心思想是默认情况下,一个线程第一次获得锁时,将对象头中的标志设置为偏向锁,并将线程ID记录在对象头中,之后该线程再次请求锁时,无需进行同步操作即可获取锁。
  • 轻量级锁 (Lightweight Locking):在多个线程之间进行竞争的情况下,JVM 将使用轻量级锁来提高性能。轻量级锁通过使用CAS(比较并交换)操作对对象头进行加锁和解锁,避免了重量级锁的开销。如果出现竞争,轻量级锁会升级为重量级锁。
  • 自旋锁 (Spin Lock):自旋锁是在获取锁时,线程不会被阻塞,而是执行忙等待,不断尝试获取锁。自旋锁适用于锁持有者保持锁时间很短且线程间竞争不激烈的情况。如果锁竞争激烈或锁持有时间较长,自旋锁会浪费 CPU 时间。
  • 锁粗化 (Lock Coarsening):锁粗化是将多个细粒度的连续加锁、解锁操作合并为一个更大范围的加锁、解锁操作,减少了加锁、解锁的次数和开销。这个优化避免了频繁的加锁、解锁对性能的影响。
  • 锁消除 (Lock Elimination):锁消除是在编译器层面进行的优化,根据程序的静态分析,判断某些锁是多余的,可以被消除。例如,当编译器确定某个对象只在单线程中被访问时,就可以将其相关的锁消除。

(2)锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。锁的优缺点的对比以及使用场景如下所示:

在这里插入图片描述

1.5.早期的 synchronized 为什么被称为重量级锁?请从操作系统层面来说明。

(1)在操作系统层面,synchronized 锁是重量级锁(也称为互斥锁或内核级锁),主要是因为它涉及到操作系统的内核态与用户态之间的上下文切换。当一个线程尝试进入一个被 synchronized 修饰的临界区时,如果该临界区已被其他线程持有,那么该线程就会被阻塞,进入阻塞状态。这个阻塞操作是由操作系统内核来实现的,并且涉及到内核态和用户态之间的切换,这是由于内核在操作系统的核心中运行,而且具有更高的权限。这个上下文切换过程是相对复杂且开销较大的,所以 synchronized 锁被称为重量级锁。

(2)在具体的实现中,JVM 通过操作系统提供的原生线程机制来实现线程同步,而原生线程机制是操作系统内核级别的,会涉及到内核的调度、线程的阻塞、唤醒等操作。当一个线程被阻塞后,它会让出 CPU 的执行权,并进入阻塞队列等待被唤醒。而唤醒线程的操作也需要操作系统内核的参与。

(3)由于重量级锁的机制涉及到内核调度和上下文切换,所以在并发量较高的情况下,频繁的锁竞争会导致大量线程的阻塞和唤醒操作,进而带来较大的系统开销,降低了系统的并发性能。

(4)为了减少这种开销,JDK 的并发包中提供了更轻量级的锁实现,比如 ReentrantLock 和 StampedLock,它们使用了更高效的自旋、CAS 操作等技术来减少线程的阻塞和上下文切换次数,提高了并发性能。但需要注意的是,在并发度不是特别高的情况下,synchronized 关键字的性能已经足够,并且由于 JVM 对其进行了优化,使用起来更加简单和安全。

1.6.synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,它们之间的区别如下:

  • volatile 是线程同步的轻量级实现,所以 volatile 性能比 synchronized 高。
  • volatile 只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
  • volatile 能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

2.volatile

2.1.volatile 是什么?有什么作用?

(1)synchronized 是 Java 中的一个关键字,翻译成中文是不稳定的意思,其作用如下:

  • 保证变量的可见性
    • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存
    • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
  • 防止 JVM 的指令重排序: volatile 关键字可以防止编译器和处理器对指令进行重排序优化
    • 在 volatile 变量的写操作之后,会插入一个内存屏障 (Memory Barrier) 指令,确保在该指令之前的所有操作都发生在该指令之前。
    • 在 volatile 变量的读操作之前,也会插入一个内存屏障指令,确保在该指令之后的所有操作都发生在该指令之后。这样,可以防止指令重排序对变量的读写操作顺序造成影响。

(2)volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。volatile 关键字能保证数据的可见性有序性,但不能保证数据的原子性。synchronized 关键字则可以保证可见性原子性

2.2.volatile 的内存语义是什么样的?

2.2.1.内存可见性

(1)以⼀段示例代码开始:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    
    public void writer() {
        a = 1;          // step 1
        flag = true;    // step 2
    }
    
    public void reader() {
        if (flag) {                 // step 3
            System.out.println(a);  // step 4
        }
    }
}

在这段代码里,我们使用 volatile 关键字修饰了⼀个 boolean 类型的变量 flag 。 所谓内存可见性,指的是当⼀个线程对 volatile 修饰的变量进行写操作(比如 step 2)时,JMM 会立即把该线程对应的本地内存中的共享变量的值刷新到主内存;当⼀个线程对 volatile 修饰的变量进行读操作(比如 step 3)时,JMM 会把立即该线程对应的本地内存置为无效,从主内存中读取共享变量的值。

在这⼀点上,volatile 与锁具有相同的内存效果,volatile 变量的写和锁的释放具有相同的内存语义,volatile 变量的读和锁的获取具有相同的内存语义。

(2)假设在时间线上,线程A先执行 writer 方法,线程 B 后执行 reader 方法。那必然会有下图:
在这里插入图片描述
而如果 flag 变量没有用 volatile 修饰,在 step 2,线程 A 的本地内存里面的变量就不会立即更新到主内存,那随后线程 B 也同样不会去主内存拿最新的值,仍然使用线程 B 本地内存缓存的变量的值 a = 0,flag = false。

2.2.2.禁止重排序

(1)在 JSR-133 之前的旧的 Java 内存模型中,是允许 volatile 变量与普通变量重排序的。 那上面的案例中,可能就会被重排序成下列时序来执行:
① 线程 A 写 volatile 变量,step 2,设置 flag 为 true;
② 线程 B 读同⼀个 volatile,step 3,读取到 flag 为 true;
③ 线程 B 读普通变量,step 4,读取到 a = 0;
④ 线程 A 修改普通变量,step 1,设置 a = 1;
可见,如果 volatile 变量与普通变量发生了重排序,虽然 volatile 变量能保证内存可见性,也可能导致普通变量读取错误。

(2)所以在旧的内存模型中,volatile 的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供⼀种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强 volatile 的内存语义:严格限制编译器和处理器对 volatile 变量与普通变量的重排序。 编译器还好说,JVM 是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实现的。

什么是内存屏障?硬件层面,内存屏障分两种:读屏障 (Load Barrier) 和写屏障 (Store Barrier) 。内存屏障有两个作用:
① 阻止屏障两侧的指令重排序;
② 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。

注意这里的缓存主要指的是 CPU 缓存,如 L1,L2 等

(3)编译器在生成字节码时,会在指令序列中插⼊内存屏障来禁止特定类型的处理器重排序。编译器选择了⼀个比较保守的 JMM 内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的 volatile 内存语义。这个策略是:
① 在每个 volatile 写操作前插⼊⼀个 StoreStore 屏障;
② 在每个 volatile 写操作后插⼊⼀个 StoreLoad 屏障;
③ 在每个 volatile 读操作后插⼊⼀个 LoadLoad 屏障;
④ 在每个 volatile 读操作后再插⼊⼀个 LoadStore 屏障。
大概示意图是这个样子:
在这里插入图片描述

再逐个解释⼀下这几个屏障(注:下述 Load 代表读操作,Store 代表写操作):

  • LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2,在 Store2 及 后续写入操作执行前,保证 Store1 的写⼊操作对其它处理器可见。
  • LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。
  • StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

对于连续多个 volatile 变量读或者连续多个 volatile 变量写,编译器做了⼀定的优化来提高性能,比如:

  • 第⼀个 volatile 读;
  • LoadLoad 屏障;
  • 第⼆个 volatile 读;
  • LoadStore 屏障

(4)再介绍⼀下 volatile 与普通变量的重排序规则:
① 如果第⼀个操作是 volatile 读,那无论第⼆个操作是什么,都不能重排序;
② 如果第⼆个操作是 volatile 写,那无论第⼀个操作是什么,都不能重排序;
③ 如果第⼀个操作是 volatile 写,第⼆个操作是 volatile 读,那不能重排序。

举个例子,我们在案例中 step 1,是普通变量的写,step 2 是 volatile 变量的写,那符合第 2 个规则,这两个 steps 不能重排序。而 step 3是 volatile 变量读,step 4是普通变量读,符合第 1 个规则,同样不能重排序。

(5)但如果是下列情况:第⼀个操作是普通变量读,第⼆个操作是 volatile 变量读,那是可以重排序的:

// 声明变量
int a = 0; 							// 声明普通变量
volatile boolean flag = false; 		// 声明 volatile 变量
// 以下两个变量的读操作是可以重排序的
int i = a; 				// 普通变量读
boolean j = flag; 		// volatile变量读

内存语义:可以简单理解为 volatile,synchronize 等关键字在 JVM 中的内存方面实现原则。

2.3.volatile 有哪些用途?

(1)从 volatile 的内存语义上来看,volatile 可以保证内存可见性且禁止重排序。 在保证内存可见性这⼀点上,volatile 有着与锁相同的内存语义,所以可以作为⼀个“轻量级”的锁来使用。但由于 volatile 仅仅保证对单个 volatile 变量的读/写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比 volatile 更强大;在性能上,volatile 更有优势。

(2)在禁止重排序这⼀点上,volatile 也是非常有用的。比如我们熟悉的单例模式,其中有⼀种实现方式是“双重锁检查”,比如这样的代码:

public class Singleton {

    private static Singleton instance; 	// 不使⽤ volatile 关键字
    
    // 双重锁检验
    public static Singleton getInstance() {
        if (instance == null) { 			// 第7⾏
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 第10⾏
                }
            }
        }
        return instance;
    }
}

如果这里的变量声明不使用 volatile 关键字,是可能会发生错误的。它可能会被重排序:

instance = new Singleton(); // 第10⾏

// 可以分解为以下三个步骤 
1 memory=allocate();		// 分配内存,相当于 c 的 malloc
2 ctorInstanc(memory) 		// 初始化对象 
3 s=memory 					// 设置 s 指向刚分配的地址

// 上述三个步骤可能会被重排序为 1-3-2,也就是: 
1 memory=allocate();		// 分配内存相当于 c 的 malloc
3 s=memory 					//设置 s 指向刚分配的地址
2 ctorInstanc(memory) 		//初始化对象

而⼀旦假设发生了这样的重排序,比如线程 A 在第 10 行执行了步骤 1 和步骤 3,但是步骤 2 还没有执行完。这个时候线程 A 执行到了第 7 行,它会判定 instance 不为空, 然后直接返回了⼀个未初始化完成的 instance! 所以 JSR-133 对 volatile 做了增强后,volatile 的禁止重排序功能还是非常有用的。

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

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

相关文章

Kotlin(六) 类

目录 创建类 调用类 类的继承------open 构造函数 创建类 创建类和创建java文件一样,选择需要创建的目录New→Kotlin File/Class Kotlin中也是使用class关键字来声明一个类的,这一点和Java一致。现在我们可以在这个类中加入字段和函数来丰富它的功…

XnViewMP for Mac: 轻松浏览,精细管理,一键操作

XnViewMP for Mac是一款专业的图片浏览器、查看器和转换器,XnViewMP Mac版支持查看超过500种图像格式并导出为大约70种不同的文件格式,提供了易于使用但功能强大的批量转换模块,还有Unicode 支持、多种语言的翻译和方便的模块化界面&#xff…

Python超入门(2)__迅速上手操作掌握Python

​​​​​​​ # 5.字符串 # 5.字符串 course "Pythons Course for Beginner" # ""内可使用 # 012345678…… course2 Python For "Beginner" # 内可使用" course3 Hi John, Here is our first email to you.# 多行字符…

Window环境下安装VMware虚拟机来安装 CentOs7

软硬件准备 软件:VMware(16 pro):阿里云盘分享. 硬件:因为是在宿主机上运行虚拟化软件VMware安装centos,所以对宿主机的配置有一定的要求。最起码i5CPU双核、硬盘500G、内存4G以上。 镜像:CentOS7 ,下载地址 http://is…

30天入门Python(基础篇)——第1天:为什么选择Python

文章目录 专栏导读作者有话说为什么学习Python原因1(总体得说)原因2(就业说) Python的由来(来自百度百科)Python的版本 专栏导读 🔥🔥本文已收录于《30天学习Python从入门到精通》 🉑🉑本专栏专门针对于零基础和需要重新复习巩固…

记账工具:轻松添加新账户并记录明细

记账工具是一款方便易用的软件,可以帮助您轻松管理您的账目。它具有简单易用的界面,让您能够快速添加新账户并记录明细。此外,它还支持打印本页功能,让您能够轻松打印出您所记录的账目信息。 第一步,我们要打开晨曦记…

苏宁API接口解析,实现获得suning商品详情

要解析苏宁API接口并实现获取苏宁商品详情,你需要按照以下步骤进行操作: 了解苏宁开放平台:访问苏宁开放平台官网,找到API接口相关的开发者文档、指南等信息。注册开发者账号:在苏宁开放平台上注册一个开发者账号&…

23062C++QTday3

1> 自行封装一个栈的类,包含私有成员属性:栈的数组、记录栈顶的变量 成员函数完成:构造函数、析构函数、拷贝构造函数、入栈、出栈、清空栈、判空、判满、获取栈顶元素、求栈的大小 头文件stack.c #ifndef STACK_H #define STACK_H #in…

【Mysql】数据库第二讲(数据库中数据类型的介绍)

数据类型 1.数据类型分类2.数值类型介绍2.1tinyint类型2.2bit类型介绍2.3小数类型介绍2.3.1 float2.3.2decimal 3.字符串类型介绍3.1char3.2varchar面试:char和varchar的区别 4.日期和时间类型5.enum和set 1.数据类型分类 2.数值类型介绍 2.1tinyint类型 数值越界测…

每日刷题-3

目录 一、选择题 二、编程题 1、计算糖果 2、进制转换 一、选择题 1、 解析:在C语言中,以0开头的整数常量是八进制的,而不是十进制的。所以,0123的八进制表示相当于83的十进制表示,而123的十进制表示不变。printf函数…

费时“吃透”4个月啃烂完了这份Redis高手心法,成功上岸收到字节offer

学习一个技术,通常只接触了零散的技术点,没有在脑海里建立⼀个完整的知识框架和架构体系,没有系统观。这 样会很吃力,而且会出现一看好像自己会,过后就忘记,⼀脸懵逼。 今天小编带你⼀起吃透Redis&#xf…

这所985初试占比67%,某学院计算机学硕一志愿竟无人报名

北京科技大学(B) 考研难度(☆☆☆☆) 内容:23考情概况(拟录取和复试分析)、院校概况、23专业目录、23复试详情、各专业考情分析、各科目考情分析。 正文1721字,预计阅读:5分钟。 2023考情概况…

Java反编译工具 JD-GUI安装使用

我们知道,将源代码转换成二进制执行代码的过程叫“编译”,那么反编译就是将二进制执行代码转换成源代码。 在java开发里,源代码是.java文件,然后经过编译后生成计算机识别的.class文件,但是.class文件是计算机识别的我…

KT142C-sop16语音芯片ic测试板的使用说明_串口如何接线

KT142C是一个提供串口的SOP16语音芯片,完美的集成了MP3的硬解码。内置330KByte的空间,最大支持330秒的语音长度,支持多段语音,支持直驱0.5W的扬声器无需外置功放 1、软件支持串口通信协议,默认波特率9600.同时支持4个…

css自学框架之ajax获取提交数据

本小结主要自学了ajax,页面部分刷新,实现动态提交数据到服务器;动态从服务区获取数据。get,post两种传递数据方法,Json、html、text、xml等多种数据格式。 展示效果如下图: 一、Javascript代码&#xff…

线程的状态 and 线程安全

在操作系统中的线程,自身是有一个状态的~,但是Java Thread是对系统线程的封装,把这里的状态又进一步精细化了。 1.NEW 系统中的线程还没创建出来呢!只是有一个Thread对象~ public class Main1 {public static void main(String[…

日入千元什么感觉?信息安全就是这么“吸金”!

一年一次的护网行动已经火热开始了,大家还记得前段时间给大家推荐的护网吗? 现在不管是国家还是企业都很重视信息安全,毕竟科技的发展越来越迅速,我们的信息安全时时刻刻都需要保障。 据研究机构统计,截止2021年&…

数字出版的资源管理系统软件开发

出版行业资源管理系统是为出版公司和数字出版机构设计的软件工具,用于帮助管理和优化出版过程。这些系统通常具有多种功能,以支持从内容创作到分销的各个方面。以下是出版行业资源管理系统的一些主要功能,希望对大家有所帮助。北京木奇移动技…

【计算机基础知识2】操作系统、应用程序和编程语言

目录 前言 一、计算机操作系统 二、计算机应用程序 三、计算机编程语言 四、操作系统、应用程序和编程语言的相互关系 总结 前言 计算机的操作系统、应用程序和编程语言是计算机科学中非常重要的三个方面。了解这三个方面的基础知识和它们之间的相互关系,对于…

编程技巧,Python缩进规则(包含快捷键)

和其它程序设计语言​ (如 Java、C 语言)采用大括号“{}”分隔代码块不同,Python 采用代码缩进和冒号( : )来区分代码块之间的层次。 ​ 在 Python 中,对于类定义、函数定义、流程控制语句、异常处理语句等…