JavaEE-多线程初阶2

news2024/11/28 22:46:15

✏️作者:银河罐头
📋系列专栏:JavaEE

🌲“种一棵树最好的时间是十年前,其次是现在”

目录

  • Thread类及常见方法
    • 获取当前线程引用
    • 休眠当前线程
  • 线程的状态
    • 线程的所有状态
    • 线程状态
    • 多线程的意义
  • 多线程带来的的风险-线程安全
    • 代码示例
    • 线程不安全的原因
  • synchronized 关键字
    • synchronized 的特性
      • 1)互斥
      • 2)可重入
    • Java 标准库中的线程安全类
  • 死锁
    • 死锁的几种情况
    • 死锁的4个必要条件
    • 如何避免死锁

Thread类及常见方法

获取当前线程引用

public static Thread currentThread();

返回当前线程对象的引用

休眠当前线程

public static void sleep(long millis) throws InterruptedException

本质上就是这个线程不参与调度了,不去CPU上执行了

image-20221203211022235

PCB是使用链表来组织的,实际情况并不是一个简单的链表,是一系列以链表为核心的数据结构。

一旦线程进入阻塞状态,对应PCB就进入阻塞队列了,就暂时无法参与调度了。

  • 比如调用sleep(1000),那么线程就要在阻塞队列待1000ms,当这个PCB回到了就绪队列,会被立即调度吗?

其实不是,实际上考虑到调度的开销,对应的线程是无法在唤醒之后立即被调度的,实际上的时间间隔大概率要大于1000ms.

挂起(hung up)就是阻塞(block)

线程的状态

线程的所有状态

  • 1.NEW 创建了Thread对象,但是还没调用start(内核里还没创建对应PCB)

  • 2.TERMINATED 表示内核里的PCB已经执行完毕了,但是Thread对象还在

  • 3.RUNNABLE 可运行的

    a)正在CPU上执行的

    b)在就绪队列里,随时可以去CPU上执行的

  • 4.WAITING

  • 5.TIMED_WAITING

  • 6.BLOCKED

4,5,6都是阻塞,都表示线程PCB在阻塞队列中,这几个状态是不同原因的阻塞

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

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

线程状态

线程的状态转化

image-20221203215929103

WAITING

Scanner这里的阻塞是因为等待IO的, 等待IO也会进行一些线程操作,内部可能会涉及到锁操作或者wait之类的操作。

读写文件,读写控制台,读写网络…都可能会造成阻塞

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

            }
        });
        // 在启动之前,获取 t 的状态,就是 NEW 状态
        System.out.println("start 之前:" + t.getState());
        t.start();
        System.out.println("t 执行中的状态:"+ t.getState());
        t.join();
        //线程执行完毕之后,就是 TERMINATED 状态
        System.out.println("t 结束之后:" + t.getState());
    }

一旦内核里的PCB消亡了,此时代码中的t对象就没啥用了。之所以存在,是迫不得已。Java中对象的生命周期自有其规则,这个生命周期和内核中线程并非完全一致。内核的线程释放的时候,无法保证Java代码中t对象也立即释放。因此,势必会存在,内核中PCB没了,但是代码中t还存在这样的情况。因此就需要通过特定的状态,来把t对象标识为"无效"。

一个线程只能start一次。

//输出结果:
start 之前:NEW
t 执行中的状态:RUNNABLE
t 结束之后:TERMINATED

之所以此处能看到RUNNABLE,主要是因为线程run里没有写sleep之类的方法

image-20221204145400187

image-20221204145830999

通过这里的循环获取,就可以看到这里的交替状态,当前获取到的状态完全取决于系统的调度操作,获取状态的这一瞬间 t 线程是正在执行还是正在 sleep

多线程的意义

多线程最核心的地方:抢占式执行,随机调度

多线程的意义是?

单个线程和多个线程之间,执行速度的差别

程序分成

CPU密集,包含了大量加减乘除等算术运算

IO密集,涉及到读写文件,读写控制台,读写网络

这种衡量执行时间的代码,运行的久一点,误差越小,

线程调度自身也是有时间开销的,运算的任务量越大,线程调度的开销相比之下就非常不明显了,从而就可以忽略不计

