多线程-线程安全

news2024/12/28 17:59:55

目录

线程安全问题

加锁(synchronized)

synchronized 使用方法

synchronized的其他使用方法

synchronized 重要特性(可重入的)

死锁的问题

对 2> 提出问题

对 3> 提出问题

 解决死锁

对 2> 进行解答

对4> 进行解答

volatile 关键字

wait 和 notify (重要)

wait使用实例:

notify使用实例:

" 线程饿死 "

notify 和 notifyAll

小结


线程安全问题

线程安全问题: 有些代码在单个线程环境下执行完全正确. 但是如果同样的代码让多个线程同时执行, 此时就可能出现 bug, 这种情况叫做 "线程安全问题" / "线程不安全", 它是多线程中最复杂, 最重要的部分

举个例子: 两个线程, 每个线程count++ 5000次, 正常情况下结果为 10w, 实际结果:如下图:

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        t1.start();
        t2.start();
        //如果没有这俩 join , 肯定不行, 线程还没有自增完毕, 就开始打印了,
        //打印出来的count 可能是 0;
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

改变一下 join 的次序可以让结果输出正确, 如下代码:

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

这个代码意味着 t1 执行时 t2并不会启动, 虽然上述代码卸载两个线程中, 但并不是同时执行的, 而第一个代码中 t1 和 t2 同时执行了, 第二个代码结果输出正确, 为 10w, 我们可以猜测是因为两个线程同时执行的原因导致第一个结果出错了.

解释:

count++ 这个操作本质上是分三步进行的 ~~ 站在 cpu 的角度, 是 cpu 通过三个指令实现的

1> load 把数据从内存读到 CPU 寄存器中

2> add 把寄存器中的数据进行 +1

3> sava 把寄存器中的数据保存到内存中

由于多个线程执行上述代码, 由于线程之间的调度顺序是 "随机" 的, 就会导致有些调度顺序下, 上述的逻辑就会出现问题.

如图:

这只是其中的一种情况, 还可能有无数种情况, 这三个步骤的排列顺序有很多种了, 还有可能 t1 连续执行了多次, 然后 t2 再次执行的情况, 有无数种排列顺序.

我们意识到在多线程程序中最困难的一点是: 现成的随机调度, 是两个线程执行逻辑的先后顺序存在很多可能, 我们要做的是保证在每一种情况下都输出正确的结果.

举个例子看一下: 不同的情况怎么输出结果的:

理想情况下:

两次相加后, 最终可以输出2

可能会出现的情况:

这种情况下两次相加得到的结果为 1

因为线程调度是随机的, 很容易出现错误情况, 这样的话最终的结果是一个随机值, 随机值小于 10w.

产生线程安全的原因:

1> 操作系统中, 线程的调度顺序是随机的 (抢占式执行)

2> 两个线程针对同一个变量进行修改

3> 修改操作不是原子的

此处给定的 count++ 就属于是非原子的操作, 先读取, 在修改, 有三个指令

4> 内存可见性问题

5> 指令重排序问题

 如何解决这个问题呢? 从这些原因入手

1> 调度随机性在系统内核里实现的, 最早的操作系统奠定了这个基调, 无能为力.

2> 有些情况可以通过调整代码结构来规避在这个问题, 有些情况规避不了

3> 有办法让 count++ 三步走成为 "原子" 的   ---->  (加锁) 的方法

加锁(synchronized)

synchronized 使用方法

需要搭配一个代码块 {   } 使用, 进入  {  就会加锁, 出去  }  就会解锁

作用

在已经加锁的状态下, 另一个线程尝试同样加这个锁, 就会产生 "锁冲突/锁竞争", 后一个线程就会阻塞等待, 一直等到前一个线程解锁为止.

使用方法举例

用上述代码进行举例:

count++ 加在代码块中, 然后 synchronized()  这个后面的 () 需要表示一个用来加锁的对象, 这个对象是啥不重要, 重要的是通过这个对象来区分两个线程是否在竞争同一个锁, 如果两个线程是针对同一个对象加锁, 就会有锁竞争, 反之不会有锁竞争, 仍然是并发执行.

