【Java多线程(3)】线程安全问题和解决方案

news2024/11/29 10:56:14

目录

一、线程安全问题

1. 线程不安全的示例

2. 线程安全的概念

3. 线程不安全的原因

二、线程不安全的解决方案

1. synchronized 关键字

1.1 synchronized 的互斥特性

1.2 synchronized 的可重入特性

1.3 死锁的进一步讨论

1.4 死锁的四个必要条件(重点)

2. volatile 关键字

3. wait 和 notify

4. wait 和 sleep 的对比(面试题)


一、线程安全问题

1. 线程不安全的示例

public class Demo {

    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();
        //等t1和t2都结束再打印count
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

上述代码中,对一个count分别在两个线程中循环加加50000次,预期输出结果应该是100000。

但是会发现多次运行后的结果都不会是100000,且每次运行输出的结果都不一致。

2. 线程安全的概念

概念:想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

如果多线程环境下代码运行结果是符合我们预期的,即在单线程环境应该的结果,则说这个线程是线程安全的。

3. 线程不安全的原因

(1)线程在系统中是随机调度的,是抢占式执行的,这是线程安全问题的罪魁祸首。这是系统内核的设计,是无法干预的。

(2)共享资源:多个线程同时修改同一份数据或资源时,如果没有进行适当的同步控制,就容易导致数据的不一致性和错误。前面的代码中,就是两个线程同时修改同一个变量。

(3)原子性:线程针对变量的修改操作,不是原子的。

(4)内存可见性问题

(5)指令重排序

二、线程不安全的解决方案

要想解决线程不安全示例代码的问题,就要从上述原因入手。

原因(1),无法干预。

原因(2),是一个切入点,但是在Java中,这种做法不是很普适,只是针对一些特定场景是可以做到的。因为上面的代码,就是要用多线程修改同一个变量。

原因(3),这是解决线程安全问题,最普适的方案。

比如上述代码中的count++,其实是由三步操作组成的:1.从内存把数据读到 CPU;2.进行数据更新;3.把数据写回到 CPU。因此这一个操作由于线程的抢占式执行,执行指令的相对顺序就会有很多可能:

这是正确的可能性。

在这种情况下,t1读到0并加加为1,这时t2抢到了CPU执行权,也读到0并加加为1,然后存了count=1到CPU,然后t1又抢到了CPU的执行权,还是存了count=1到CPU。最终导致两次++只有一次生效。

实际上,一个线程的 save 在另一个线程的load之前,就是ok的;反之,就都是有问题的。

1. synchronized 关键字

可以通过一些操作,把上述一系列“非原子”的操作,打包成一个“原子”操作,也就是锁。

锁,本质上也是操作系统提供的功能,通过api给应用程序了。Java(JVM)对于这样的系统api又进行了封装。即synchronized关键字。

1.1 synchronized 的互斥特性

synchronized 会起到互斥效果(也成为锁竞争锁冲突), 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象的 synchronized,就会阻塞等待。
  • 进⼊ synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

就比如在学校上厕所,有以下几步:

  1. 关上门并上锁。此时外面还有人想在这个坑上厕所,就得阻塞等待。
  2. 上厕所。
  3. 开门并解锁。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表⽰当前的 "锁定" 状态(类似于厕所的 "有⼈/⽆⼈"). 如果当前是 "⽆⼈" 状态, 那么就可以使⽤, 使⽤时需要设为 "有⼈" 状态. 如果当前是 "有⼈" 状态, 那么其他⼈⽆法使⽤, 只能排队。

Java中随便一个对象,都可以作为加锁的对象。

public class Demo1 {