public static void main(String[] args) {
        //serial();
        concurrency();
    }
    //串行执行,一个线程完成
    public static void serial(){
        //为了衡量代码的执行速度,加上个计时的操作
        //currentTimeMillis获取到当前系统 ms 级的时间戳
        long beg = System.currentTimeMillis();
        long a = 0;
        for(long i = 0;i < 100_0000_0000L;i++){
            a++;
        }
        long b = 0;
        for(long i = 0;i < 100_0000_0000L;i++){
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - beg) + " ms");
    }

    public static void concurrency(){
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            long a = 0;
            for(long i = 0;i < 100_0000_0000L;i++){
                a++;
            }
        });
        Thread t2 = new Thread(()->{
            long b = 0;
            for(long i = 0;i < 100_0000_0000L;i++){
                b++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("并发执行时间:" + (end - beg) + " ms");
    }

image-20221204162234345

多线程更快,多线程可以更充分利用多核心CPU的资源

这个例子中单线程到多线程时间为什么不是正好缩短一半?

不确定t1和t2是在两个CPU上并行执行的还是并发执行的。

实际上,t1和t2在执行过程中会经历很多次调度,这些调度有些是并行执行(在2个核心上),有些是并发执行的(1个核心上)。

到底是多少次并行,多少次并发,不好预估,取决于系统的配置,也取决于当前程序的运行环境。(系统同一时刻跑了很多程序,并发的概率更大,很多程序来抢CPU)

另一方面,线程调度自身也是有时间消耗的。

梳理一遍上述代码的执行逻辑:main线程先调用t1.start,启动t1开始计算t1的同时main再启动t2.start。启动t2的同时t1仍然在继续计算。同时main线程进入t1.join,此时main线程阻塞等待,t1,t2还是在继续执行。等t1执行完了,main线程从t1.join返回再执行t2.join,main线程等待t2,t2执行完了,main线程从t2.join返回,继续执行计时操作。

  • 关于join的理解,比如这里是main线程里调用t1.join(),就是main线程阻塞等待t1结束

不是说多线程一定能提高效率

1.是否是多核(现在CPU基本上都是多核了)

2.当前核心是否空闲(如果CPU这些核心已经都满载了,这个时候启动多线程也没用)

多线程带来的的风险-线程安全

造成线程安全这种问题的原因是多线程的抢占式执行,带来的随机性。从单线程到多线程,代码执行顺序的可能性从一种情况变成了无数种情况。所以就需要保证在这无数种线程调度顺序的情况下,代码的执行结果都是正确的。只要有一种情况代码结果不正确,就被视为线程不安全。

代码示例

class Counter{
    public int count = 0;
    public void add(){
        count++;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count = " + counter.count);
    }
}
//三次运行结果:
count = 53782
count = 55883
count = 68803

预期结果是10w,而实际结果不是10w,并且每次运行的结果都不一样,程序出现bug了!

++ 操作本质上分为三步:

1.先把内存的值读取到CPU的寄存器中 load

2.把CPU寄存器的值进行 + 1 运算 add

3.把得到的结果写回到内存中 save

//这3个操作就是CPU上执行的3个指令。指令是机器语言

如果是两个线程并发执行count ++,此时就相当于两组 load add save 进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异。

线程一些可能的调度顺序:

image-20221204195705675

其中情况1,2结果是正确的,其他情况都是有问题的

image-20221204200223216

比如情况3

image-20221204200553634

经历2次自增之后count结果还是1

其实这里就和 事务 读未提交 read uncomitted 是一样的,相当于t1读到的是一个t2未提交的脏数据,于是出现了脏读问题

这里的多线程和并发事务本质上都是"并发编程"问题,并发处理事务,底层是基于多线程这样的方式来实现的

image-20221204201422905

这是一个线程,这个线程要具体执行,就需要先编译成很多CPU指令,写的任何一个代码都是需要编译成很多CPU指令的。

image-20221204201851396

由于线程的抢占式执行,导致当前执行到任意一个指令的时候线程都可能被调度走,CPU让别的线程来执行。

CPU里有个重要的组成部分,寄存器。寄存器也能存数据,空间更小,访问速度更快,CPU进行的运算都是针对寄存器的数据进行的。

CPU里的寄存器有很多种,

有的是通用寄存器(用来参与运算的) EAX , EBX , ECX…

有的是专用寄存器(有特定功能的) EBP ESP EIP…

保存上下文的,用PCB里的内存把当前所有的寄存器都给保存起来。

机器指令就是汇编,机器指令就是直接在CPU上运行的,势必要经常操作寄存器

当前这个代码是否有可能结果正好是10w?有可能,概率很小

假设2个线程每次调度的顺序都是情况1,2

当前结果一定大于5w?

实际运行基本都是大于5w的,但是也不一定

还可能出现t1自增1次,t2自增2次,(情况5)最终还是增1这种

线程不安全的原因

  • 1.(根本原因)抢占式执行,随机调度
  • 2.代码结构:多个线程修改同一个变量

一个线程修改一个变量,没事;

多个线程读取同一个变量,没事;

多个线程修改多个不同的变量,没事。

因此可以通过修改代码的结构来规避这个问题,这种调整不一定都是能使用的,代码结构也是源于需求的。调整代码结构是个方案,但是不是一个普适性特别高的方案

String是不可变对象

不可变对象,天然就是线程安全的。

像有些编程语言,比如erlang,语法里没有"变量"这个概念,所有的"变量"都是不可变的,这样的语言更适合并发编程,出现线程安全的概率大大降低了

  • 3.原子性

如果修改对象是原子的,那就罢了

如果是非原子的,出现问题的概率就非常高了。

原子:不可拆分的最小单位。

count ++ 这里可以拆分成 load add save 三个操作,这三个操作中的每个操作都是原子的,单个指令是无法再进一步拆分了。

如果++操作是原子的,那么线程安全问题就迎刃而解了(出问题的本质是"脏读",t1修改的结果还没提交,t2就读了)

针对线程安全问题,如何解决?最主要的手段就是从这个原子性入手,把这个非原子的操作变成原子的,加锁。

  • 4.内存可见性问题

如果是一个线程读,一个线程改呢?也可能出问题,可能出现此处读的结果不太符合预期。

  • 5.指令重排序

本质上是编译器优化出bug了,编译器在保持逻辑不变的情况下,调整代码的执行顺序,从而加快代码的执行效率

上述分析出的是5个典型的原因,不是全部。一个代码究竟是线程安全还是不安全,都得具体情况具体分析,难以一概而论。

如果一个代码踩中了上面的原因,也可能线程安全;如果一个代码没踩中上面的原因,也可能线程不安全。

原则是多线程运行代码不出bug就是安全的。

在阅读代码的时候,脑子里分析出所有可能执行的情况,并一一判定里面是否有问题

重排序指的是单个线程里,顺序发生调整。

synchronized 关键字

synchronized 的特性

1)互斥

如何从原子性入手来解决线程安全问题,通过加锁。

synchronized public void add(){
        count++;
    }
//synchronized 这是一个关键字,表示加锁

加了synchronized之后,进入方法会加锁,出了方法会解锁。

如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功。

image-20221205143418519

t2加锁没加成,一直阻塞等待到 t1 unlock了之后才继续执行。

加锁,说是保证原子性,其实不是说让这里的3次操作一次完成,也不是这3步操作过程中不调度,而是让其他也想操作的线程阻塞等待了。

加锁本质上是把并发变成了串行。

操作系统中的基本设定,系统里的锁"不可剥夺"特性,一旦一个线程获取到锁,除非他主动释放,否则无法强占。

synchronized 的行为就是阻塞等待,一直等下去。 Java中还有一种锁, ReentrantLock 这个锁,获取不到就放弃

一旦加锁之后,代码的速度是大打折扣的。算的快前提是算得准。

虽然加锁之后算的慢了,但是还是比单线程快,加锁只是针对count++ 加锁了,除了 count++ 之外还有for循环的代码,for循环代码是可以并发执行的,只是count++ 串行执行了。一个任务中,一部分并发,一部分串行,仍然是要比所有代码都串行要更快的。

package Thread;
import java.util.concurrent.CountDownLatch;
class Counter{
    public int count = 0;
    synchronized public void add(){
        count++;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //搞两个线程,两个线程分别针对 counter 来调用 5w 次的 add 方法
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        //启动线程
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count = " + counter.count);
    }
}
//结果:
count = 100000
  • synchronized使用方法

1.修饰方法

1)修饰普通方法。锁对象就是this

