【JavaEE初阶】落霞与孤鹜齐飞,秋水共长天一色 - (重点)线程

news2024/12/27 8:18:21


在这里插入图片描述

本篇博客给大家带来的是线程的知识点, 由于时间有限, 分三天来写, 本篇为线程第二篇.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .

王子,公主请阅

  • 1. Thread类的常见方法
    • 1. 等待线程 - join()
    • 1.2 获取当前线程引用
    • 1.3 休眠当前线程
  • 2. 线程的状态
    • 2.1 线程的所有状态
  • 3. 多线程带来的的风险-线程安全 (重点)
    • 3.1 观察线程安全
    • 3.2 线程安全的概念
    • 3.3 线程不安全的原因
      • 3.3.1 操作系统中,线程的调度顺序是随机的
      • 3.3.2 修改数据共享
      • 3.3.3 修改操作不是原子性的
      • 3.3.4 内存可见性
      • 3.3.5 指令重排序
  • 4. synchronized关键字 - 监视锁monitor lock
    • 4.1 synchronized的特性
      • 4.1.1 可重入
      • 4.1.2 互斥
      • 4.1.3 多个线程,N把锁.即哲学家就餐问题 (重点)
      • 4.1.4 死锁的成因(重点)
    • 4.2 synchronized 使用示例
      • 4.2.1 修饰代码块
      • 4.2.2 synchronized 修饰普通方法
      • 4.2.3 修饰静态方法
    • 4.3 Java 标准库中的线程安全类
  • 5. volatile 关键字
    • 5.1 volatile能保证内存可见性
    • 5.2 volatile不保证原子性
  • 6. wait和notify
    • 6.1 wait()方法
    • 6.2 notify()方法
    • 6.3 notifyAll()方法
    • 6.4 wait 和 sleep的区别(面试题)

1. Thread类的常见方法

1. 等待线程 - join()

有时,我们需要等待⼀个线程完成它的工作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个方法明确等待线程的结束。

t.join(); 表示等待 t 线程结束

//join 实现线程等待效果
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t 线程工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        //让主线程来等待t线程执行结束.
        //一旦调用join, 主线程就会触发阻塞, 此时t线程可以趁机完成后续的工作
        //一直阻塞到 t 执行结束完毕了, join 才会解除阻塞, 才能继续执行.

        System.out.println("join 等待开始.");
        t.join(); //主线程等待 t 线程结束
        System.out.println("join 等待结束.");
    }
}

在这里插入图片描述

1.2 获取当前线程引用

在这里插入图片描述

public class test6 {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
    }
}

1.3 休眠当前线程

因为线程的调度是不可控的,所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

在这里插入图片描述

public class test6 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3*1000);
        System.out.println(System.currentTimeMillis());
    }
}

2. 线程的状态

2.1 线程的所有状态

线程的状态是⼀个枚举类型 Thread.State

public class test6 {
    public static void main(String[] args) {
        for(Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

• NEW: 安排了工作, 还未开始行动. 即Thread对象已经有了, 但start方法还没调用.
• RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 就是之前说的就绪状态.
• TERMINATED: 工作完成了. Thread对象还在, 内核中的线程已经没了.
• BLOCKED: 都表示排队等着其他事情. 由于锁竞争导致的阻塞.
• WAITING: 都表示排队等着其他事情. 由于wait这种没有固定时间的方式产生的阻塞.
• TIMED_WAITING: 表示排队等着其他事情. 由于sleep这种固定时间的方式产生的阻塞.


在这里插入图片描述

把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为⼀些事情需要去忙,例如需要填写信息、回家取证件、发呆⼀会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,以后再详解;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

观察 1: 关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换

public class test6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {

            }
        },"李四");
        System.out.println(t.getName() + ": " + t.getState());
        t.start();
        while(t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());
        }
        System.out.println(t.getName() + ": " + t.getState());
    }
}

观察 2: 关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换