    //两个线程同时修改同一个变量,会有线程安全问题
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        //线程安全
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                //synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时,
                //其他线程如果也执⾏到同⼀个对象, synchronized 就会阻塞等待.
                //进⼊ synchronized 修饰的代码块, 相当于 加锁
                //退出 synchronized 修饰的代码块, 相当于 解锁
                synchronized (locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

如此,代码就能正确输出100000了。

这里的锁针对的是多个线程竞争同一把锁,如果多个线程使用的锁是不同的,不会产生互斥的。

注意:
  • 上⼀个线程解锁之后, 下⼀个线程并不是⽴即就能获取到锁. ⽽是要靠操作系统来 "唤醒". 这也就 是操作系统线程调度的⼀部分⼯作。
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B ⽐ C 先来的, 但是 B 不⼀定就能获取到锁, ⽽是和 C 重新竞争, 并不遵守先来后到的规则。

再看一个示例:

class Counter {
    private int count = 0;

    //2.由于调用当前方法都需要上锁 可使用方法锁(锁对象是this)
    public synchronized void add() {
        count++;
    }

    //如果是静态方法,锁对象是类名.class(类对象)
    public synchronized static void func() {

    }

    public int getCount() {
        return count;
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //1.直接将counter对象当作锁对象使用
                /*
                synchronized (counter) {
                    counter.add();
                }*/
                counter.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                /*
                synchronized (counter) {
                    counter.add();
                }*/
                counter.add();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

上述代码中,由于调用add方法都需要上锁,就可以在add方法中加上synchronized关键字,将add()设成一个同步方法(方法锁)。这样代码结果一样是正确输出100000.

  • 如果是普通方法,它的锁对象是this,也就是调用这个方法的对象。
  • 如果是静态方法,它的锁对象是类对象,也就是类名.class 。

1.2 synchronized 的可重入特性

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现自己把自己锁死的问题。
理解 "把自己锁死"
⼀个线程没有释放锁, 然后⼜尝试再次加锁.
//第⼀次加锁, 加锁成功
lock();
//第⼆次加锁, 锁已经被占⽤, 阻塞等待.
lock();
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就⽆法进行解锁操作,这时候就会死锁。

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.  

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

    private int count;

    public void add() {
        count++;
    }

    //锁对象是this
    public synchronized void func() {
        System.out.println("调用了func这个同步方法(方法锁)");
    }

    public int getCount() {
        return count;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        Counter2 locker = new Counter2();
        //嵌套锁示例
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                locker.func(); //由于该方法是一个普通的同步方法,因此锁对象也是locker
            }
        });

        t1.start();

        //可重入锁
        //如果某个线程加锁的时候, 发现锁已经被⼈占⽤, 但是恰好占⽤的正是⾃⼰, 那么仍然可以继续获取到锁, 并让计数器⾃增.
        //解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
    }
}

根据以上解释,该代码并不会造成死锁。

1.3 死锁的进一步讨论

死锁的三种比较典型的场景:

  1. 锁是不可重入锁,并且一个线程针对一个锁对象,连续加锁两次,通过引入可重入锁,问题就解决了,因此Java并没有这个问题。
  2. 两个线程两把锁。
  3. N个线程,M把锁。

先看场景2,两个线程两把锁:

有线程1和线程2,以及有锁A和锁B,现在,线程1和2都需要获取到 锁A 和 锁B。线程1拿到锁A,不释放A,继续获取锁B。即先让两个线程分别拿到一把锁,然后再尝试获取对方的锁。

public class Demo4 {
    public static void main(String[] args) {
        //循环等待造成的死锁示例
        //线程一拥有锁1,线程二拥有锁2,双方在拥有自身锁的同时尝试获取对方的锁,
        //最终两个线程就会进入无限等待的状态,这就是死锁。
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                //确保t2线程拿到另一个锁对象了
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1线程正在执行");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                //确保t1线程拿到另一个锁对象了
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("t1线程正在执行");
                }
            }
        });

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

在这个示例中,线程1拿到锁1,线程2拿到锁2,此时线程1要等待锁2释放才能继续执行,线程2也要等待锁1释放才能继续执行,就造成死锁了。

场景3,N个线程M把锁:

哲学家就餐问题,也就是在场景2的基础上更复杂了一些。

 5个哲学家吃面,只有5只筷子。这个模型大部分情况是可以正常工作的。