2)修饰静态方法。锁对象就是类对象(Counter.class)

2.修饰代码块。显式/手动指定锁对象

加锁要明确是针对哪个对象加锁。

如果两个线程针对同一个对象加锁,会产生阻塞等待(锁竞争/锁冲突)。

如果两个线程针对不同对象加锁,不会阻塞等待(不会锁竞争/锁冲突)。

两个线程,一个线程加锁,一个线程不加锁,这种情况没有锁竞争。

image-20221212122238491

线程 对 对象加锁

举个例子,张三的npy是李四,相当于张三对李四加锁了,如果王五也想和李四处对象(王五也想对李四加锁)。就会出现锁竞争/锁冲突。因为张三已经对李四加锁了,所以王五就只能阻塞等待。不过赵六刚好单身,王五可以对赵六加锁。张三和王五对不同对象加锁,不会发生阻塞等待。

(两男争一女会出现锁竞争)

没有锁竞争=>抢占式执行=>很有可能出现线程安全问题

image-20221212180855343

image-20221212192133498

问:构造方法可以使用synchronized关键字修饰吗?

不能❌

synchronized关键字作用于方法上,是给当前对象实例/类加锁,而在构造方法上加 synchronized,此时对象实例还没产生;另外构造方法每次都是构造出新的对象,不存在多个线程同时读写同一对象中的属性的问题,所以不需要同步。

