Java共享问题 、synchronized 线程安全分析、Monitor、wait/notify

文章目录

  • 1.共享带来的问题
    • 1.1 临界区 Critical Section
    • 1.2 竞态条件 Race Condition
  • 2. synchronized语法及理解
    • 2.1 方法上的 synchronized
  • 3.变量的线程安全分析
    • 3.1.成员变量和静态变量是否线程安全?
    • 3.2.局部变量是否线程安全?
      • 3.2.1 局部变量线程安全分析
  • 4.Monitor
    • 4.1 Java 对象头
    • 4.2 Monitor概念
  • 5.synchronized原理
    • 5.1 轻量级锁
    • 5.2 锁膨胀
    • 5.3 自旋优化
    • 5.4 偏向锁
      • 5.4.1 偏向状态
      • 5.4.2 撤销偏向锁
        • 5.4.2.1 撤销-调用对象hashCode
        • 5.4.2.3 撤销-其他线程使用对象
        • 5.4.2.4 撤销- 调用wait/notify
      • 5.4.3 批量重偏向
      • 5.4.4 批量撤销
      • 5.4.5 锁消除
      • 5.4.6 锁粗化
  • 6.wait/notify
    • 6.1 wait/notify原理
    • 6.2 API介绍
    • 6.3 wait、notify 的正确使用

1.共享带来的问题

(1)两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		 for (int i = 0; i < 5000; i++) {
 			counter++;
 		}
	}, "t1");
 	Thread t2 = new Thread(() -> {
 		for (int i = 0; i < 5000; i++) {
 			counter--;
 		}
 	}, "t2");
 	t1.start();
	t2.start();
 	t1.join();
 	t2.join();
 	log.debug("{}",counter);
}

(2)以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如:
①对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

②而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

(3)如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

(4)出现负数的情况
在这里插入图片描述

(5)出现正数的情况:

在这里插入图片描述

1.1 临界区 Critical Section

(1)一个程序运行多个线程本身是没有问题的
(2)问题出在多个线程访问共享资源
①多个线程读共享资源其实也没有问题
②在多个线程对共享资源读写操作时发生指令交错,就会出现问题
(3)一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
(4)例如,下面代码中的临界区

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		for (int i = 0; i < 5000; i++)
		// 临界区
 		{
		counter++;
		}
 	}, "t1");
 	Thread t2 = new Thread(() -> {
 		for (int i = 0; i < 5000; i++)
 		// 临界区
 		{
 		counter--;
 		}
 	}, "t2");
 	t1.start();
 	t2.start();
 	t1.join();
 	t2.join();
 	log.debug("{}",counter);
}

1.2 竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。为了避免临界区的竞态条件发生,有多种手段可以达到目的。
(1)阻塞式的解决方案:synchronized,Lock
(2)非阻塞式的解决方案:原子变量
(3)synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
(4)虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
①互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
②同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

2. synchronized语法及理解

(1)语法

synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}

(2)理解
在这里插入图片描述
①synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
②当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
②这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,自身发生了上下文切换,由运行阶段变为阻塞状态
③这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
④当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

2.1 方法上的 synchronized

class Test{
 public synchronized void test() {
 
 }
}
等价于
class Test{
 public void test() {
	 synchronized(this) {

	 }
  }
}
class Test{
 public synchronized static void test() {
  }
}
等价于
class Test{
 public static void test() {
 	synchronized(Test.class) {
 
 	}
  }
}

3.变量的线程安全分析

3.1.成员变量和静态变量是否线程安全?

(1)如果它们没有共享,则线程安全
(2)如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
①如果只有读操作,则线程安全
②如果有读写操作,则这段代码是临界区,需要考虑线程安全

3.2.局部变量是否线程安全?

(1)局部变量是线程安全的
(2)但局部变量引用的对象则未必
①如果该对象没有逃离方法的作用访问,它是线程安全的
②如果该对象逃离方法的作用范围,需要考虑线程安全

3.2.1 局部变量线程安全分析

public static void test1() {
 int i = 10;
 i++;
}

(1)每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。如图:
在这里插入图片描述
(2)局部变量引用的对象则稍有不同
①先看一个成员变量的例子

class ThreadUnsafe {
	ArrayList<String> list = new ArrayList<>();
 	public void method1(int loopNumber) {
 		for (int i = 0; i < loopNumber; i++) {
 			// { 临界区, 会产生竞态条件
 			method2();
 			method3();
  			// } 临界区
 		}
	 }
 	private void method2() {
 		list.add("1");
 	}
 	private void method3() {
 		list.remove(0);
 	}
}