如果出现极端情况,比如同一时刻,所有人拿起右手的筷子,并且每个人都不肯让出自己手里的筷子,此时所有人都吃不到面条了。

1.4 死锁的四个必要条件(重点)

  • 1. 锁具有互斥特性。(基本特点)
  • 2. 锁不可抢占:一个线程拿到锁之后,除非它自己主动释放锁,否则别人抢不走。(基本特点)
  • 3. 请求和保持:一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁。(代码结构)
  • 4. 循环等待:多个线程获取多个锁的过程中,出现了循环等待。如前面的场景2。(代码结构)

必要条件,意味着只要上述条件缺少一个,就不会构成死锁。

基本特点无法改变,因此我们要从后两点解决死锁问题。

  • 请求和保持,就是尽量不要让锁嵌套。
  • 循环等待,如果就会有锁的嵌套,例如场景2中,约定必须先获取locker1后获取locker2,这里即使出现锁嵌套,也不会死锁。即它们都先抢锁1,另一个没抢到的就阻塞等待了,等锁1执行完,锁2也更早释放了,自然就不会死锁了。

场景3中,约定每个哲学家必须先获取编号小的筷子,后获取编号大的筷子,一样能够有效避免死锁。

2. volatile 关键字

这里我们解决线程不安全的原因4:内存可见性问题。

先看一段代码:

import java.util.Scanner;

public class Demo5 {

    private static int count = 0;

    public static void main(String[] args) {
        //内存可见性示例 - volatile
        Thread t1 = new Thread(() -> {
            System.out.println("t线程开始执行");
            while (count == 0) {
                //
            }
            System.out.println("t线程结束");
        });

        Thread t2 = new Thread(() -> {
            //控制t2线程执行在t1之后
            Scanner scanner = new Scanner(System.in);
            count = scanner.nextInt();
        });

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

上述代码中,我们的预期效果应该是:

t1首先会进行循环,t2抢到CPU后,用户输入非0整数,就会使t1线程退出循环,结束线程。

 而实际上,t1并没有真正出现退出的情况,这个问题产生的原因,就是“内存可见性”。

 上述代码站在指令的角度来理解,使一个线程写,一个线程读。

while循环中,首先load从内存读取数据到CPU寄存器;然后cmp比较(同时会产生跳转),如果比较的条件成立,继续顺序执行,不成立,就跳转到另外一个地址执行。

当前循环中的执行速度是很快的,短时间内出现大量的load和cmp反复执行,而load执行消耗的时间会比cmp快很多很多。

由于load的执行速度很慢,且JVM还发现,每次在t2修改之前,load执行的结果其实是一样的,因此,JVM就把上述load操作优化了,只是第一次真正进行load,这就导致后续t2修改count,此时t1也感知不到了。

如果上述代码中,循环体存在IO操作或者阻塞操作(sleep),这就会使循环执行速度大幅度降低,此时就不会优化load,也就不会有上述问题了。

但是JVM到底优不优化,是不能确定的,此时就需要通过一些方式来控制,不让它触发优化。

Java就引入了volatile关键字,给变量修饰上这个关键字之后,编译器就不会按上述优化策略进行优化了,其作用主要有以下两个:

  1. 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
  2. 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

在上述代码中,变量count加上volatile关键字,问题就解决了。 

 

有一个注意点,volatile关键字保证的是内存可见性,但不能够保证原子性。

因此在目录一的第一个示例中,不使用synchronized而用volatile还是一样会出现同样的问题,原因就是volatile不能保证原子性。

3. wait 和 notify

原因5的解决包含在了synchronized和volatile中,采用这两种方法都能够解决指令重排序问题。这里就来看线程执行的先后顺序问题。

由于线程之间是抢占式执行的,因此线程之间的执行的先后顺序是难以预知的,但实际开发中有时候我们希望合理协调多个线程之间的执行先后顺序。这里就涉及到了线程的等待通知机制,通过等待的方式,能够让线程一定程度按照我们预期的顺序来执行,我们无法主动让某个线程被调度,但是可以主动让某个线程等待。

假设你去一家繁忙的餐厅用餐,餐厅只有几张桌子供应,但人们排队等待就餐。这里的顾客可以比作线程,桌子可以比作共享资源,比如餐具。餐厅经理就是一个控制资源访问的调度程序。

  1. 等待就餐的顾客:顾客排队等待就餐,他们是等待状态的线程。他们不会一直站在那里,而是会进入一个等待区域,表示线程被挂起。

  2. 顾客就餐:当有桌子空出来时,经理会通知等待区域的顾客,告诉他们可以进入餐厅就餐了。这个通知过程就相当于唤醒了一个或多个等待状态的线程。

  3. 桌子资源:餐厅的桌子是共享资源,多个顾客(线程)需要共享这些资源。当一个顾客就餐时,这个桌子就被占用了,其他顾客需要等待它被释放。

  4. 餐厅经理:餐厅经理就是控制资源访问的调度程序。他会检查哪些桌子空闲,然后通知等待区域的顾客。

在这个场景中,等待区域就相当于等待池,经理的通知就相当于线程的唤醒操作,桌子就相当于共享资源,顾客就相当于线程。通过这个生活场景的类比,可以更好地理解线程的等待通知机制。

等待通知机制,就是通过条件,判定当前逻辑是否能够执行,如果不能执行,就主动wait(主动进行阻塞),把执行的机会让给别的线程,避免该线程进行一些无意义的重试。等到后续条件满足了(其他线程通知了),再让阻塞的线程被唤醒。

 

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

即wait内部做的事情不仅仅是阻塞等待,还要解锁。要想解锁,就得先加上锁。

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

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法. 如果是不同锁对象就没有联系了.
  • wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.
import java.util.Scanner;

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        //等待唤醒机制的示例
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            synchronized (locker) {
                System.out.println("t2唤醒之前");
                //通过scanner控制阻塞,用户输入之前,都是阻塞状态
                scanner.next();
                locker.notify();
                System.out.println("t2唤醒之后");
            }
        });

        t1.start();
        Thread.sleep(100);
        t2.start();
    }
}