monitor lock 监视器锁

JVM给 synchronized 起的名字,代码报异常可能会见到 monitor lock

2)可重入

image-20221212193047032

因为在Java里这种代码是很容易出现的,为了避免不小心就死锁,Java就把 synchronized 设定为可重入的了。但是 C++ , Python , 操作系统原生的锁,都是不可重入的。

就是在锁对象里记录一下,当前锁是哪个线程持有的。如果加锁线程和持有线程是同一个,就直接放过,否则就阻塞。

上面只是死锁的一种情况,还有别的情况。

Java 标准库中的线程安全类

如果多个线程操作同一个集合类,就需要考虑线程安全的问题。

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

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

Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer

上述这些已经内置了 synchronized 加锁,相对来说更安全一点。

StringBuffer 的核心方法都带有 synchronized .

还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的,比如String

死锁

死锁的几种情况

1.一个线程,一把锁,连续加锁两次,如果锁是不可重入锁,就会出现死锁。Java中synchronized和ReentrantLock都是可重入锁。

2.两个线程,两把锁,t1和t2各自先针对锁A和锁B加锁,再尝试获取对方的锁。

举个栗子:家钥匙锁车里了,车钥匙锁家里了。

public class ThreadDemo {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                    //sleep是为了确保两个线程先把第一把锁拿到(线程是抢占式执行的)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1 把 locker1 和 locker2 都拿到了");
                }
            }

        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1){
                    System.out.println("t2 把 locker2 和 locker1 都拿到了");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

image-20221212210355985

程序结果没有输出,死锁了。

image-20221212211754984

image-20221212211808217

针对这样的死锁问题,可以借助 jconsole 这样的工具来进行定位,看线程的状态和调用栈就可以分析出程序在哪里死锁了。

3.多个线程 多把锁(相当于2的一般情况)

经典案例:哲学家就餐问题

image-20221212215101681

5个哲学家围着桌子吃意大利面,只有5只筷子。每个哲学家有两种状态,1.思考人生(相当于线程阻塞的状态) 2.拿起筷子吃面条(相当于线程获取到锁然后进行一些计算)。由于操作系统随机调度,这5个哲学家随时有可能想吃面也随时有可能在思考人生,要想吃面就得拿起左手和右手的两根筷子。

假如出现了极端情况,同一时刻哲学家同时拿起左手的筷子,这时哲学家拿不起右手的筷子,都要等待右边的哲学家把筷子放下,出现了死锁。

死锁的4个必要条件

1.互斥使用。线程1拿到了锁,线程2就得等着,(锁的基本特性)

2.不可抢占。线程1拿到了锁之后,必须是线程1主动释放,不能说是线程2把锁强行获取到。

3.请求和保持。线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的。(不会因为获取锁B就把锁A释放了)

4.循环等待。线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A。线程1在获取B时等待线程2释放B;同时线程2在获取A时要等待线程1释放A。

前3个条件都是锁的基本特性(对于synchronized这把锁来说无法改变)

条件4循环等待是这4个条件里唯一一个和代码结构相关的,也是程序员可以控制的。

世界上的锁不是只有synchronized,还会存在一些其他情况的锁,可能和上述1,2,3条件还有变数。

如何避免死锁

打破必要条件即可,突破口是循环等待。

解决办法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候都让线程遵守上述规则,此时循环等待自然破除。

以哲学家用餐为例如何破除死锁:

image-20221213114519760

//上面例子
//约定两个线程先拿编号小的后拿编号大的。(先拿locker1,后拿locker2)
public class ThreadDemo {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1 把 locker1 和 locker2 都拿到了");
                }
            }

        });
        Thread t2 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t2 把 locker2 和 locker1 都拿到了");
                }
            }
        });
        t1.start();
        t2.start();
    }
}
//输出结果:
t1 把 locker1 和 locker2 都拿到了
t2 把 locker2 和 locker1 都拿到了

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

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

