JUC------共享模型------管程

概念

什么是管程

管程(Monitor,直译是”监视器“的意思)是一种操作系统中的同步机制,它的引入是为了解决多线程或多进程环境下的并发控制问题。
翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类支持并发访问,是线程安全的
参考: https://www.cnblogs.com/xidongyu/p/10891303.html

临界区

临界区 Critical Section

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

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量
    本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
    时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
    的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized

语法

synchronized(对象) // 线程1, 线程2(blocked)
{临界区
}
@Slf4j
public class SynchronizedTest1 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (SynchronizedTest1.class){count++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (SynchronizedTest1.class){count--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。
为了加深理解,请思考下面的问题

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
    答: 个人认为是锁住了整个循环,第二个循环只能等第一个循环执行完才能执行,执行顺序上相当于没有使用多线程,代码一行一行执行。
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
    答:有线程安全问题,相当于并没有锁。
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
    答: 有线程安全问题,相当于并没有锁。

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) {}}
}

问题:synchronized锁的到底是什么?
参考这个回答,自认为还是不错的

https://blog.csdn.net/YangYF1997/article/details/117164944?spm=1001.2101.3001.6650.9&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-117164944-blog-122815348.235%5Ev43%5Epc_blog_bottom_relevance_base9&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-117164944-blog-122815348.235%5Ev43%5Epc_blog_bottom_relevance_base9&utm_relevant_index=16

synchronized锁住同一个对象,线程才会互斥阻塞,才会线程安全。

线程八锁 练习

其实就是考察 synchronized 锁住的是哪个对象

  • 当synchronized修饰一个static方法时,多线程下,获取的是类锁(即Class本身,注意:不是实例),
    作用范围是整个静态方法,作用的对象是这个类的所有对象。
  • 当synchronized修饰一个非static方法时,多线程下,获取的是对象锁(即类的实例对象),
    作用范围是整个方法,作用对象是调用该方法的对象

----------------结论: 类锁和对象锁不同,它们之间不会产生互斥

一、