追妹子: 你想妹子表白, 成功了就相当于加锁了, 另一个小哥准备追同一个妹子, 就得阻塞等待, 等你俩分手了他才有机会, 如果他准备追另一个对象, 那么可以直接表白.

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //我们任意定义一把锁
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                //进行加锁
                synchronized (locker) {  
                    count++;
                    i++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                //进行加锁
                synchronized (locker) {
                    count++;
                    i++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

 运行结果正确了.

加过锁之后两个线程相互影响, 在进行 count++ 时会先加锁, t1 线程加过锁了, t1没执行完之前, t2 操作会出现阻塞. 只有当 t1 中 count++ 的操作执行完之后才会让 t2 中的 count++ 进行操作, 这就避免了 t1 中的 load add save 与 t2 中的 load add save 操作 出行穿插, 此时线程安全问题就迎刃而解了.

如果在 两个线程加锁时 使用不同的 锁 那么就不会出现锁竞争, 上述问题就不会解决

其中synchronized 后面 () 中的锁对象到底是哪个对象无所谓, 重要的是俩线程加锁的对象是否是同一个对象.

synchronized的其他使用方法

synchronized 还可以修饰 一个方法

class Counter {
    public int count;
    synchronized public void increase() {
        count++;
    }
    public void increase2() {
        synchronized (this) {
            count++;
        }
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(counter.count);
    }
}

两种的写法是一样的, 上面是下面的简化版本

synchronized还可以修饰一个 静态方法

两种的写法是一样的, 上面代码是下面代码的简化版本

其中 Counter.class 是类对象

.java => .class => JVA加载到内存中(类对象) 可以看作是 .Java 文件中的二进制码

类对象中包含以下内容: 

1> 类的属性有哪些, 名字, 类型, 权限

2> 类的方法有哪些, 名字, 类型, 权限

3> 类本身继承自哪个类, 实现了哪些接口

在一个 Java 进程中, 类对象是唯一的.

synchronized 用的锁是存在 Java 对象头里的

Java 的一个对象, 对应的内存空间中, 除了你自己定义的一些属性之外, 还有一些自带的属性, 在对象头中, 其中就有属性表示当前的对象是否已经加锁.

synchronized 重要特性(可重入的)

可重入定义: 一个线程连续针对一把锁加锁两次, 不会出现死锁, 满足这个要求就是 "可重入" , 不满足就是 "不可重入" .

 死锁的解释: 有一个线程 t , 锁对象 locker,  t 线程中存在下列代码:

synchronized (locker) {

          synchronized (locker) {

                 ........... 

         }

}

第一次加锁能够加锁成功, 此时 locker 属于 "被锁定" 的状态, 第二次加锁 locker 已经是锁定状态, 第二次加锁操作, 应该要 "阻塞等待" 的, 等到锁被释放之后才能加锁成功

第二次想要加锁成功, 需要第一次加锁释放锁, 释放锁就要第二次加锁成功

这样就出现了死锁现象. 就是一个bug, 可能会出现这种情况

    private static Object locker = new Object();
    public static void func1() {
        synchronized (locker) {
            func2();
        }
    }
    public static void func2() {
        func3();
    }
    public static void func3() {
        func1();
    }
    public static void func4() {
        synchronized (locker) {
        }
    }

 这种bug时常出现而且不容易发现.

问题是: 上述代码中, synchronized 是可重入锁, 没有因为第二次加锁而死锁, 加入上述加锁过程有 N 层, 释放时机该如何判定? 

解答: 此处无论有多少层锁, 都是到在最外层才能释放锁, 提前释放会线程不安全

引用计数: 锁对象中不但要记录谁拿到了锁, 还要记录锁被加了几次, 每加锁一次, 计数器 +1, 每解锁一次, 计数器 -1, 除了最后一个大括号恰好减成 0 , 才真正释放锁.

死锁的问题

关于死锁总结: 

1> 一个线程针对一把锁, 连续加锁两次, 如果是不可重入锁, 就死锁了. (synchronized 不会出现)

2> 两个线程, 两把锁 (此时无论是不是可重入锁, 都会死锁)

 t1  t2 两个线程,  A 和 B 两把锁

t1 获取锁 A, t2 获取锁 B,  t1 尝试获取B, t2 尝试获取 A. 这种情况出现死锁.

3> N 个线程, M 把锁 (相当于 2> 的扩充) 更容易出现死锁的情况了, 经典模型: 哲学家就餐问题.

对 2> 提出问题

public class Test {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁
                // 之后再进行动作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t1 加锁成功!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t2 加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

此时的代码会出现死锁情况, 什么都没法打印出来

 此时打开 jconsole (java带的一个查看线程情况的工具) 就可以看到这俩线程的状态:

当前线程出现了阻塞状态;

对 3> 提出问题

死锁是属于很严重的 bug (导致线程卡住, 无法执行后续工作)

死锁的成因涉及到四个 必要条件

1> 互斥使用 (锁的基本特性) 当一个线程持有一把锁之后, 另一个线程也想获取到锁, 就要阻塞等待

2> 不可抢占 (锁的基本特性) 当锁已经被线程 1 得到之后, 线程2 只能等线程1 主动释放出来, 不能强行抢过来

3> 请求保持 (代码结构) 一个线程尝试获取多把锁, (先拿到锁1 之后, 在尝试获取锁2 的时候锁1 不会释放, 就是的上面的2> 例子

4> 循环等待/环路等待  (代码结构) 等待的依赖关系形成环了, 上面的3> 哲学家就餐的例子

 解决死锁

如何解决/避免死锁呢?

核心是破坏上述必要条件

1> 和 2> 是锁的特性, 不能改变, 要从 3> 和 4> 着手

3> 来说, 调整代码结构, 避免编写 "锁嵌套" 逻辑,  当然这个方案不一定好使, 有的需求可能就是需要获取多个锁之后再操作

4> 通过约定加锁顺序, 就可以避免循环的等待

对 2> 进行解答

调整代码结构, 避免编写 "锁嵌套" 逻辑

public class Test {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁
                // 之后再进行动作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker2) {
                System.out.println("t1 加锁成功!");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker1) {
                System.out.println("t2 加锁成功!");
            }
        });
        t1.start();
        t2.start();
    }
}