相关文章

Python课程设计题目

文章目录1.基于Python的XX管理系统。2.基于Python的XX工具开发1.基于Python的XX管理系统。 实例要求&#xff1a;采用面向对象或是字典保存实例的属性信息。 功能要求&#xff1a;提供增加&#xff0c;删除&#xff0c;修改&#xff0c;单个查询&#xff0c;查询所有&#xff…

NFC标签 “ PN29_S

产品参数产品参数 产品型号 PN29_S 产品尺寸 (mm) 9546.45.4 显示技术 E ink 显示区域 (mm) 29(H)66.9(V) 分辨率 (像素) 296128 像素尺寸(mm) 0.2270.226 显示颜色 黑/白 视觉角度 180 工作温度 0℃ - 50℃ 电池 无需电池 工作频率 13.56 MHZ NF…

数据结构单循环链表

循环链表的特点是无须增加存储量&#xff0c;仅对表的链接方式稍作改变&#xff0c;即可使得表处理更加方便灵活。 【例】在链表上实现将两个线性表(a1&#xff0c;a2&#xff0c;…&#xff0c;an)和(b1&#xff0c;b2&#xff0c;…&#xff0c;bm)连接成一个线性表(a1&…

智云通CRM:那些令你无法控制的销售局面(一)

销售的复杂性并不仅仅是指购买者得人数很多。复杂销售的定义在最近的十年里有了突破性的变化和发展&#xff0c;这种变化和发展中出现了很多挑战&#xff0c;不仅业务员&#xff0c;就连业务经理都难以应对。因此&#xff0c;唯有制定简化处理程序的计划才能妥善应对这一局面。…

利用if语句求解成绩等级问题

1 问题 成绩进行划分等级&#xff0c;人工划分容易出错&#xff0c;且数量庞大。 2 方法 public class Text06 { public static void main(String[] args) { int score100; System.out.println("score"); // 1.成绩大于等于85 if (sc…

前端_Vue_4.类与样式绑定、条件渲染

文章目录一、Class与Style绑定1.1. 绑定HTML class1.1.1. 绑定对象1.1.2. 绑定数组1.1.3. 在组件上使用1.2. 绑定内联样式1.2.1. 绑定对象1.2.2. 绑定数组1.2.3. 自动前缀1.2.4. 样式多值二、条件渲染2.1. v-if2.2. v-else2.3. v-else-if2.4. \<template\> 上的 v-if2.5.…

电脑开不了机系统应该如何恢复正常

电脑不仅携带方便&#xff0c;而且功能也十分强大&#xff0c;不过电脑使用时会时不时出现问题&#xff0c;如果电脑开不了机怎么办 怎么回事?这是我们经常会遇到的这种的问题&#xff0c;今天小编就和大家分享电脑开不了机了的原因及解决方法。 工具/原料&#xff1a; 系统…

outlook中抄送操作和163撤回邮件