public class test7 {
    public static void main(String[] args) {
        final Object object = new Object();

        Thread t1 = new Thread(() -> {
            synchronized(object) {
                while(true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            synchronized(object) {
                System.out.println("嘻嘻嘻");
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

在这里插入图片描述
在这里插入图片描述
使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

修改上面的代码, 把 t1 中的 sleep 换成 wait, 使用 jconsole 可以看到 t1 的状态是 WAITING

public class test7 {
    public static void main(String[] args) {
        final Object object = new Object();

        Thread t1 = new Thread(() -> {
            synchronized(object) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            synchronized(object) {
                System.out.println("嘻嘻嘻");
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

在这里插入图片描述

结论:
• BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
• TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒.

3. 多线程带来的的风险-线程安全 (重点)

3.1 观察线程安全

public class test7 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();

        //如果没有join方法肯定不行, 线程还没自增完就开始打印了, 很可能打印出来的count是 0;
        t1.join();
        t2.join();
        System.out.println("count: " + count);
    }
}

上述代码的预期结果为100000, 可无论怎么运行达不到预期结果, 这就是一个线程安全问题.

3.2 线程安全的概念

我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境下的结果,则说这个程序是线程安全的。

3.3 线程不安全的原因

3.3.1 操作系统中,线程的调度顺序是随机的

这是线程安全问题的罪魁祸首, 随机调度使一个程序在多线程环境下,执行顺序存在很多的变数. 程序员必须保证在任意执行顺序下, 代码都能正常执行.

3.3.2 修改数据共享

多个线程修改同一个变量, 上面的线程不安全的代码中, 涉及到多个线程针对 count 变量进行修改.此时这个 count 是一个多个线程都能访问到的 “共享数据”

3.3.3 修改操作不是原子性的

什么是原子性呢?
我们把一段代码想象成一个房间, 每个线程就是要进入房间里的人. 如果没有任何的保护机制, 当A进入房间后, 还没有出来; B是不是也可以进入房间,打断A在房间里的隐私. 这个就是不具备原子性的.
有时也把这个现象叫做同步互斥.
那么其实解决这个问题的关键就是给房间里加上一把锁, A进入房间后,把门锁上自然就不会被打断了. 这样就保证了代码的原子性.
一条 java 语句不一定是原子的,也不一定只是一条指令

比如: 上述代码中的count++, 其实是由CPU中的三条指令来实现的.

1. load 把数据从内存读到CPU寄存器
2. add 把寄存器中的数据+1
3. save 把寄存器中的数据保存到内存中.

在这里插入图片描述

给上面的代码加锁, 保证count++的原子性:

//线程安全问题.
public class Demo13 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 不调整代码结构, 进行写加锁 也能解决线程安全问题.
                //() 中 需要针对同一个对象.
                synchronized(locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker) {
                    count++;
                }
            }
        });
        t1.start();


        t2.start();
        //没有调用join方法肯定是不行的.
        //两线程都调用join()方法, 确保 主线程main 等待 t1,t2线程执行结束,再结束.
        t1.join();
        t2.join();
        // 预期结果10W
        System.out.println("count: " + count );
    }
}

当两个对象尝试对一个对象加锁, 此时就会出现"锁冲突"/“锁竞争”. 一旦竞争出现,一个线程能够拿到锁,继续执行代码; 另一个线程拿不到锁,就只能阻塞等待,等待前一个线程释放锁之后,它才有机会拿到锁,继续执行.
这样的规则,本质上是把"并发执行" 变为 “串行执行”
此时就不会出现 “穿插” 的情况了

synchronized除了用于修饰代码块还可用于修饰一个实例方法或者静态方法

class Counter {
    public int count;

    //第一种简化写法: 本质上就是第二种写法.
    synchronized public void increase() {
        count++;
    }
    //第二种写法:
    public void increase2() {
        synchronized(this) {
            count++;
        }
    }
    //第三种写法
    //修饰静态方法相当于给类对象加锁.
    synchronized public static void increase3() {

    }
    //第四种写法
    public static void increase4() {
        synchronized (Counter.class/*类对象*/) {

        }
    }
}

public class Demo14 {
    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();
        System.out.println("count = "+ counter.count);
    }
}

3.3.4 内存可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到. 系统编译器为了提高效率做出一些优化导致线程不安全.(详见 5. volatile关键字)

3.3.5 指令重排序

什么是代码重排序?
⼀段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序.
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这⼀点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

4. synchronized关键字 - 监视锁monitor lock

4.1 synchronized的特性

4.1.1 可重入

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

理解 “死锁”
一个线程没有释放锁, 然后⼜尝试再次加锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁

for (int i = 0; i < 50000; i++) {
            synchronized(object) {
                synchronized(object) {
                    count++;
                }
            }
        }

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (出了最后一个 “}” 才能被别的线程获取到)

4.1.2 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的。
在这里插入图片描述
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队


理解 “阻塞等待”.
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试
进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程,再来获取到这个锁.
注意:
• 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
• 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.

synchronized的底层是使用操作系统的mutex lock实现的.

4.1.3 多个线程,N把锁.即哲学家就餐问题 (重点)

五个哲学家(线程)坐在桌前, 桌上只有一碗面条和五只筷子(锁), 五个哲学家都想吃面条:
在这里插入图片描述
如果五个哲学家都想吃面条,他们拿起左边的筷子, 此时会发现右边没有筷子,于是五个哲学家阻塞等待, 出现死锁, 因为等待的过程中哲学家都不会放下左手的筷子.

解决上述死锁的方法: 针对锁进行编号, 约定加锁的时候,先加编号小的,后加编号大的. 所有的线程都必须遵守这个规则.

4.1.4 死锁的成因(重点)

1. 锁是互斥使用的, 线程已经拿到锁1之后, 线程2想要获取锁1 只能阻塞等待.
2. 不可抢占. 当锁1 已经被线程1获取. 线程2想获取锁1不能强行抢过来.
3. 请求保持(代码结构). 一个线程尝试获取多把锁. (先拿到锁1 之后, 再尝试获取锁2的时候, 锁1不会释放.
4. 循环等待(代码结构).

4.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也必要搭配⼀个具体的对象来使用

4.2.1 修饰代码块

1) 锁任意对象:

public class test8 {
    private Object locker = new Object();
    public void method() {
        synchronized (locker) {

        }
    }
}

2) 锁当前对象:

public class test8 {
   /* private Object locker = new Object();*/
    public void method() {
        synchronized (this) {

        }
    }
}

4.2.2 synchronized 修饰普通方法

public class test8 {
   public synchronized void methond() {

   }
}

4.2.3 修饰静态方法

public class test8 {
   public synchronized static void methond() {

   }
}

两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.

4.3 Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

线程不安全的类: ArrayList , LinkedList , HashMap , TreeMap , HashSet , TreeSet , StringBuilder

线程安全的类:Vector, HashTable(前面两个不推荐使用) , ConcurrentHashMap , StringBuffer(有一些没涉及修改的方法没加锁,不影响线程安全.) , String.

5. volatile 关键字

5.1 volatile能保证内存可见性

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

CPU 进行大部分操作,都很快,一旦操作到读/写内存,此时速度一下就降下来了

导致线程不安全的关键 -> 为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器减少读内存的次数,也就可以提高整体程序的效率了.

public class test9 {
    private /*volatile*/ static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           while(isQuit == 0) {
               //...
           }
           System.out.println("t1结束");
        });
        t1.start();