这样就能正常输出结果了.

对4> 进行解答

通过约定加锁顺序, 就可以避免循环的等待

针对锁进行编号, 比如约定 加多把锁的时候先加编号小的锁, 后加编号大的锁.(所有线程都遵守这个规则) 举个例子如下图:

最终死锁问题迎刃而解, 本质上是破除了循环等待

synchronized 使用规则并不复杂, 抓住一个原则, 两个线程针对同一个对象加锁, 就会产生锁竞争.

volatile 关键字

作用: 

1> 保证内存可见性

2> 禁止指令重排序

1>什么是 内存可见性

计算机运行的程序/代码, 经常要访问数据, 这些依赖的数据往往存在在 内存中, (定义一个变量, 变量就是存在内存中), CPU 使用这个变量的时候, 就会把这个内存中的数据先读出来, 放到 CPU 的寄存器中在参与运算 (load)

CPU 读内存 相当于 读硬盘 快几千上万倍, 读寄存器 相比于 读内存 又快了几千上万倍, 为了提高效率, 编译器把代码做出优化, 把一些本来要读内存的操作, 优化成读其寄存器, 减少读内存的次数, 也就可以提高整体程序的效率了

举个例子:

public class Test {
    private static int isQuit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(isQuit == 0) {

            }
            System.out.println("t1 退出");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            System.out.println("请输入 isQuit: ");
            Scanner sc = new Scanner(System.in);
            isQuit = sc.nextInt();
        });
        t2.start();
    }
}