//结果是 先1后2 或者先2后1 
@Slf4j
public class Test1 {public static void main(String[] args) {Number n1 = new Number();new Thread(() -> {//n1对于a来讲就是thisn1.a();}, "t1").start();new Thread(() -> {//n1对于b来讲也是this 和 n1.a()中n1一样都是thisn1.b();}, "t2").start();}}@Slf4j
class Number {/***  public synchronized void a() {} 相当于 synchronized (this) {},所以其锁住的是number实例对象本身*/public synchronized void a() {log.debug("1");}public synchronized void b() {log.debug("2");}
}

二、

/*** 程序执行 1s后先打印1后打印2,或者程序先打印2,1s后打印1 * @author Spider Man* @date 2024-05-10 15:50*/
public class Test2 {public static void main(String[] args) {Number2 n2 = new Number2();new Thread(() -> {n2.a();}, "t1").start();new Thread(() -> {n2.b();}, "t2").start();}
}@Slf4j
class Number2 {public synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public synchronized void b() {log.debug("2");}
}

三、

/***       首先,该案例启用了3个线程,但是线程 t3 没有上锁,t1和t2共用一把锁,所以可以把其看成两个梯队,第一梯队两个线程,第二梯队一个线程,*   而且t3总是在第一梯队中,要么在第一梯队的第一个,要么在第一梯队在第二个*   所以其运行情况可分为三种:*      t3 t2 1s后t1      t2 t3 1s后t1    t3  1s后t1 t2*   这个案例主要在于 c() 方法没有上锁,所以不用排队总是会被第一次调用,* @author Spider Man* @date 2024-05-10 16:00*/
public class Test3 {public static void main(String[] args) {Number3 n3 = new Number3();new Thread(() -> {n3.a();}, "t1").start();new Thread(() -> {n3.b();}, "t2").start();new Thread(() -> {n3.c();}, "t3").start();}
}
@Slf4j
class Number3 {public synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public synchronized void b() {log.debug("2");}public void c(){log.debug("3");}
}

四、

/***  synchronized 是由不同的number对象加锁,所以两个线程不会阻塞互斥*  永远都是  t2 1s后t1* @author Spider Man* @date 2024-05-10 16:18*/
@Slf4j
public class Test4 {public static void main(String[] args) {Number4 n1 = new Number4();new Thread(() -> {n1.a();}, "t1").start();Number4 n2 = new Number4();new Thread(() -> {n2.b();}, "t2").start();}
}
@Slf4j
class Number4 {public synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public synchronized void b() {log.debug("2");}}

五、

/***  因为a()方法由static,所以其锁住的是类对象, b()方法是非static方法,锁住的是实例对象*  所以两个线程不是同一个锁,所以都是单独运行,但因为t1会先睡1s,所以视觉上总是t2先打印之后打印t1* @author Spider Man* @date 2024-05-10 17:14*/
public class Test5 {public static void main(String[] args) {Number5 n1 = new Number5();new Thread(() -> {n1.a();}, "t1").start();new Thread(() -> {n1.b();}, "t2").start();}
}@Slf4j
class Number5 {public static synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public synchronized void b() {log.debug("2");}}

六、

/*** 对类对象加锁,类对象整个内存只有一份,所以其用的锁是同一个对象* 所以其结果是 1s后 t1 t2 或者 t2 1s后t1* @author Spider Man* @date 2024-05-10 17:14*/
public class Test6 {public static void main(String[] args) {Number6 n1 = new Number6();new Thread(() -> {n1.a();}, "t1").start();new Thread(() -> {n1.b();}, "t2").start();}
}@Slf4j
class Number6 {// 对类对象加锁,类对象整个内存只有一份,所以其用的锁是同一个对象public static synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public static synchronized void b() {log.debug("2");}}

七、

/*** 两个线程不是同一个锁,所以都是单独运行,但因为t1会先睡1s,所以视觉上总是t2先打印之后打印t1* @author Spider Man* @date 2024-05-10 17:14*/
public class Test7 {public static void main(String[] args) {Number7 n1 = new Number7();Number7 n2 = new Number7();new Thread(() -> {n1.a();}, "t1").start();new Thread(() -> {n2.b();}, "t2").start();}
}@Slf4j
class Number7 {public static synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public synchronized void b() {log.debug("2");}}

八、

/*** 其结果是 1s后 t1 t2 或者 t2 1s后t1* @author Spider Man* @date 2024-05-10 17:14*/
public class Test8 {public static void main(String[] args) {Number8 n1 = new Number8();Number8 n2 = new Number8();new Thread(() -> {n1.a();}, "t1").start();new Thread(() -> {n2.b();}, "t2").start();}
}@Slf4j
class Number8 {public static synchronized void a() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");}public static synchronized void b() {log.debug("2");}}

线程安全分析

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

  • 成员变量
    有可能会发生线程安全。如果只有读操作,则线程安全,如果有读还有写,(临界区),就是线程不安全的。

  • 局部变量
    局部变量是线程安全的,因为它是线程私有的,不满足共享条件。

    原理是,每次方法调用对应着一个栈帧的创建,局部变量保存在栈帧的局部变量表中,而栈是线程私有的。

    但,局部变量引用的对象则不一定:
    解释一:
    如果该对象没有跨越方法的作用范围,那么它是线程安全的
    如果该对象跨越了方法的作用范围,它就不是线程安全的。

    解释二:
    如果该对象没有逃离方法的作用访问,它是线程安全的
    如果该对象逃离(return)方法的作用范围,需要考虑线程安全

    解释三:
    有一种情况就是子类继承父类,重写父类中的方法,在重写的方法中开一个线程去操作共享变量,这样就会有线程安全问题

    这个时候就要通过 private、final 这些修饰符去限制了

视频举例:https://www.bilibili.com/video/BV16J411h7Rd?p=66&vd_source=4085910f7c5c4dddcc04446ebf3aed6b

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

线程安全类方法的组合

比如:这就是线程安全的,两个线程调用同一个实例table的同一个方法put。

Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");
}).start();new Thread(()->{table.put("key", "value2");
}).start();

也就是说他们每个方法是原子的,被sychronized修饰,但多个方法的组合不是原子的,会发生线程安全问题。
比如:下面代码中hashtable的get方法和put方法虽然都是线程安全的,但是其是单个方法的原子性的线程安全,也就是说其源码中sychronized只单独修饰了put或get方法,但现在示例中两个线程的都用到了get和put,所以第一个线程get判断时,cpu很可能会把时间片分给了第二个线程,直到第二个线程运行完才把时间片分给第一个线程去执行最后的put方法。

public class HashTableTest2 {public static void main(String[] args) throws InterruptedException {Hashtable<String, String> hashtable = new Hashtable<>();Thread t1 = new Thread(() -> {if (hashtable.get("1")==null){hashtable.put("1", "1");}});Thread t2 = new Thread(() -> {if (hashtable.get("1")==null){hashtable.put("1", "2");}});t1.start();t2.start();t1.join();t2.join();System.out.println(hashtable);}
}

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
因为String 的 replace和substring 最终都是重新创建了个string 对象(new String)

线程安全—示例分析

前提知识:servlet在tomcat中只有一个实例
例1、

public class MyServlet extends HttpServlet {// 是否安全?  ---不安全Map<String,Object> map = new HashMap<>();// 是否安全?  --安全String S1 = "...";// 是否安全?  --安全final String S2 = "...";// 是否安全?  --不安全Date D1 = new Date();// 是否安全?  --不安全final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述变量}}

例2、
线程不安全,因为UserServiceImpl中成员变量count值被改变。

public class MyServlet extends HttpServlet {// 是否安全?private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}}public class UserServiceImpl implements UserService {// 记录调用次数private int count = 0;public void update() {// ...count++;} 
}

例3、
前提知识:
Spring默认使用单例模式管理Bean,简单来说就是在IoC容器中,默认情况下一个类只会存在一个它的实例,即对应的每个(标注了@Component等注解的)类只会被实例化一次。

答案:线程不安全,spring 中的对象默认是单例的,会被共享,所以针对MyAspect类中的start成员变量也是会被共享的,又因为before和after方法都对start作了写的操作,所以其是线程不安全的。

@Aspect
@Component
public class MyAspect {// 是否安全?private long start = 0L;@Before("execution(* *(..))")public void before() {//前置通知 记录时间start = System.nanoTime();}@After("execution(* *(..))")public void after() {//后置通知  记录时间long end = System.nanoTime();//求差值System.out.println("cost time:" + (end-start));}
}

例4、线程安全 三层结构中的典型调用

public class MyServlet extends HttpServlet {// 是否安全    --安全  UserServiceImpl 中 UserDao 成员变量没有被执行写的操作private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}}public class UserServiceImpl implements UserService {// 是否安全    --安全    UserDaoImpl中没有成员变量private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}}public class UserDaoImpl implements UserDao { public void update() {String sql = "update user set password = ? where username = ?";// 是否安全   --安全 因为其是局部变量,局部变量每个线程都会存一份在栈帧中try (Connection conn = DriverManager.getConnection("","","")){// ...} catch (Exception e) {// ...}}
}

例5、 线程不安全,根例4的区别在于userDao中Connection 在成员变量中

public class MyServlet extends HttpServlet {// 是否安全   --不安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService {// 是否安全  --不安全private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}
}public class UserDaoImpl implements UserDao {// 是否安全  不安全private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}

例6
线程安全,虽然UserDaoImpl 中 Connection 是成员变量,但因为UserServiceImpl 中的update方法每次调用都会创建一个新的UserDaoImpl对象,其是局部变量,所以是线程安全的

public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService { public void update() {UserDao userDao = new UserDaoImpl();  ------关键点userDao.update();}
}public class UserDaoImpl implements UserDao {// 是否安全  private Connection = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}

例7
线程不安全,其中 foo 的行为是不确定的(比如若有个类继承Test并重写其foo方法,在重写的方法中又新启了个线程对sdf进行操作,虽然SimpleDateFormat 是局部变量,但是其引用逃逸,泄露),可能导致不安全的发生,被称之为外星方法

public abstract class Test {public void bar() {// 是否安全SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");foo(sdf);}public abstract foo(SimpleDateFormat sdf);public static void main(String[] args) {new Test().bar();}
}public void foo(SimpleDateFormat sdf) {String dateStr = "1999-10-11 00:00:00";for (int i = 0; i < 20; i++) {new Thread(() -> {try {sdf.parse(dateStr);} catch (ParseException e) {e.printStackTrace();}}).start();}
}

例8、

虽然使用了 synchronized 关键字来保护对共享变量 i 的访问,但是这里对共享变量 i 的同步锁锁定的是 Integer 对象,而不是 i 的实际值。由于 Integer 是不可变对象,每次对 i 进行自增操作时,实际上是创建了一个新的 Integer 对象,因此每个线程获得的锁都是不同的对象,无法保证线程安全

@Slf4j
public class Test4 {private static Integer i = 0;public static void main(String[] args) {List<Thread> list = new ArrayList<>();for (int j = 0; j < 2; j++) {Thread thread = new Thread(() -> {for (int k = 0; k < 5000; k++) {synchronized (i) {i++;}}}, "" + j);list.add(thread);}list.stream().forEach(t -> t.start());list.stream().forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});log.debug("{}",i);}}

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

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

相关文章

在vue3中测试执行typescript代码片段

官方推荐的vitest方法 在vue3typescript项目中&#xff0c;一般来说用vite做构建系统。做测试的话&#xff0c;目前官方推荐的是vitest。官方文档在 https://vitest.dev/guide/ 在vue3typescriptvite5项目中&#xff0c;运行安装命令 npm install -D vitest 就可以把vites…

批量获取oracle库存储过程

场景:需要获取oracle库下的存储过程做备份 示例:获取单个存储过程,sql实现如下 SELECT owner,object_name,object_type FROM dba_objects; SELECT DBMS_LOB.SUBSTR(DBMS_METADATA.GET_DDL(FUNCTION,CONCATSTR,TEST)) FROM DUAL; CREATE OR REPLACE EDITIONABLE FUNCTION &quo…

【教学类-55-02】20240512图层顺序挑战(四格长条纸加黑色边框、4*4、7张 、43200张去掉非7色有23040张,去掉重复样式有几种?)

作品展示 背景需求&#xff1a; 之前的代码吗存在几个问题&#xff0c;最大的问题是不能生成“”长条黑边框”” 【教学类-55-01】20240511图层顺序挑战&#xff08;四格长条纸&#xff09;&#xff08;4*4&#xff09;和“手工纸自制参考图”-CSDN博客文章浏览阅读485次&…

使用Navicat将MySql数据库导入和导出

一&#xff0c;导出数据表 1.使用Navicat打开数据库&#xff0c;右键数据库&#xff0c;点击转储SQL文件&#xff0c;点击结构和数据。 2.选择生成文件的地方 3.等待生成完成 4.生成完成 二&#xff0c;导入数据库表和数据SQL文件 1.新建一个数据库 2.右键选择运行SQl文件 记…

OSPF协议1

OSPF开放式最短路径优先协议 1&#xff0c;OSPF协议因为其传递的是拓扑信息&#xff0c;之后&#xff0c;通过SPF算法将图形结构转换成为树形结构&#xff0c;所以&#xff0c;其计算出的路径不存在环路。并且&#xff0c;OSPF是使用带宽作为开销值的评判标准&#xff0c;所以…

如何利用甘特图来提高资源的是使用效率?

在项目管理中&#xff0c;甘特图是一种常用的工具&#xff0c;用于规划和跟踪项目进度。它通过条形图的形式展示项目的时间表和任务依赖关系&#xff0c;帮助项目经理和团队成员清晰地了解项目的时间线和进度。通过合理利用甘特图&#xff0c;可以显著提高资源的使用效率&#…

一文入门DNS

概述 DNS是一个缩写&#xff0c;可以代表Domain Name System&#xff0c;域名系统&#xff0c;是互联网的一项基础服务。也可以代表Domain Name Server&#xff0c;域名服务器&#xff0c;是进行域名和与之相对应的IP地址相互转换的服务器。DNS协议则是用来将域名转换为IP地址…

1W、2W 3KVAC隔离 宽电压输入 交直两用AC/DC 电源模块 ——TP01(02)AZ 系列

TP01(02)AZ为客户提供一款超小体积模块式开关电源&#xff0c;该系列模块电源输出功率为1W、2W&#xff0c;具有极低的空载损耗&#xff0c;低漏电流仅0.1mA&#xff0c;小体积&#xff0c;隔离耐压高达3KV等特点。产品安全可靠&#xff0c;EMC 性能好&#xff0c;EMC 及安全规…

Ubuntu安装Mysql数据库无法远程连接

1.远程端口未开 2.Mysql 数据库中user表 2.1 用户名root 的host字段味更改为% 允许远程访问&#xff1b; use mysql;SELECT user,host,plugin,authentication_string FROM user;userhostpluginauthentication_stringroot%auth_socketdebian-sys-maintlocalhostcaching_sha2_p…

树莓派配置双网卡分别为AD HOC和AP模式

树莓派配置双网卡分别为AD HOC和AP模式 需求说明&#xff1a;为了实现分级网络管理&#xff0c;将多个无人机分簇&#xff0c;簇间使用AD HOC进行无中心自组织的网络&#xff0c;簇内使用AP-AC模式进行中心化网络。因此&#xff0c;需要配置一台设备&#xff0c;同时完成AD HOC…

Quartz.Net(1)

Quartz 1 Quartz是一个强大的、开源的、轻量级的任务调度框架 Quartz官方文档 2 Quartz中有五个重要的概念 Scheduler 调度器Trigger 触发器Job 工作任务ThreadPool 线程池 &#xff0c;不是CLI的线程池&#xff0c;而是Quartz特有的线程池JobStrore 调度存储&#xff0c;存…

三、配置带HybridCLR的ARCore开发环境

预告 本专栏将介绍如何使用这个支持热更的AR开发插件&#xff0c;快速地开发AR应用。 专栏&#xff1a; Unity开发AR系列 插件简介 通过热更技术实现动态地加载AR场景&#xff0c;简化了AR开发流程&#xff0c;让用户可更多地关注Unity场景内容的制作。 “EnvInstaller…”支…

【eclipse】如何在IDE里创建一个Java Web项目?

如何在eclipse中创建一个动态Web项目并成功运行&#xff1f; 一、 最终效果 懒得写那么多了…我也不知道该怎么写了&#xff0c;有点乱&#xff0c;有问题可以在评论里留言&#xff0c;我看到会解决的&#xff0c;在这个过程中也踩到了一些坑&#xff0c;但好在有CSDN帮助解决…

【C++杂货铺】红黑树

目录 &#x1f308;前言&#x1f308; &#x1f4c1; 红黑树的概念 &#x1f4c1; 红黑树的性质 &#x1f4c1; 红黑树节点的定义 &#x1f4c1; 红黑树的插入操作 &#x1f4c1; 红黑树和AVL树的比较 &#x1f4c1; 全代码展示 &#x1f4c1; 总结 &#x1f308;前言…

文章模版--测试

学习目标&#xff1a; 提示&#xff1a;这里可以添加学习目标 例如&#xff1a; 一周掌握 Java 入门知识 学习内容&#xff1a; 提示&#xff1a;这里可以添加要学的内容 例如&#xff1a; 搭建 Java 开发环境掌握 Java 基本语法掌握条件语句掌握循环语句 学习时间&#…

四川汇昌联信:拼多多网点怎么开?大概需要多少钱?

想要开一家拼多多网点&#xff0c;你肯定很关心需要准备多少资金。下面&#xff0c;我们就来详细解答这个问题&#xff0c;并从多个角度分析开设网点的要点。 一、 开设拼多多网点&#xff0c;首要任务是确定启动资金。根据不同的经营模式和地区差异&#xff0c;成本会有所不同…

WIFI模块的AT指令联网数据交互--第十天

1.1.蓝牙&#xff0c;ESP-01s&#xff0c;Zigbee, NB-Iot等通信模块都是基于AT指令的设计 初始配置和验证 ESP-01s出厂波特率正常是115200, 注意&#xff1a;AT指令&#xff0c;控制类都要加回车&#xff0c;数据传输时不加回车 1.2.上电后&#xff0c;通过串口输出一串系统…

【面试经典题】环形链表

个人主页&#xff1a;一代… 个人专栏&#xff1a;数据结构 在面试中我们经常会遇到有关链表的相关题目&#xff0c;面试官通常会对题目给出拓展 下面我就两个leetcode上的一个双指针的题目为例&#xff0c;并对其进行拓展 题目链接&#xff1a;环形链表 题目描述&#xf…

145.二叉树的后序遍历

刷算法题&#xff1a; 第一遍&#xff1a;1.看5分钟&#xff0c;没思路看题解 2.通过题解改进自己的解法&#xff0c;并且要写每行的注释以及自己的思路。 3.思考自己做到了题解的哪一步&#xff0c;下次怎么才能做对(总结方法) 4.整理到自己的自媒体平台。 5.再刷重复的类…

深入理解前端性能优化:从网络到渲染

1. 网络优化 减少HTTP请求 合并资源&#xff1a;通过合并CSS和JavaScript文件减少请求次数。资源内联&#xff1a;对于小的CSS和JavaScript&#xff0c;直接内联到HTML中。 使用HTTP/2 HTTP/2支持多路复用&#xff0c;减少请求阻塞&#xff0c;提高加载速度。 开启GZIP压缩…