       Thread t2 = new Thread(() -> {
           System.out.println("输入isQuit的值:");
           Scanner scanner = new Scanner(System.in);
           isQuit = scanner.nextInt();
       });
       t2.start();
    }
}

上述代码的预期结果是输入非0值,t1线程就要退出.
但是当输入非0值时,此时t1线程并未结束.

在这里插入图片描述
在这里插入图片描述
很明显上述代码存在线程安全问题, 这本质上是由编译器的错误优化引起的.

解决: 用volatile修饰变量isQuit. 告诉编译器不需要优化. (其实不用volatile修饰变量, 在t1线程中用上 sleep 方法也能达到预期结果, 所以并不能够很好的确定什么时候一定会出现这种问题)
总结: 涉及到修改操作的变量用上volatile修饰总是没错的.

5.2 volatile不保证原子性

volatile 和 synchronized 有着本质的别.synchronized 能够保证原子性, volatile 保证的是内存可见性.

public class tes10 {
    static class Counter {
        volatile public int count = 0;
        void increase() {
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final 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();
        System.out.println(counter.count);
    }
}

如上代码, 删掉synchronized ,利用volatile修饰count, count 达不到预期结果. 说明volatile 不能保证原子性.

6. wait和notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

篮球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”.而完成⼀个具体的进攻得分动作, 则需要多个运动员相互配合, 按照⼀定的顺序执行⼀定的动作, 线程1 先 “传球” , 线程2 才能 “扣篮”.
完成这个协调工作, 主要涉及到三个方法
• wait() / wait(long timeout): 让当前线程进入等待状态.
• notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

6.1 wait()方法

wait 做的事情:

  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足⼀定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常

wait 结束等待的条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public class Demo19 {
    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之后");
        }
    }
}