当我们输入 isQuti == 1 或者其他不为零的数时程序应该停止运行, 但结果不是. 打开 jconsole 可以看到 t1 线程正在执行, 为 RUNNABLE 状态

之前是两个线程修改用一个变量会引起线程安全问题, 现在是一个线程读, 一个线程修改也有可能会有问题, 就是因为 内存可见性引起的

 

使用方法:

在 isQuit 变量加上 volatile限制就可以了

关于内存可见性还涉及到一个关键概念, JMM(Java Memory Model, Java 内存模型) Java规范文档的叫法.

JMM 把存储空间分为 主内存 和工作内存, t1线程对应 isQuit 变量, 本身是在 主内存中的, 由于此处的优化就会把 isQuit 变量放到工作内存中. 进一步的 t2 修改主内存的 isQuit, 不会影响到 t1 的工作内存. 主内存就是咱们平常说的内存, 工作内存就是 CPU 寄存器.

volatile 可以保证内存可见性, 但是不能保证原子性

wait 和 notify (重要)

wait 和 notify 都是 Object 方法, 随便定义一个对象, 都可以使用 wait notify .它俩需要配合使用

作用: 用来协调多个线程的执行顺序

本身多个线程的执行顺序是随机的 (系统随即调度, 抢占式执行) 很多时候希望通过一定手段, 协调执行顺序. join 使用像线程结束的先后顺序, 相比之下此处是希望线程不结束, 也能够有先后顺序的控制.

wait 等待:  让指定线程进入阻塞状态

notify 通知:  唤醒对应的阻塞状态的线程

wait如何使用

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");
        object.wait();
        System.out.println("wait 之后");
    }
}

此处代码报错, 非法的 监视器 状态 异常 , 其中 synchronized 就是监视器锁. 

wait 操作在执行的时候要做 三件事

1> 释放当前的锁

2> 让线程进入阻塞

3> 线程被唤醒的时候重新获取到锁

通过object 调用wait 释放锁的过程. 释放锁的前提就是 先加锁

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("wait 之前");
            //将 wait 放到 synchronized 里面调用, 保证确实是拿到了锁
            object.wait();
            System.out.println("wait 之后");
        }
    }
}

应该把 wait 写到 synchronized 里面加锁, 此时可以运行代码, 但是wait 会持续阻塞等待下去, 直到其他线程调用 notify 唤醒. 

此处的状态就是 waiting 状态

wait 除了默认的无参版本之外, 还有一个带参数的版本, 带参数的版本就是指定一个时间

参数的版本就是指定超时时间, 避免 wait 无休止地等待下去.

notify如何使用

public class Test {
    public static void main(String[] args){
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                System.out.println("wait 之前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object) {
                System.out.println("进行通知");
                object.notify();
            }
        });
        t1.start();
        t2.start();
    }
}

 

结果如图.

" 线程饿死 "

针对这种情况:可以使用 wait 和 notify 来解决

让 1 号老铁在发现没钱的时候就进行 wait.(wait 内部本身就会释放锁, 并且进入阻塞)

让 1 号老铁不再进行后续的锁竞争, 把所释放出来让别人获取. 给其他老铁提供机会

运钞车把钱运过来的线程就是调用 notify 唤醒的线程

notify 和 notifyAll

notify 一次唤醒一个线程

notifyAll 一次唤醒全部线程

调用 wait 不一定只有一个线程调用, N 个线程都可以调用 wait, 此时有多个线程调用的时候这些线程都会进入阻塞状态., 唤醒的时候就有两种方式了

nitifyAll: 唤醒的时候 wait 涉及到一个重新获取锁的过程, 需要串行执行(用的更少)

notify: 更可控(用的更多)

小结

保证线程安全需要 : 保证原子性, 可见性, 顺序性:

了解synchronized 和 wait notify , volatile 的语法, 目的