上述代码中,t1线程启动后先睡眠一会,保证t1先抢占到CPU,执行到wait进入等待状态。然后t2抢到CPU执行权,输入之后,notify就会唤醒上述wait操作,从而使t1回到RUNNABLE状态,并参与调度。

t1唤醒,并不是立即执行的,要先重新获取到锁,由于t2此时还没释放锁,意味着t1会从WALTING->RUNNABLE->BLOCKED。因此唤醒后,会等t2执行完毕,t1才继续执行。因此这段代码中的执行顺序是固定的。

如果这里并没有控制t1先执行,有可能t2会先抢到CPU执行权,从而先执行了notify,此时t1还没wait,locker上也没有其它任何线程wait,此时之间t2的notify就不会有任何效果(也不会抛异常),但是后续t1进入wait之后,就无法唤醒了。

还有其他的注意点:

  • 当有多个线程等待时,notify只能唤醒多个等待线程中的一个,且这个唤醒的线程是随机的。
  • wait操作和join一样也提供了带超时时间的版本,指定时间内如果没有被notify,就自动唤醒。

如果要用时唤醒所有等待的线程,可以使用notifyAll方法:

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        //多个线程等待,notify()只能随机唤醒一个,,notifyAll()则可以唤醒所有等待线程
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2等待之后");
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t3唤醒之前");
                locker.notifyAll();
                System.out.println("t3唤醒之后");
            }
        });

        t1.start();
        t2.start();
        Thread.sleep(100);
        t3.start();
    }
}

同样的,t1和t2被唤醒后,也需要t3执行完毕。

 

4. wait 和 sleep 的对比(面试题)

其实理论上 wait 和 sleep 完全是没有可⽐性的,因为⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间。
  • 唯一的相同点:都可以让线程放弃执行⼀段时间。

