当前位置: 首页 > news >正文

JavaEE:多线程(2):线程状态,线程安全

目录

线程状态

线程安全

线程不安全

加锁

互斥性

可重入 

死锁

死锁的解决方法 

Java标准库中线程安全类

内存可见性引起的线程安全问题

等待和通知机制

线程饿死

wait

notify


线程状态

就绪:线程随时可以去CPU上执行,也包含在CPU上执行的线程

阻塞:这个线程暂时不方便去CPU上执行

Java中,线程有下面几个状态

1. NEW:Thread对象创建好了,但还没有调用start在系统中创建线程

2. TERMINATED:Thread对象仍然存在,但是系统内部的线程已经执行完毕了

3. RUNNABLE:就绪状态,表示这个线程随时可以去CPU上执行,或者在CPU上执行的线程

4. TIMED_WAITING:指定时间的阻塞,到达一定时间后会自动解除阻塞(sleep或者带有超时时间的join)

5. WAITING:不带时间的阻塞(死等),一定要满足某个条件才能解除阻塞(join或者wait)

6. BLOCKED:由于锁竞争引起的阻塞

 

⚠只有处于NEW状态才能start

 

如果某个进程卡住了,也可以使用jconsole来查看线程的状态


线程安全

之前提过,引入多线程是为了实现并发编程,但是不能仅仅只靠多线程,因为这里面的问题和注意事项很多

后面一些其他的编程语言引入了封装层次更高的并发编程模型

erlang: actor模型

go: csp模型

js: 基于定时器/异步 模型

python:主要使用基于时间循环模型


线程安全:某个代码无论是在单个线程下执行还是在多个线程下执行都不会产生bug

线程不安全

按照常理count = 5w+5w = 10w,但是打印结果远低于10w

而且每次打印的结果不同

这里的count++其实是由三个CPU指令构成的

1)load 从内存读取数据到CPU寄存器

2)add 把寄存器中的值 +1

3) save 把寄存器的值写回到内存中

如果两个线程并发执行上面的操作,因为线程之间调度的顺序是不确定的,所以会存在变数

除了上面的,还能画出无数种情况。这也是多线程复杂性的缘由,每次写多线程代码都要遍历千万种时间线,确保每种时间线都没有bug

诶为什么有无数种情况嘞?

完全有可能t2执行一次++,而t1执行了2次/3次/更多次++,再排列组合,就会产生很多种情况了


为什么上面那张图第一和第二种执行结果是对的?

t1执行add之后把结果1保存到t1寄存器上,save结果把count = 0改为1;

t1执行add之后把结果2保存到t2寄存器上,save结果把count = 1改为2

(两个线程有各自的上下文(有各自一套寄存器的值),不会相互影响)

后面的情况呢?

如果程序自增的数目越小,count的差异越难看出来。可能导致第一个线程算完了,第二个线程还没开始运行。


线程不安全的原因总结

1. 操作系统上的线程“抢占式执行”“随机调度”,线程之间的执行顺序不确定(根本原因)

2. 代码结构。代码中多个线程同时修改同一个变量(单线程修改,多线程读取同一个变量没问题;多线程修改不同变量没问题)

(所以之前学过的String对象不可变的特性就是线程安全的)

3. 上面的多线程操作本身不是原子的。(直接原因)

count++需要多个CPU指令,可能一个线程的指令还没执行完就给调度走,给其他线程可乘之机

单个CPU指令就是原子的,要么执行完,要么不执行

(可以通过加锁把这多个CPU指令打包成一个整体,实现线程间的锁竞争)

4. 内存可见性问题

5. 指令重排序问题


加锁

互斥性

加锁具有互斥,排他的特性,一般采用synchronized关键字(调用系统api进行加锁)

加锁前需要准备好一个锁对象,依托于这个锁对象进行加锁和解锁操作