6.2 notify()方法

notify 方法是唤醒等待的线程.

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  2. 如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到"原则)
  3. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class test10 {
    static class WaitTask implements Runnable {
        private Object locker;

        public WaitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized(locker) {
                while(true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    static class NotifyTask implements Runnable {
        private Object locker;

        public NotifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized(locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notif 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new WaitTask(object));
        Thread t2 = new Thread(new NotifyTask(object));
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

6.3 notifyAll()方法

notify方法只是唤醒某⼀个等待线程. 使用notifyAll发法可以一次唤醒所有的等待线程.

6.4 wait 和 sleep的区别(面试题)

相同点:
就是都可以让线程放弃执行一段时间

不同点:
1. wait 需要搭配 synchronized 使用. sleep 不需要.
2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

本篇博客到这里就结束啦, 感谢观看 ❤❤❤

🐎🐎🐎期待与你的下一次相遇!!!

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

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

相关文章

java_判断语句——acwing

题目一&#xff1a;倍数 665. 倍数 - AcWing题库 代码 import java.util.Scanner;public class Main{public static void main(String[] args) {Scanner sc new Scanner(System.in);int a sc.nextInt(), b sc.nextInt();if(a%b0 || b%a0) System.out.printf("Sao Mu…

深度解析棋牌游戏开发:从搭建到运营的全流程实战分享

作为从事游戏开发十五年的技术老兵&#xff0c;经历了国内游戏市场从端游到手游的全流程变迁。市面上大多数棋牌产品&#xff0c;无论是传统房卡模式还是创新竞技玩法&#xff0c;自己曾经都参与设计和研发过。今天&#xff0c;我将结合多年的实战经验&#xff0c;分享棋牌游戏…

mfc110u.dll是什么意思,mfc110u.dll丢失解决方法大全详解

mfc110u.dll是Microsoft Foundation Classes (MFC)库的一个特定版本&#xff08;版本11.0&#xff09;的Unicode动态链接库文件。MFC是Microsoft为C开发者设计的一个应用程序框架&#xff0c;主要用于简化Windows应用程序的开发工作。这个框架封装了很多Windows API函数&#x…

【C++跬步积累】—— 继承

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;C跬步积累 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日一题 &#x1f7e1; Linux跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0…

爬虫专栏第二篇:Requests 库实战:从基础 GET 到 POST 登录全攻略

简介&#xff1a;本文聚焦 Requests 库的强大功能与应用实战。首先介绍其安装步骤及版本选择要点&#xff0c;随后深入讲解 GET 请求&#xff0c;以百度页面为例&#xff0c;展示如何发起基本 GET 请求、巧妙添加 headers 与参数以精准搜索&#xff0c;以及正确设置 encoding 避…

计算机网络:IP协议详细讲解

目录 前言 一、IP网段划分 二、IP报头 三、解决IP地址不足-->NAT技术 前言 在之前&#xff0c;我们学习了传输层中的TCP和UDP&#xff0c;重点是TCP协议&#xff0c;他帮我们解决具体到主机的哪个应用&#xff08;端口&#xff09;、传输的可靠&#xff08;序列号、校验和…

Proteus8.17下载安装教程

Proteus是一款嵌入式系统仿真开发软件&#xff0c;实现了从原理图设计、单片机编程、系统仿真到PCB设计&#xff0c;真正实现了从概念到产品的完整设计&#xff0c;其处理器模型支持8051、HC11、PIC10/12/16/18/24/30/DsPIC33、AVR、ARM、8086和MSP430等&#xff0c;能够帮助用…

C++设计模式(装饰模式)

一、介绍 1.动机 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”&#xff0c;由于继承为类型引入的静态特质&#xff0c;使得这种扩展方式缺乏灵活性&#xff1b;并且随着子类的增多&#xff08;扩展功能的增多&#xff09;&#xff0c;各种子类的组合&#xff0…

【VMware】Ubuntu 虚拟机硬盘扩容教程(Ubuntu 22.04)

引言 想装个 Anaconda&#xff0c;发现 Ubuntu 硬盘空间不足。 步骤 虚拟机关机 编辑虚拟机设置 扩展硬盘容量 虚拟机开机 安装 gparted sudo apt install gparted启动 gparted sudo gparted右键sda3&#xff0c;调整分区大小 新大小拉满 应用全部操作 调整完成

03-12、SpringCloud Alibaba第十二章,升级篇,服务注册与配置中心Nacos

SpringCloud Alibaba第十二章&#xff0c;升级篇&#xff0c;服务注册与配置中心Nacos 一、为什么SpringCloud Alibaba 1、为什么 有了spring cloud这个微服务的框架&#xff0c;为什么又要使用spring cloud alibaba这个框架了&#xff1f;最重要的原因在于spring cloud中的…

java网络通信(三):TCP通信、实现客户端-服务端消息通信

目录 1、什么是 TCP协议&#xff1f; 2、代码实现TCP协议的一发一收 2.1、客户端 2.2、服务端 2.3 结果演示 3、代码实现TCP协议的多发多收 3.1 客户端 3.2 服务端 3.3 结果演示 简介&#xff1a;本文章主要是演示如何用java代码以及TCP协议实现网络通信&#xff0c;实…

剖析go协程池实现原理

go协程池实现 在go语言编程中有一种池肯定避免不了&#xff0c;那就是-协程池&#xff0c;无论你是日常工作还是面试中面试官都无法避免协程池&#xff0c;掌握协程池你也就算是入门go的并发编程了&#xff0c;打一波广告后面会有专门的文章来介绍如何在go中进行并发编程。 协…

华为关键词覆盖应用市场ASO优化覆盖技巧

在我国的消费者群体当中&#xff0c;华为的品牌形象较高&#xff0c;且产品质量过硬&#xff0c;因此用户基数也大。与此同时&#xff0c;随着影响力的增大&#xff0c;华为不断向外扩张&#xff0c;也逐渐成为了海外市场的香饽饽。作为开发者和运营者&#xff0c;我们要认识到…

万能门店小程序管理系统 onepic_uploade 任意文件上传漏洞复现

0x01 产品简介 万能门店小程序管理系统是一款功能强大的工具,旨在为各行业商家提供线上线下融合的全方位解决方案。是一个集成了会员管理和会员营销两大核心功能的综合性平台。它支持多行业使用,通过后台一键切换版本,满足不同行业商家的个性化需求。该系统采用轻量后台,搭…

QT:信号和槽01

QT中什么是信号和槽 概念解释 在 Qt 中&#xff0c;信号&#xff08;Signals&#xff09;和槽&#xff08;Slots&#xff09;是一种用于对象间通信的机制。信号是对象发出的事件通知&#xff0c;而槽是接收并处理这些通知的函数。 例如&#xff0c;当用户点击一个按钮时&#…

SQL面试50题

数据表关系图 数据表 CREATE TABLE student (id int(11) NOT NULL AUTO_INCREMENT,name varchar(255) NOT NULL,sex enum(female,male) NOT NULL,birth date NOT NULL,credit float(5,2) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT25 DEFAULT CHARSETutf8;…

下载maven 3.6.3并校验文件做md5或SHA512校验

一、下载Apache Maven 3.6.3 Apache Maven 3.6.3 官方下载链接&#xff1a; 二进制压缩包&#xff08;推荐&#xff09;: ZIP格式: https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zipTAR.GZ格式: https://archive.apache.org/dist/…

基于poi和javabean的excel读取

写在前面 示例写出时间&#xff1a;2024-12-02 这仅仅是excel读取的一个示例, 记录一下&#xff0c;这里也改了一下之前的导出&#xff0c;主要是为了兼容读取 之前的博客地址 基于poi和JavaBean的excel导出 poi依赖 <dependency><groupId>org.apache.poi</gr…

一键生成后端服务,MemFire Cloud重新定义开发效率

作为开发者&#xff0c;特别是独立开发者和小团队成员&#xff0c;大家都知道开发的最大难题之一就是搭建后端服务。要让一个应用从零开始&#xff0c;除了前端的开发工作外&#xff0c;还需要考虑数据库、接口、认证、存储等等一系列繁琐的后台工作。而MemFire Cloud这款神器&…

Maven、JAVAWeb、Servlet

知识点目标 1、MavenMaven是什么Maven项目的目录结构Maven的Pom文件Maven的命令Maven依赖管理Maven仓库JavaWeb项目 2.网络基础知识 3、ServletMaven Maven是什么 Maven是Java的项目管理工具&#xff0c;可以构建&#xff0c;打包&#xff0c;部署项目&#xff0c;还可以管理…