掌握死锁的几种情况, 及如何解决死锁问题.

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

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

相关文章

线下研讨会 技术沙龙|乐鑫芯片与 ESP RainMaker® 为科技初创企业赋能

众多科技初创企业在智能硬件市场迅猛发展的背景下&#xff0c;对不断变化的需求展现出了高度的敏锐性&#xff0c;期望能够快速将其转化为切实的产品方案。然而&#xff0c;面对复杂繁重的软硬件集成任务&#xff0c;这些企业往往容易陷入研发瓶颈、资金短缺以及效率低下等多重…

Mybatis技术内幕-基础支撑层

整体架构 MyBatis 的整体架构分为三层&#xff0c; 分别是基础支持层、核心处理层和接口层。 基础支持层 基础支持层包含整个MyBatis 的基础模块&#xff0c;这些模块为核心处理层的功能提供了良好的支撑。 解析器模块 XPathParser MyBatis提供的XPathParser 类封装了XPat…

HackMyVM-Minimal

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 gobuster 文件包含漏洞 提权 web信息收集 main方法 question_1 question_2 question_3 prize.txt 软连接 信息收集 arp ┌──(root?0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: E…

centos7.9系统安全加固

1、限制用户登陆 vim /etc/hosts.deny&#xff0c;若禁止192.168.0.158对服务器进行ssh的登陆&#xff0c;添加如下内容 sshd : 192.168.0.158 添加完毕后就生效了&#xff0c;直接用192.168.0.158访问主机&#xff0c;就无法连接了&#xff0c;显示 Connection closing...Soc…

pycharm报错Process finished with exit code -1073740791 (0xC0000409)

pycharm报错Process finished with exit code -1073740791 (0xC0000409) 各种垃圾文章&#xff08;包括chatgpt产生的垃圾文章&#xff09;&#xff0c;没有给出具体的解决办法。 解决办法就是把具体报错信息显示出来&#xff0c;然后再去查。 勾选 然后再运行就能把错误显示…

图像分割各种算子算法-可直接使用(Canny、Roberts、Sobel)

Canny算子&#xff1a; import numpy as np import cv2 as cv from matplotlib import pyplot as pltimg cv.imread("../test_1_1.png") edges cv.Canny(img, 100, 200)plt.subplot(121),plt.imshow(img,cmap gray) plt.title(Original Image), plt.xticks([]), …

vue2+swiper——实现多图轮播+层叠轮播——技能提升

今天看到同事在写轮播图&#xff0c;由于是jq的写法&#xff0c;我没有过多参与&#xff0c;我只写vue的部分。。。虽然语言不一样&#xff0c;但是用法还是要会的。下面介绍通过swiper组件来实现轮播效果。 解决步骤1&#xff1a;安装swiper npm install swiper5.4.5 我这边…

数据分享—全国分省河流水系

河流水系数据是日常研究中必备的数据之一&#xff0c;本期推文主要分享全国分省份的水系和河流数据&#xff0c;梧桐君会不定期的更新数据&#xff0c;欢迎长期订阅。 数据预览 山东省河流水系 吉林省河流水系 四川省河流水系 数据获取方式 链接&#xff1a;https://pan.baidu.…

基于阿里云向量检索 Milvus 版与 PAI 搭建高效的检索增强生成(RAG)系统

阿里云向量检索 Milvus 版现已无缝集成于阿里云 PAI 平台&#xff0c;一站式赋能用户构建高性能的检索增强生成&#xff08;RAG&#xff09;系统。您可以利用 Milvus 作为向量数据的实时存储与检索核心&#xff0c;高效结合 PAI 和 LangChain 技术栈&#xff0c;实现从理论到实…

网络基础(三)——网络层

目录 IP协议 1、基本概念 2、协议头格式 2.1、报头和载荷如何有效分离 2.2、如果超过了MAC的规定&#xff0c;IP应该如何做呢&#xff1f; 2.3、分片会有什么影响 3、网段划分 4、特殊的ip地址 5、ip地址的数量限制 6、私有ip地址和公网ip地址 7、路由 IP协议 网络…

LINUX 精通 1——2.1.1 网络io与io多路复用select/poll/epoll

LINUX 精通 1 day12 20240509 算法刷题&#xff1a; 2道高精度 耗时 107min 课程补20240430 耗时&#xff1a;99 min day 13 20240512 耗时&#xff1a;200min 课程链接地址 前言 杂 工作5-10年 够用 费曼&#xff1a;不要直接抄&#xff0c;自己写&#xff1b;不要一个…

【微服务】spring aop实现接口参数变更前后对比和日志记录

目录 一、前言 二、spring aop概述 2.1 什么是spring aop 2.2 spring aop特点 2.3 spring aop应用场景 三、spring aop处理通用日志场景 3.1 系统日志类型 3.2 微服务场景下通用日志记录解决方案 3.2.1 手动记录 3.2.2 异步队列es 3.2.3 使用过滤器或拦截器 3.2.4 使…

安全工程师面试题

安全工程师面试题安全工程师是一个非常重要的职位&#xff0c;他们负责保护公司的网络和系统免受黑客和恶意软件的攻击。如果你想成为一名安全工程师&#xff0c;那么你需要准备好面试。下面是一… 1安全工程师面试题 安全工程师是一个非常重要的职位&#xff0c;他们负责保护…

【全开源】Java俱乐部系统社区论坛商城系统源码-奔驰奥迪保时捷大众宝马等汽车俱乐部

特色功能&#xff1a; 会员中心&#xff1a;会员中心可以帮助企业更好地管理客户&#xff0c;包括设置积分商城、会员卡充值、个人汽车档案等功能&#xff0c;对不同的会员群体展开有针对性的营销&#xff0c;并维护和积累自己的粉丝群体。信息服务&#xff1a;负责定期发布新…

后端项目开发笔记

Maven打包与JDK版本不对应解决方法 我这里使用jdk8。 <build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configurat…

【Docker】Ubunru下Docker的基本使用方法与常用命令总结

【Docker】docker的基本使用方法 镜像image与容器container的关系基本命令- 查看 Docker 版本- 拉取镜像- 查看系统中的镜像- 删除某个镜像- 列出当前 Docker 主机上的所有容器&#xff0c;包括正在运行的、暂停的、已停止的&#xff0c;以及未运行的容器- 列出当前 Docker 主机…

day05-面向对象内存原理和数组

day05 面向对象内存原理和数组 我们在之前已经学习过创建对象了,那么在底层中他是如何运行的。 1.对象内存图 1.1 Java 内存分配 Java 程序在运行时&#xff0c;需要在内存中分配空间。为了提高运算效率&#xff0c;就对空间进行了不同区域的划分&#xff0c;因为每一片区域…

leetcode——反转链表

206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;创建三个指针n1,n2,n3&#xff0c;遍历原链表&#xff0c;通过三者之间的关系将链表反转。下面给出图示&#xff1a; 下面给出题解代码&#xff1a; typedef struct ListNode ListNode; struct List…

C++入门指南(上)

目录 ​编辑 一、祖师爷画像 二、什么是C 三、C发展史 四、C在工作领域的应用 1. 操作系统以及大型系统软件开发 2. 服务器端开发 3. 游戏开发 4. 嵌入式和物联网领域 5. 数字图像处理 6. 人工智能 7. 分布式应用 五、如何快速上手C 一、祖师爷画像 本贾尼斯特劳斯…

vmware虚拟机内删除文件后宿主机空间不释放

问题描述 linux下&#xff0c;vmware内虚拟机删除文件&#xff0c;宿主机空间不释放&#xff0c;D盘快满了 解决方法 通过vmware-toolbox进行空间回收 安装 在虚拟机内操作 yum install -y open-vm-tools 清理 在虚拟机内操作 #查看磁盘的挂载点 sudo /usr/bin/vmware…