如果一个线程针对一个对象加锁之后,其他线程尝试对这个对象进行加锁操作就会被阻塞(锁冲突/锁竞争),阻塞到前一个线程释放锁为止

即使指令会被调度,但是其他线程也无法插手

加锁VS串行

这里给t1和t2加锁,只是让count++这个操作串行执行;而for循环还是并行执行的。这样的运行效率还是会比t1,t2串行执行高的

⚠不存在锁竞争(可能线程不安全)的情况:

1.一个线程加锁,另一个线程不加锁

2.两个线程加的锁对象不同

⚠关于锁的混淆

class Test{public int count = 0;public void add(){synchronized (this){count++;}}
}
public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Test t = new Test();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {t.add();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {t.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count: " + t.count);}
}

这段代码执行效果是怎样的?this指的是啥?

this指向t,而两个线程都调用t.add()方法count++

所以两个线程存在锁竞争

这里的this也可以替换成Test.class,获取Test的类对象,实例化后也就是t

(.class指的是类对象,包含了类的各种信息:类名,属性,访问限定符,参数,接口等)

还可以这么写,之前学过的StringBuilder和StringBuffer就是这么写的,可以保障线程安全

在关键方法前加上synchronized

(如果synchronized加到static方法上,就是给类对象加锁)


可重入 

问题:下面的代码可以运行后打印hello吗? 

    public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(()->{synchronized (locker){synchronized (locker){System.out.println("hello");}}});t.start();}

常规认为此时第一个locker对象已经处于加锁状态,这时候如果再尝试对locker加锁不就会阻塞吗?

但是运行完之后发现真能打印hello

关键在于这两次加锁其实是同一个线程再运行的

当前由于是同一个线程,此时锁对象就知道了第二次加锁的线程就是持有锁的线程,第二次的操作就可以直接放行通过,不会出现阻塞

Java里面用synchronized加锁,利用了它的可重入特性

真正的加锁,把计数器+1,说明当前这个对象被加锁一次,同时记录线程是谁

即使上述synchronized嵌套10层8层的,也不会使解锁操作混乱,始终能够保证在正确的时机解锁,这里的计数器就是用来识别解锁时机的关键要点


死锁

场景:1.一个线程,一把锁,如果锁是不可重入锁,并且一个线程对这把锁加锁两次,就会出现死锁