执行
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
	at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 	at java.util.ArrayList.remove(ArrayList.java:496) 
 	at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 	at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 	at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 	at java.lang.Thread.run(Thread.java:748) 

分析:
无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
method3 与 method2 分析相同

②将 list 修改为局部变量那么就不会有上述问题了

class ThreadSafe {
 	public final void method1(int loopNumber) {
 		ArrayList<String> list = new ArrayList<>();
 		for (int i = 0; i < loopNumber; i++) {
 			method2(list);
 			method3(list);
	 }
 }
 	private void method2(ArrayList<String> list) {
 		list.add("1");
 	}
 	private void method3(ArrayList<String> list) {
 		list.remove(0);
 	}
}

分析:
list 是局部变量,每个线程调用时会创建其不同实例,没有共享
而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
method3 的参数分析与 method2 相同

4.Monitor

4.1 Java 对象头

(1)java的对象头由以下三部分组成:
①Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

在这里插入图片描述

②指向类的指针
该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Java对象的类数据保存在方法区。
③数组长度(只有数组对象才有)
只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。
(2)普通对象
在这里插入图片描述
(3)数组对象
在这里插入图片描述

4.2 Monitor概念

(1)Monitor被翻译为监视器或管程(由操作系统提供)
(2)每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就会被设置指向Monitor对象的指针
(3)Monitor的结构如下:
在这里插入图片描述
①刚开始Monitor中Owner为null
②当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
③在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
④Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时候是非公平的
注意:
①synchronized必须是进入同一个对象的monitor才有上述的效果
②不加synchronized的对象不会关联监视器,不遵从以上规则

5.synchronized原理

5.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized。例如:
在这里插入图片描述

  • 加锁
    (1)方法被调用时会产生一个栈帧,线程0执行到method1()的synchronized(obj)时会在线程的栈帧中创建锁记录(Lock Record)对象(该对象对我们是不可见的,是JVM层面的),每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    在这里插入图片描述

(2)让锁记录中的Object reference指向锁对象,尝试用cas把锁记录中的数据和锁对象中的Mark Word做一个交换,交换是为了表示加锁。
在这里插入图片描述

①如果cas替换成功,对象头中存储了锁记录地址状态00,表示由该线程给对象加锁,这时图示如下
在这里插入图片描述
②如果cas失败,有两种情况:
一种是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程;
另一种是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
在这里插入图片描述

  • 解锁
    (1)当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,重入计数减一
    在这里插入图片描述
    (2)当退出synchronized代码块(解锁时),锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
    ①成功,则解锁成功
    ②失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
在这里插入图片描述
(1)当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

在这里插入图片描述

(2)这时Thread-1加轻量级锁失败,进入锁膨胀流程。
①即为Object对象申请Monitor锁,让Object执行指向重量级锁地址。
②然后自己进入Monitor的EntryList BLOCKED
在这里插入图片描述
(3)当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

5.3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功,这时当前线程就可以避免阻塞。(自旋即让这个线程先不进入阻塞,而是进行几次循环,如果在循环的过程持锁线程已经退出了同步块释放了锁,就可以避免阻塞)
(1)自旋重试成功和失败的情况
①自旋重试成功
在这里插入图片描述
②自旋重试失败
在这里插入图片描述
(2)在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,是比较智能的。
(3)自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
(4)Java7之后不能控制是否开启自旋功能。

5.4 偏向锁

轻量级锁在没有竞争时(只有自己这个线程),每次重入仍然需要执行CAS操作。Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有
例如:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4.1 偏向状态

在这里插入图片描述
(1)一个对象创建时:
①如果开启了偏向锁(默认开启),那么对象创建后,markword值位0x05即最后3位为101,这时它的thread、epoch、age都为0
②偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数
-XX:BiasedLockingStartupDelay=0来禁用延迟
③如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
(2)禁用偏向锁
添加VM参数 -XX:-UseBiasedLocking禁用偏向锁

5.4.2 撤销偏向锁

5.4.2.1 撤销-调用对象hashCode

调用对象的hashCode()方法,会禁用掉偏向锁。因为如果处于偏向锁的对象头只能存线程ID,存不下哈希码了
在这里插入图片描述

5.4.2.3 撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

5.4.2.4 撤销- 调用wait/notify

只有重量级锁才有wait/notify方法

5.4.3 批量重偏向

(1)如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
(2)当撤销偏向随的阈值超过20次后,jvm会觉得是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程

5.4.4 批量撤销