不同点:

  1. wait是Object类中的一个方法,sleep是Thread类中的一个方法。
  2. wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用。
  3. wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作。

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

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

相关文章

2024UI自动化面试题汇总【建议收藏】

1.你是如何搭建ui自动化框架的&#xff1f; 在搭建ui自动化框架&#xff0c;使用的是po设计模式&#xff0c;也就是把每一个页面所需要 操作的元素和步骤都封装成一个页面类中。然后使用seleniumunittest搭建 四层框架实现数据、脚本、业务逻辑分离&#xff08;关键字驱动&…

怎样在Linux搭建NTP服务器

搭建 NTP&#xff08;Network Time Protocol&#xff09;服务器可以帮助你在局域网内提供时间同步服务&#xff0c;让网络中的设备都使用统一的时间。以下是在 Linux 系统上搭建 NTP 服务器的基本步骤&#xff1a; 安装 NTP 服务器软件&#xff1a; 在终端中执行以下命令安装 N…

计算机网络实验四:MAC地址、IP地址、ARP地址

目录 实验四&#xff1a;MAC地址、IP地址、ARP地址 4.1 实验目的 4.2 实验步骤 4.2.1 构建网络拓扑 4.2.2 配置各网络设备 4.2.3 网络功能验证测试 4.3 实验总结 实验四&#xff1a;MAC地址、IP地址、ARP地址 4.1 实验目的 &#xff08;1&#xff09;掌握计算机网络中…

使用mybatis的@Interceptor实现拦截sql

一 mybatis的拦截器 1.1 拦截器介绍 拦截器是一种基于 AOP&#xff08;面向切面编程&#xff09;的技术&#xff0c;它可以在目标对象的方法执行前后插入自定义的逻辑。 1.2 语法介绍 1.注解Intercepts Intercepts({Signature(type StatementHandler.class, method “…

深度理解C++多继承和多态

首先我们看看多继承的多态是如何发生的。 #include <iostream>using std::cout; using std::endl;class A {public:virtualvoid a(){cout<<"virtual A::a()"<<endl;}virtualvoid b(){cout<<"virtual A::b()"<<endl;}virtua…

【C++进阶】多态,带你领悟虚函数和虚函数表

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;c大冒险 总有光环在陨落&#xff0c;总有新星在闪烁 【本节目标】 1. 多态的概…

京东云8核16G服务器配置租用优惠价格1198元1年、4688元三年

京东云轻量云主机8核16G服务器租用优惠价格1198元1年、4688元三年&#xff0c;配置为8C16G-270G SSD系统盘-5M带宽-500G月流量&#xff0c;华北-北京地域。京东云8核16G服务器活动页面 yunfuwuqiba.com/go/jd 活动链接打开如下图&#xff1a; 京东云8核16G服务器优惠价格 京东云…

uniapp 微信小程序 输入框跟随手机键盘弹起

需求&#xff1a;手机键盘弹起后&#xff0c;页面底部的输入框跟随弹起&#xff0c;且页面不被顶上去 html: <textareaclass"textinput"placeholder-class"input-place"auto-height:maxlength"2000"v-model"text"placeholder"…

工业测试测量仪器与人工智能(AI)如何结合

工业测试测量仪器与人工智能&#xff08;AI&#xff09;的结合可以通过多种方式实现&#xff0c;其中一些主要方法包括&#xff1a; 1. 数据分析和预测 智能数据分析&#xff1a;利用AI算法对从传感器和测试仪器收集的数据进行分析&#xff0c;识别模式、趋势和异常&#xff0…

基于单片机热敏电阻PT100温度控制系统设计

**单片机设计介绍&#xff0c;基于单片机热敏电阻PT100温度控制系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机热敏电阻PT100的温度控制系统设计是一个综合了硬件和软件设计的工程任务。以下是对该设计概要的…

我如何学会在学术界培养人际关系,并变得更加友善