2. 两个线程,两把锁。线程1获取到锁A,线程2获取到锁B,1尝试获取锁B,2尝试获取锁A,就出现死锁了

    public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(()->{synchronized (A){//sleep一下,给t2时间能拿到Btry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//尝试获取B,但没有释放Asynchronized (B){System.out.println("t1拿到两把锁");}}});Thread t2 = new Thread(()->{synchronized (B){//sleep一下,给t1时间能拿到Atry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//尝试获取A,但是没有释放Bsynchronized (A){System.out.println("t2拿到两把锁");}}});t1.start();t2.start();}

这段代码就是一个例子,t1和t2谁都不让谁,造成了死锁。结果就是什么都打印不了

用jconsole查看线程情况,发现在t1线程第17行代码卡住了

t2线程在32行卡住了

3. N个线程M把锁

先来说一个经典的哲学家就餐问题

现在有5个滑稽哲学家(每个哲学家就是一个线程),围着餐桌吃一碗面条,放在他们面前有5筷子(5只筷子相当于5把锁),每个哲学家除了吃面还要思考人生

当某个线程拿到锁的时候就会一直持有,除非他吃完了,主动放下筷子(释放锁),其他滑稽不能硬抢筷子

由于每个哲学家啥时候吃面,啥时候思考人生这件事不确定,所以绝大部分情况下,这个模型可以正常运行

但是如果所有哲学家都想吃面,都先去拿左手的筷子,一只筷子哪够啊。等他们再去拿右手的筷子时,发现右手的筷子已经被右边的人拿了,此时就产生了死锁


死锁的解决方法 

产生死锁的四个必要条件

1.互斥使用,获取锁的过程是互斥的。一个线程拿到锁,另一个线程只能阻塞等待(最基本特性)

2.不可抢占。一个线程拿到锁之后,只能主动解锁,不能让别的线程强行把锁抢走(最基本特性)

3.请求保持。一个线程拿到锁A之后,持有A的前提下尝试获取B(代码结构)

4.循环等待/环路等待(代码结构,最容易破坏)

解决死锁问题,核心思路就是破坏上述四个必要条件任意一个

几种方案:

1.引入额外的筷子(锁)

2.去掉一个哲学家(线程)

3.引入计数器,限制最多多少人吃面

4.引入加锁顺序规则

5.银行家算法

实际开发中,我们一般采用第4个方案

比如对上面的5个哲学家,针对5只筷子进行编号,约定每个哲学家获取筷子的时候,一定要先获取编号小的筷子,后获取编号大的筷子

按照这个逻辑,1号滑稽需要先获取1号筷子,此时其他人获取1号筷子就会阻塞等待

5号筷子就是空闲的


用这个思路,我们可以把上面第2个场景的代码进行修改,约定t2获取锁的顺序,让t2先获取锁A再获取锁B

        Thread t2 = new Thread(()->{synchronized (A){//sleep一下,给t1时间能拿到Atry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (B){System.out.println("t2拿到两把锁");}}});

Java标准库中线程安全类

上面的几个类自带锁,在多线程环境下使用,出现问题的概率比较小


内存可见性引起的线程安全问题

如果一个线程写,一个线程读,是否会有线程安全问题?

    private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){//循环体里面先不写东西}System.out.println("t1进程结束");});Thread t2 = new Thread(()->{System.out.println("输入flag的值:");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}

这个代码预期只要t2里面输入的不是0就能让t1线程结束

但是最终效果是,我们输入一个非0的值的时候,t1没有真的结束

我们来分析一下问题所在

这个循环有2条核心指令

1.load读取内存中flag的值到CPU寄存器里

2.拿着寄存器的值和0进行比较(条件跳转)

执行指令的速度是很快的,在用户指令输入前几秒钟,循环已经执行了上百亿次

1.每次load操作执行的结果每次都是一样的

2.load操作开销远远超过条件跳转

load执行很多次,每次结果都一样,而且每次开销巨大,这让JVM产生怀疑,不知道你这里的load操作是否有必要

JVM很激进,直接把load操作优化了(只有前几次进行load操作,后面没发现flag有变化直接干掉load,干掉之后就不再重读读内存,直接使用寄存器之前缓存的值,能大幅度提高循环执行速度)

所以这部分代码,相当于t2修改了内存,但是t1没有看到这个内存的变化,就称为内存可见性问题

内存可见性,高度依赖编译器的优化具体实现,啥时候触发啥时候不触发不清楚

我们暴力一点,直接用sleep让t1线程暂停循环,暂停的过程中编译器的优化就不会触发,进程也就能顺利结束了

还有一种方法,Java提供了volatile关键字来强制读取内存,使上述的优化被强制关闭,确保每次循环条件都会重新从内存中读取数据

这种方法,牺牲了速度,但是保证了准确性

拓展:JMM模型--Java规范文档上的一个概念


等待和通知机制

wait和notify:和join用途类似。因为多个线程之间存在随机调度,所以这里引入wait和notify是为了能从应用层面上干预到多个不同线程代码的执行顺序。

join:等待另一个线程执行完,才继续执行

wait:等待,当另一个线程收到notify通知之后就结束等待,不要求另一个线程必须执行完

换句话说,在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让先执行的线程先把对应的代码执行完了。

这里的干预指的是不影响系统的调度策略,因为内核里调度线程仍然是无序调度。

线程饿死

假设有一台ATM机器,有四个老铁要来取钱。

1号牢铁(线程)进去取钱,把ATM一锁,后面排队的人就只能等他取完后才能进去。1号进去后发现ATM里面居然没有钱!那他就只能出来等运钞车给ATM补钱。

1号牢铁释放了锁,其他牢铁就会来竞争这把锁,而且1号牢铁也可以参与到锁竞争中。如果好巧不巧又让1号拿到了锁进去了,发现没钱又出来,再竞争锁。。。。

如此循环,其他牢铁(线程)根本不可能拿到锁进入ATM

(补充:1号线程一直拿得到锁的情况发生概率还是蛮高的。

1号先拿到了锁,处于RUNNABLE状态;而其他线程因为锁冲突出现阻塞,处于BLOCKED状态。

BLOCKED状态的线程需要系统进行唤醒之后才能参与到锁竞争中(唤醒比较慢),1号线程不用被唤醒就可以直接参与竞争)

这种情况叫做“线程饿死”

那怎么解决线程饿死呢?

这里1号牢铁发现自己要执行的逻辑的前提条件不具备(ATM里面没钱),这时1号牢铁就要放弃参与到锁竞争中(进入线程阻塞状态),一直等到条件具备了(ATM里面又有钱了)再去解除阻塞,参与到锁竞争中。

这时候就可以用到wait和notify了,让1号滑稽看看当前条件(ATM是否有钱)是否满足,不满足就wait,其他线程如果让条件满足之后,再通过notify唤醒1号滑稽

对应的代码

这里的wait做了三件事:

1.释放锁;

2.进入阻塞等待;

3.当其他线程调用notify的时候,wait解除阻塞,并重写获取到锁


wait

public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException {Object object = new Object();object.wait();}
}

1. 随便拿个对象都可以进行wait

2. 直接调用wait会出现监视器异常

⚠wait进来要先释放锁,释放锁的前提就是能拿到锁,而sychronized也叫做监视器锁,wait没有放到sychronized里面的话就会出现监视器异常

        Object object = new Object();synchronized (object){// 调用wait的对象一定要和synchronized中的锁对象是一致的object.wait();}

wait解的是object的锁,当wait被唤醒后,也能重新获取到object锁

代码执行起来在wait这里阻塞了

此时就需要notify来把它唤醒

wait有三个版本

无参数版本的wait是死等,一直等到notify通知

带有超时时间的wait,当超过预定时间时就停止等待(即使没有notify来通知)


notify

    public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker){System.out.println("t1 wait之前");try {locker.wait();//wait, sleep和1join都可能被interrupt提前唤醒,需要放到try-catch里面} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait之后");}});Thread t2 = new Thread(()->{try {Thread.sleep(1000);synchronized (locker){//Java约定notify放到synchronized里面System.out.println("t2 notify之前");locker.notify();System.out.println("t2 notify之后");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();}

分析代码:

t1线程启动就会调用wait进入阻塞等待,t2线程启动就会先sleep,到时间再进行notify唤醒t1

注意要把sleep写到synchronized外面,否则因为t1和t2执行顺序不确定,可能t2先拿到锁,t1没执行到wait,t2就进行notify了,那逻辑上就有问题了

执行过程:

1.t1执行起来之后,先拿到锁,打印t1 wait之前,进入wait方法

2.t2执行起来之后,先进行sleep,这个sleep可以让t1先拿到锁

3.t2 sleep之后,由于t1在wait状态,锁是释放的。t2就可以拿到锁,接下来打印t2 notify之前,执行notify操作,使t1从1WAITING状态恢复

4.但此时t2还没有释放锁,t1暂时无法获取锁,出现小小的阻塞

5.t2执行完notify之后继续打印t1 wait之后


⚠wait和notify要通过同一个对象来联系

object1.wait();

object2.notify();

此时是无法唤醒的

⚠notifyAll() 唤醒这个对象上所有等待的线程


⚠wait和sleep

wait提供了一个带有超时时间的版本,sleep也能指定时间,时间到解除阻塞继续执行

wait和sleep都可以被提前唤醒:wait由notify唤醒,sleep由interrupt唤醒

使用wait最主要的目标一定是不知道要等多少时间的前提下使用的。而sleep一定要知道要等多少时间才使用

相关文章:

JavaEE:多线程(2):线程状态,线程安全

目录 线程状态 线程安全 线程不安全 加锁 互斥性 可重入 死锁 死锁的解决方法 Java标准库中线程安全类 内存可见性引起的线程安全问题 等待和通知机制 线程饿死 wait notify 线程状态 就绪&#xff1a;线程随时可以去CPU上执行&#xff0c;也包含在CPU上执行的…...

Flutter 自定义AppBar实现滚动渐变

1、使用ListView实现上下滚动。 2、使用Stack&#xff1a;允许将其子部件放在彼此的顶部&#xff0c;第一个子部件将放置在底部。所以AppBar&#xff0c;写在ListView下面。 3、MediaQuery.removePadding&#xff1a;当使用ListView的时候发现&#xff0c;顶部有块默认的Padd…...

编程语言MoonBit新增矩阵函数的语法糖

MoonBit更新 1. 新增矩阵函数的语法糖 新增矩阵函数的语法糖&#xff0c;用于方便地定义局部函数和具有模式匹配的匿名函数&#xff1a; fn init {fn boolean_or { // 带有模式匹配的局部函数true, _ > true_, true > true_, _ > false}fn apply(f, x) {f(x)}le…...

Angular:跨域请求携带 cookie

新建拦截器&#xff0c;设置 XMLHttpRequest&#xff1a;withCredentials 属性 1. 新建文件夹 http-interceptors 该文件夹下可有多个不同用途的拦截器2. 新建拦截器 common.interceptor.ts import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "an…...

【C++】list容器迭代器的模拟实现

list容器内部基本都是链表形式实现&#xff0c;这里的迭代器实现的逻辑需要注意C语言中指针的转换。 list容器如同数据结构中的队列&#xff0c;通常用链式结构进行存储。在这个容器中&#xff0c;我们可以模仿系统的逻辑&#xff0c;在头结点后设置一个“ 哨兵 ”&#xff0c;…...

Docker镜像操作

镜像名称 镜名称一般分两部分组成&#xff1a;[repository]:[tag]。 在没有指定tag时&#xff0c;默认是latest&#xff0c;代表最新版本的镜像。 这里的mysql就是repository&#xff0c;5.7就是tag&#xff0c;合一起就是镜像名称&#xff0c;代表5.7版本的MySQL镜像。 镜像…...

【Java-框架-SpringSecurity】单点登录(认证和授权)- 随笔

项目文件&#xff1b; 【1】 【2】 【3】 【4】 【5】 【6】 【7】 【8】...

大数据开发之Scala

第 1 章&#xff1a;scala入门 1.1 概述 scala将面向对象和函数式编程结合成一种简洁的高级语言 特点 1、scala和java一样属于jvm语言&#xff0c;使用时都需要先编译为class字节码文件&#xff0c;并且scala能够直接调用java的类库 2、scala支持两种编程范式面向对象和函数式…...

数字时代的大对决

数字时代如今正酝酿着一场大对决&#xff0c;浏览器、艺术品、音乐平台和社交通信的巅峰之战正在发生。Brave、Yuga Labs、Audius和Discord分别对标着Chrome、Disney、Spotify和WhatsApp&#xff0c;这场数字时代的较量不仅涉及浏览器、艺术品、音乐平台和社交通信的竞争&#…...

网络防御保护1

网络防御保护 第一章 网络安全概述 网络安全&#xff08;Cyber Security&#xff09;是指网络系统的硬件、软件及其系统中的数据受到保护&#xff0c;不因偶然的或者恶意的原因而遭受到破坏、更改、泄露&#xff0c;系统连续可靠正常地运行&#xff0c;网络服务不中断 随着数…...

解决Windows下Goland的Terminal设置为Git Bash失败

路径不要选错了&#xff1a; 如果还是不行&#xff1a; 把bash路径加进去试试 goland设置Terminal...

x-cmd pkg | jq - 命令行 JSON 处理器

目录 简介首次用户功能特点类似工具进一步探索 简介 jq 是轻量级的 JSON 处理工具&#xff0c;由 Stephen Dolan 于 2012 年使用 C 语言开发。 它的功能极为强大&#xff0c;语法简洁&#xff0c;可以灵活高效地完成从 JSON 数据中提取特定字段、过滤和排序数据、执行复杂的转…...

网络安全笔记

一、简介 网络安全是指通过管理和技术手段保护网络系统免受未经授权的访问、数据泄露、破坏或摧毁。随着互联网的普及&#xff0c;网络安全问题日益突出&#xff0c;对个人和企业信息安全构成了严重威胁。因此&#xff0c;了解和掌握网络安全知识对于保护个人信息和企业数据至…...

php基础学习之代码框架

一&#xff0c;标记 脚本标记&#xff08;已弃用&#xff09;&#xff1a;<script language"php"> php代码 </script> 标准标记&#xff1a;<?php php代码 ?> 二&#xff0c;基础输出语句 不是函数&#xff0c;…...

LCD-LMD-PSO-ELM的电能质量分类,LCD特征提取,LMD特征提取,粒子群算法优化极限学习机

目录 背影 极限学习机 LCD-LMD-PSO-ELM的电能质量分类,LCD特征提取,LMD特征提取,粒子群算法优化极限学习机 主要参数 MATLAB代码 效果图 结果分析 展望 完整代码下载链接:LCD-LMD-PSO-ELM的电能质量分类,LCD特征提取,LMD特征提取,粒子群算法优化极限学习机资源-CSDN文库…...

【ARMv8M Cortex-M33 系列 7 -- RA4M2 移植 RT-Thread 问题总结】

请阅读【嵌入式开发学习必备专栏 】 文章目录 问题小结栈未对齐 经过几天的调试&#xff0c;成功将rt-thead 移植到 RA4M2&#xff08;Cortex-M33 核&#xff09;上&#xff0c;thread 和 shell 命令已经都成功支持。 问题小结 在完成 rt-thread 代码 Makefile 编译系统搭建…...

分享 GitHub 上的敏感词汇工具类:sensitive-word

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…...

洛谷P1319 压缩技术(C语言)

这样一道入门题目&#xff0c;本来可以用for循环直接操作&#xff0c;但作者异想天开(xian de dan teng)地把所有数据登记在一个数组里面&#xff0c;然后再统一按格式输出。也就是定义一个数组Map&#xff0c;大小为n成n&#xff0c;然后按照输入数据&#xff0c;把Map中每一个…...

HQL,SQL刷题简单查询,基础,尚硅谷

今天刷SQL简单查询&#xff0c;大家有兴趣可以刷一下 目录 相关表数据&#xff1a; 题目及思路解析&#xff1a; 总结归纳&#xff1a; 知识补充&#xff1a; 关于LIKE操作符/运算符 LIKE其他使用场景包括 LIKE模糊匹配情况 相关表数据&#xff1a; 1、student_info表 2、sc…...

MSG3D

论文在stgcn与sta-lstm基础上做的。下面讲一下里面的方法&#xff1a; 1.准备工作 符号。这里是对符号进行解释。 一个人体骨骼图被记为G(v,E) 图卷积&#xff1a; 图卷积定义 考虑一种常用于处理图像的标准卷积神经网络 (CNN)。输入是像素网格。每个像素都有一个数据值向…...

kafka(二)——常用命令

常用脚本 kafka执行脚本默认在安装的bin目录下&#xff0c;本文中示例均基于bin目录执行。 #查询topic状态&#xff0c;新建&#xff0c;删除&#xff0c;扩容 kafka-topics.sh #查看&#xff0c;修改kafka配置 kafka-configs.sh #配置&#xff0c;查看kafka集群鉴权信息 kaf…...

使用Flink处理Kafka中的数据

目录 使用Flink处理Kafka中的数据 前提&#xff1a; 一&#xff0c; 使用Flink消费Kafka中ProduceRecord主题的数据 具体代码为&#xff08;scala&#xff09; 执行结果 二&#xff0c; 使用Flink消费Kafka中ChangeRecord主题的数据 具体代码(scala) 具体执行代码① 重要逻…...

跟着pink老师前端入门教程-day07

去掉li前面的项目符号&#xff08;小圆点&#xff09; 语法&#xff1a;list-style: none; 十五、圆角边框 在CSS3中&#xff0c;新增了圆角边框样式&#xff0c;这样盒子就可以变成圆角 border-radius属性用于设置元素的外边框圆角 语法&#xff1a;border-radius:length…...

Pixelmator Pro Mac版 v3.5 图像处理软件 兼容 M1/M2

在当今数字化时代&#xff0c;图像编辑软件成为了许多人必备的工具之一。无论您是摄影师、设计师还是普通用户&#xff0c;您都需要一款功能强大、易于使用的图像编辑软件来处理和优化您的照片和图像。而Pixelmator Pro for Mac正是满足这一需求的理想选择。 Pixelmator Pro f…...

《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(15)-Fiddler弱网测试,知否知否,应是必知必会

1.简介 现在这个时代已经属于流量时代&#xff0c;用户对于App或者小程序之类的操作界面的数据和交互的要求也越来越高。对于测试人员弱网测试也是需要考验自己专业技术能力的一种技能。一个合格的测试人员&#xff0c;需要额外关注的场景就远不止断网、网络故障等情况了。还要…...

【vscode】远程资源管理器自动登录服务器保姆级教程

远程资源管理器自动登录服务器 介绍如何配置本地生成rsa服务端添加rsa.pub配置config文件 介绍 vscode SSH 保存密码自动登录服务器 对比通过账号密码登录&#xff0c;自动连接能节约更多时间效率&#xff0c;且通过vim修改不容易发现一些换行或者引号导致的错误&#xff0c;v…...

写点东西《Javascript switch 语句的替代方法》

写点东西《Javascript switch 语句的替代方法》 那么 switch 语句有什么问题&#xff1f; Object Literal 查找的替代方法 将我们学到的东西变成一个实用函数 您需要的一切都在一个地方# [](#javascript-version) Javascript 版本Tyepscript version&#x1f31f;更多精彩 本文…...

python学习笔记10(循环结构2)

&#xff08;一&#xff09;循环结构2 1、扩展模式 语法&#xff1a; for 循环变量 in 遍历对象&#xff1a; 语句块1 else: 语句块2 说明&#xff1a;else在循环结束后执行&#xff0c;通常和break和continue结合使用 2、无限循环while while 表达式&#xff1a; 语句块…...

Codefroces 191A - Dynasty Puzzles

思路 d p dp dp d p i , j dp_{i,j} dpi,j​ 表示以 i i i 开始以 j j j 结尾的最长长度。方程&#xff1a; d p j , r m a x ( d p j , l , d p j , l l e n g t h l , r ) dp_{j,r}max(dp_{j,l}\;,\;dp_{j,l}length_{l,r}) dpj,r​max(dpj,l​,dpj,l​lengthl,r​) 有点区…...

HIVE中关联键类型不同导致数据重复,以及数据倾斜

比如左表关联键是string类型&#xff0c;右表关联键是bigint类型&#xff0c;关联后会出现多条的情况 解决方案&#xff1a; 关联键先统一转成string类型再进行关联 原因&#xff1a; 根据HIVE版本不同&#xff0c;数据位数上限不同&#xff0c; 低版本的超过16位会出现这种…...