当撤销偏向锁阈值超过40次后,jvm就会这样觉得,自己是不是偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

5.4.5 锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。

5.4.6 锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

6.wait/notify

6.1 wait/notify原理

在这里插入图片描述
(1)线程获取某个对象的Monitor锁,Owner线程发现条件不满足,调用wait方法,即可进入WaiSet变为WAITING状态
(2)BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
(3)BLOCKED线程会在Owner线程释放锁时唤醒
(4)WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

6.2 API介绍

  • obj.wait() 让已经进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 让object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

final static Object obj = new Object();
public static void main(String[] args) {
	new Thread(() -> {
 		synchronized (obj) { // 必须获得此对象的锁,才能调用API方法
 			log.debug("执行....");
 			try {
 				obj.wait(); // 让线程在obj上一直等待下去
 			} catch (InterruptedException e) {
 				e.printStackTrace();
 			}
 			log.debug("其它代码....");
		}
 	}).start();
 	new Thread(() -> {
 		synchronized (obj) {
 			log.debug("执行....");
 			try {
 				obj.wait(); // 让线程在obj上一直等待下去
 			} catch (InterruptedException e) {
 				e.printStackTrace();
 			}
 			log.debug("其它代码....");
 		}
 	}).start();
 	// 主线程两秒后执行
 	sleep(2);
 	log.debug("唤醒 obj 上其它线程");
 	synchronized (obj) {
 		obj.notify(); // 唤醒obj上一个线程
 		// obj.notifyAll(); // 唤醒obj上所有等待线程
	}
}

6.3 wait、notify 的正确使用

(1)sleep(long n) 和 wait(long n) 的区别
①sleep是 Thread 方法,而 wait 是 Object 的方法
②sleep不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
③sleep在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
④ 它们的状态都是TIMED_WAITING

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NIO学习总结(二)——Selector、FileLock、Path、Files、聊天室实现

一、Selector 1.1 Selector简介 1.1.1 Selector 和 Channel的关系 Selector 一般称为选择器 &#xff0c;也可以翻译为 多路复用器 。 它是 Java NIO 核心组件中的一个&#xff0c;用于检查一个或多个 NIO Channel&#xff08;通道&#xff09;的状态是否处于可读、可写。由…

ubuntu20.04环境搭建:etcd+patroni+pgbouncer+haproxy+keepalived的postgresql集群方案