(1)CC和BCC 电子邮件中的CC 英文全称是 Carbon Copy(抄送)。 电子邮件中的BCC英文全称是 Blind Carbon Copy(暗抄送)。 两者的区别在于在BCC栏中的收件人可以看到所有的收件人名(TO&#xff0c;CC&#xff0c;BCC)&#xff0c;而在TO和CC栏中的收件人看不到BBC的收件人名。 …

Android 代码混淆Proguard

混淆概念 Android代码混淆&#xff0c;又称Android混淆&#xff0c;是伴随着Android系统的流行而产生的一种APP保护技术&#xff0c;用于保护APP不被破解和逆向分析。 在Android的具体表现就是打包时&#xff0c;将项目里的包名、类名、变量名根据混淆规则进行更改&#xff0c…

【POJ No. 1743】音乐主题 Musical Theme

【POJ No. 1743】音乐主题 Musical Theme 北大OJ 题目地址 【题意】音乐旋律被表示为N &#xff08;1≤N ≤20000&#xff09;个音符的序列&#xff0c;它们是[1, 88]内的整数&#xff0c;每个音符都代表钢琴上的一个键。许多作曲家都围绕一个重复的主题谱写音乐&#xff0c;该…

建木HA部署

背景 在建木v2.6.1之前&#xff0c;建木Server仅支持单机部署&#xff0c;如果出现单机故障&#xff0c;难以应用于在线场景&#xff0c;并且单机压力过大时&#xff0c;会影响系统延展性。 什么是HA HA&#xff08;High Availability Cluster&#xff09;是高可用集群系统的…

【软件测试】开发人员不鸟自己?看看资深测试如何做的......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试人员经常抱怨开…

jsp+servlet+mysql实现的新闻发布管理系统源码+运行教程+文档

今天给大家演示一下一款由jsp servlet mysql实现的新闻发布管理系统&#xff0c;主要实现了前台游客浏览新闻、评论新闻&#xff0c;后台管理员管理新闻等功能&#xff0c;新闻有热点新闻、最新更新等方式在首页展示&#xff0c;还有幻灯片展示重大新闻等功能&#xff0c;满足了…

一招解决开发环境问题——远程容器开发指南

前言 使用C作为主要开发语言的程序猿们应该会认同搭建开发环境是一件烦人的事情。为了编译一个程序不仅需要下载各种依赖包&#xff0c;还可能面临本地系统不兼容、编译器版本不一致、包版本冲突等各种问题。笔者在运营iLogtail开源社区的过程中发现开发和调试环境问题也是成员…

web网页设计期末课程大作业——HTML+CSS+JavaScript美食餐饮文化主题网站设计与实现

&#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计&#x1f469;‍&#x1f393;,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等&#xff0c;用的最多的还是DW&#xff0c;当然不同软件写出的…

为云原生插上翅膀,天翼云弹性存储CStor-CSI助力容器腾飞

云原生是一种新型技术体系&#xff0c;已成为云计算未来的发展方向&#xff0c;越来越多的数字化项目与云原生紧密相连。作为云原生的基座&#xff0c;容器是必不可少的核心技术。然而&#xff0c;以Docker为代表的容器引擎&#xff0c;并不能满足大批量的容器业务需求&#xf…

推荐一套yyds的Java学习资料(非常经典)

Java 是全球最受欢迎的编程语言之一&#xff0c;在世界编程语言排行榜 TIOBE 中&#xff0c;Java 一直霸占着前三名&#xff0c;有好多年甚至都是第一名。 最近几年&#xff0c;全球约有 1/3 的专业程序员将 Java 作为主要编程语言&#xff0c;这一比例在我国更是高达 1/2&…

CTFSHOW web入门 java反序列化篇 web855

web855 得到源码后看到readObject里面有两条路可以走 1、写文件&#xff0c;文件名固定&#xff0c;文件内容开头固定后面内容可以通过write写入 2、执行命令&#xff0c;但是shellcodoe是不可控的&#xff08;static&#xff09; 如果两条路分开来看都没啥可利用的价值&…

ai绘画新功能上线,说一句话就能生成好看的AI画作

ai绘画可以将自己的图片生成二次元&#xff0c;还可以通过关键词描述生成好看的画作&#xff0c;这些我们都早已尝试过了&#xff0c;并且也玩得不亦乐乎&#xff0c;但AI绘画还能进行语音创作&#xff0c;只需要同AI说一句话&#xff0c;它就能创造出相关的画作&#xff0c;所…

【Linux开发笔记】VSCode+WSL——Windows搭建最轻量便捷的Ubuntu/Linux开发环境

1.概述 我们一般搭建Ubuntu开发环境都是采用VMware或者VirtualBox的虚拟机安装Ubuntu的方案&#xff0c;但是这样的方案会有几个弊端&#xff1a; 安装、启动慢&#xff1b;使用图形桌面时卡顿、鼠标不跟手、打字有延迟&#xff1b;磁盘空间占用比较大&#xff1b;内存资源占用…