我是一名初级教授&#xff0c;压力很大&#xff0c;工作到筋疲力尽&#xff0c;但在工作和家庭中仍然感到不足。因此&#xff0c;当我的入门编程课程的三名学生在学期结束时来到我的办公室&#xff0c;对他们的成绩感到担忧时&#xff0c;我觉得我没有时间处理他们的抱怨。我觉…

总结IP协议各类知识点

前言 本篇博客博主将详解IP协议中的各类知识点&#xff0c;坐好板凳发车啦~ 一.IP协议格式 1.1 4位版本号&#xff08;version&#xff09; 指定IP协议的版本&#xff0c;对于IPv4来说&#xff0c;就是4。 1.2 4位头部长度&#xff08;header length&#xff09; IP头部的…

数据结构算法系列----贪心算法

目录 一、什么是贪心 1、定义&#xff1a; 2、举例&#xff1a; 二、例题 完整代码&#xff1a; 一、什么是贪心 1、定义&#xff1a; 贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法。在贪心算法中&#xff0c;通过 局部最优 解来达到全局最优解。贪心算法…

(C语言)fgets与fputs函数详解

目录 1. fputs函数详解 1.1 向文件流输入数据 1.2 向标准输出流输出数据 2. fgets函数详解 2. 1 从文件流中得到数据 2.2 从标准输入流读取数据 1. fputs函数详解 头文件&#xff1a;stdio.h 函数有两个参数&#xff1a;str 与 stream 作用&#xff1a;写一串字符串到流…

计算机系统基础 5 物理地址的形成

历史 早期&#xff0c;程序员自己管理主存&#xff0c;通过分解程序并覆盖主存的方式执行程序 取指令和存储操作数所有的地址都是物理地址&#xff1b; 执行速度快&#xff0c;无需进行地址转换&#xff1b; 未采用虚拟存储机制。 1961年有人提出自动执行overlay…

【动手学深度学习-pytorch】9.2长短期记忆网络(LSTM)

长期以来&#xff0c;隐变量模型存在着长期信息保存和短期输入缺失的问题。 解决这一问题的最早方法之一是长短期存储器&#xff08;long short-term memory&#xff0c;LSTM&#xff09; (Hochreiter and Schmidhuber, 1997)。 它有许多与门控循环单元&#xff08; 9.1节&…

【学习笔记】java项目—苍穹外卖day04

文章目录 1. 新增套餐1.1 需求分析和设计1.2 代码实现1.2.1 DishController1.2.2 DishService1.2.3 DishServiceImpl1.2.4 DishMapper1.2.5 DishMapper.xml1.2.6 SetmealController1.2.7 SetmealService1.2.8 SetmealServiceImpl1.2.9 SetmealMapper1.2.10 SetmealMapper.xml1.…

HarborCDN技术分析

一、介绍 简要介绍 ​​Harbor​​ 是由VMware公司开源的企业级的Docker Registry管理项目&#xff0c;它包括权限管理(RBAC)、LDAP、日志审核、管理界面、自我注册、镜像复制和中文支持等功能。Harbor 的所有组件都在 Dcoker 中部署&#xff0c;所以 Harbor 可使用 Docker C…

NC269391 炸鸡块哥哥的粉丝题

题目描述 智乃作为炸鸡块哥哥的粉丝&#xff0c;做了一场炸鸡块哥哥的比赛后得出一个结论&#xff0c;那就是炸鸡块哥哥的话&#xff0c;最多只能信半句。 现在给你一个长度为N的字符串S&#xff0c;请输出前 个字符&#xff0c;表示只能相信半句话。 例如当炸鸡块哥哥说&…

既有理论深度又有技术细节——深度学习计算机视觉

推荐序 我曾经试图找到一本既有理论深度、知识广度&#xff0c;又有技术细节、数学原理的关于深度学习的书籍&#xff0c;供自己学习&#xff0c;也推荐给我的学生学习。虽浏览文献无数&#xff0c;但一直没有心仪的目标。两周前&#xff0c;刘升容女士将她的译作《深度学习计…