搭建基于etcdpatronipgbouncerhaproxykeepalived的postgresql集群方案 宿主机操作系统:ubuntu20.04 使用kvm搭建虚拟环境(如没有安装kvm&#xff0c;请先自行安装kvm) 1、安装kvm服务 ①、查看虚拟支持 如果CPU 支持硬件虚拟化则输出结果大于0&#xff0c;安装kvm-ok命令检…

蓝桥省赛倒计时 35 天-双指针

双指针介绍 双指针算法是一种常用的算法技巧&#xff0c;它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作。 pointer 双指针并非真的用指针实现&#xff0c;一般用两个变量来表示下标&#xff08;在后面都用指针来表示&#xff09;。 双指针算法使用两个指针在数…

Android Gradle 开发与应用 (六) : 创建buildSrc插件和使用命令行创建Gradle插件

1. 前言 前文中&#xff0c;我们介绍了在Android中&#xff0c;如何基于Gradle 8.2&#xff0c;创建Gradle插件。这篇文章&#xff0c;我们以buildSrc的方式来创建Gradle插件。此外&#xff0c;还介绍一种用Cmd命令行的方式&#xff0c;来创建独立的Gradle插件的方式。 1.1 本…

第3集《天台教观纲宗》

乙二、约观行释 诸位法师慈悲&#xff01;陈会长慈悲&#xff01;诸位菩萨&#xff01;阿弥陀佛&#xff01; 请大家打开讲义第六页。我们看到乙二、约观行释。这一科是讲到天台教观的修学宗旨。 我们前面讲到&#xff0c;天台教观整个建立的过程&#xff0c;它是先有观法&a…

06 数据结构之树

引言&#xff1a; 数的代码实现&#xff0c; 先序遍历、中序、后序、层次遍历 /* binary_tree.h */ #ifndef _BINARY_TREE_H #define _BINARY_TREE_H#include <stdio.h> #include <stdlib.h> #include <string.h>#define DEBUG(msg) \printf("--%s--, %…

Tensorflow2.0+部署(tensorflow/serving)过程备忘记录Windows+Linux

Tensorflow2.0部署&#xff08;tensorflow/serving&#xff09;过程备忘记录 部署思路&#xff1a;采用Tensorflow自带的serving进模型部署&#xff0c;采用容器docker 1.首先安装docker 下载地址&#xff08;下载windows版本&#xff09;&#xff1a;https://desktop.docke…

python 蓝桥杯之动态规划入门

文章目录 DFS滑行&#xff08;DFS 记忆搜索&#xff09; 思路&#xff1a; 要思考回溯怎么写&#xff08;入参与返回值、递归到哪里&#xff0c;递归的边界和入口&#xff09; DFS 滑行&#xff08;DFS 记忆搜索&#xff09; 代码分析&#xff1a; 学会将输入的数据用二维列表…

WebMagic框架

1.webmagic框架 webmagic框架是一个Java实现的爬虫框架&#xff0c;底层依然是HttpClient和jsoup 组件&#xff1a; downloader&#xff1a;下载器组件PageProcessor&#xff1a;页面解析组件&#xff08;必须自定义&#xff09;scheculer&#xff1a;访问队列组件pipeline&am…

redis 性能优化一

目录 前言 尾延迟 前言 说到redis 性能优化&#xff0c;优化的目的是什么&#xff1f;提高响应&#xff0c;减少延迟。就要关注两点&#xff0c;一是尾延迟&#xff0c;二是Redis 的基线性能。只有指标&#xff0c;我们的优化&#xff0c;才有意义&#xff0c;才能做监控以及…

Java中常用的集合及方法(3)

1、List&#xff08;接上级--常用方法示例补充&#xff09; 1.4 常用的方法 1.4.2 LinkedList&#xff08;JDK8&#xff09; LinkedList是Java中一个实现了List接口和Deque接口的类&#xff0c;它采用链表结构存储数据&#xff0c;支持高效的插入和删除操作。 LinkedList中…

【C++】深度解剖多态

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;了解什么是多态&#xff0c;熟练掌握多态的定义&a…

NIO学习总结(一)——简介、Channel、Buffer

相关代码地址&#xff1a;nio_demo_learn: nio学习相关代码 (gitee.com) 一、BIO、NIO和AIO 1.1 阻塞IO&#xff08;BIO&#xff09; BIO即同步阻塞IO&#xff0c;实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时&#xff0c;服务器就…

分布式搜索elasticsearch

1.初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 例如&#xff1a; 在GitHub搜索代码 在电商网站搜索商品 在百度搜索答案…

MySQL主从读写分离之Proxysql(openEuler版)

实验目的&#xff1a; 基于proxysql实现MySQL的主从读写分离。 实验过程&#xff1a; 前期准备&#xff1a; 一共有四台虚拟机&#xff0c;其中三台为配置好的一主两从虚拟机&#xff0c;还有一台干净的虚拟机用来配置proxysql。 主机名地址master192.168.27.137node1192.…

NGINX源码安装详细配置文档

NGINX源码安装详细配置文档 一、基础Linux指令 查看nginx进程是否启动&#xff1a;ps -ef | grep nginx 关闭防火墙&#xff1a;systemctl stop firewalld 开放80端口&#xff1a;firewall-cmd --zonepublic --add-port80/tcp --permanent 关闭80端口&#xff1a;firewall-cmd …

(C语言)strcpy与strcpy详解,与模拟实现

目录 1. strcpy strcpy模拟实现&#xff1a; 实现方法1&#xff1a; 实现方法2&#xff1a; 2. strcat strcat模拟实现&#xff1a; 1. strcpy 作用&#xff1a;完成字符串的复制。 头文件&#xff1a;<string.h> destination是字符串要复制到的地点&#xff0c;s…

redis持久化-rdb

redis持久化-rdb策略 redis持久化rdb策略触发时机自动触发手动触发bgsave redis持久化 &#x1f680;我们知道redis是将数据存储在内存当中的&#xff0c;通常使用来作为关系型数据库的缓存使用的&#xff0c;以缓解当大量请求到来时关系型数据库的压力。 &#x1f680;既然数…

[LeetCode][226]翻转二叉树

题目 226. 翻转二叉树 给你一棵二叉树的根节点 root&#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 示例 2&#xff1a; 输入&#xff1a;root [2,1,3] 输出&#x…

025—pandas 根多列判断不在其他列的数据

思路 是有两个相同结构的数据表&#xff0c;已知第二个表是第一个表的部分数据&#xff0c;需要以其中两列为单位&#xff0c;判断在第一个表中存在&#xff0c;在另外一个表中不存在的数据。 思路&#xff1a; 我们先将 df1 和 df2 的 x、y 列取出&#xff0c;组合为元组形成…