Java核心知识体系8:Java如何保证线程安全性

1 Java内存模型(JMM) 如何解决并发问题

维度1:使用关键字、属性进行优化JMM本质实际就是:Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。这些方法包括了:

  • volatile、synchronized 和 final 关键字

  • Happens-Before 规则

维度2:从 顺序一致性、可见性、有序性、原子性角度

  • 顺序一致性

一个线程中的所有操作按照程序的顺序执行,不受其他线程的影响。

  • 原子性

Java程序中,对数据的读和写操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行,否则会产生问题。通过下面的案例可以看出,哪些是原子操作,哪些是非原子操作:

// 1个动作,线程直接将值赋给idx,也就是直接写到内存中
idx = 100

// 3个动作:先定义 jdx,再读取idx的值,最后赋值给jdx
jdx := idx

// 3个动作:读取jdx的值,进行加1操作,然后新值重新写入新的值
jdx ++

从上面的案例中可以看中,只有第一个例子才是具备原子性的,因为他只有一个存的动作。至于其他的例子,包含读取、操作、赋值等多个动作,有一个动作失败则不成立。所以,基本读取和赋值,Java内存模型可以保证原子性操作,如果要实现更大范围、步骤更多的操作的原子性,则需要通过synchronized或者Lock来实现。synchronized和Lock的存在是为了够保证任一时刻只有一个线程能够执行该代码块,这样也就解决了原子性。

  • 可见性

Java提供了volatile关键字来保证可见性,使用volatile来修饰共享变量,可以保证修改的值立即更新到主存中。这样其他线程读取数据时,始终都会从内存中读取到新值。而普通的共享变量不能保证可见性,因为修改之后,不确定什么时候被写入主存,当其他Thread去读取时,内存中很有可能还是原来的旧值,所以无法保证可见性。另外,通过synchronized关键字和Lock功能也能够保证可见性,因为能限制同一时刻只有一个线程获取锁然后执行同步代码,且在释放之前会将变量的修改更新到主存中。所以实时可见。

  • 有序性

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外,通过synchronized关键字和Lock功能也能够保证可见性,因为能限制同一时刻只有一个线程获取锁然后执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。注:JMM是通过Happens-Before 规则来保证Thread操作有序性。

2.1 关键字: volatile、synchronized 和 final

在Java中,volatile、synchronized和final是三个非常重要的关键字,它们都与并发编程密切相关。下面是对这三个关键字的详细介绍:

2.1.1 volatile

volatile是Java中的一种修饰符,它用于声明一个共享变量,以确保多个线程对该变量的访问是可见的和有序的。volatile关键字的作用是禁止指令重排和强制刷新缓存,以保证操作的顺序性和可见性。当一个变量被声明为volatile时,它表示该变量的值可能会被意想不到地改变。编译器和处理器会注意到这个变量的特殊性,并采取相应的措施来保证多个线程对该变量的访问是正确的。具体来说,volatile关键字会禁止编译器对volatile变量进行优化,每次读取该变量时都会直接从它的内存地址中读取,而不是从寄存器或缓存中读取。同时,volatile关键字也会强制处理器在每个操作该变量的指令之后立即刷新缓存,以保证其他线程能够看到最新的值。需要注意的是,虽然volatile关键字可以保证可见性和有序性,但它并不能保证原子性。也就是说,如果一个操作包含多个步骤,而这些步骤不能被一个指令替换,那么这个操作就不能被保证为原子性。在这种情况下,需要使用锁或者其他同步机制来保证原子性。

2.1.2 synchronized

synchronized是Java中的一种关键字,它用于实现同步代码块和方法。synchronized关键字可以保证同一时刻只有一个线程能够执行被synchronized修饰的代码块或方法。synchronized关键字会创建一个锁对象或锁标识符,当一个线程获取了这个锁对象或锁标识符后,其他线程就不能再获取这个锁对象或锁标识符,直到第一个线程释放了这个锁对象或锁标识符。synchronized关键字可以保证多个线程对共享变量的访问是互斥的,也就是说在同一时刻只有一个线程能够访问共享变量。这样可以避免多个线程同时修改共享变量而导致数据不一致的问题。同时,synchronized关键字还可以保证多个线程之间的操作是有序的,即一个线程在执行synchronized代码块或方法之前必须等待其他线程完成之前的操作。需要注意的是,synchronized关键字虽然可以保证互斥性和有序性,但它并不能保证原子性。也就是说,如果一个操作包含多个步骤,而这些步骤不能被一个指令替换,那么这个操作就不能被保证为原子性。在这种情况下,需要使用其他同步机制来保证原子性。

2.1.3 final

final是Java中的一种修饰符,它用于声明一个最终变量或方法。final关键字表示该变量或方法不能被修改或重写。具体来说,final关键字可以用于声明一个常量,该常量的值不能被修改;也可以用于声明一个方法,该方法不能被重写。final关键字在并发编程中也有着重要的作用。final关键字可以保证一个共享变量的值只被一个线程修改,这样可以避免多个线程同时修改共享变量而导致数据不一致的问题。同时,final关键字还可以保证一个方法的执行不会被其他线程中断或干扰,这样可以保证方法的原子性和可见性。需要注意的是,final关键字并不能保证多个线程之间的操作是有序的。也就是说,在一个线程中执行final方法时,其他线程可能会同时执行自己的操作,而这些操作之间是没有顺序关系的。在这种情况下,需要使用其他同步机制来保证操作的顺序性。

2.2 Happens-Before 规则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,在JVM 中还有Happens-Before规则,用来确定并发操作之间的顺序关系。Happens-Before规则定义了以下几种顺序关系:

2.2.1 程序顺序规则(Program Order Rule)

在一个程序中,按照代码的顺序,先执行的操作Happens-Before后执行的操作。这意味着在程序中,如果一个操作先于另一个操作执行,那么这个操作的结果对后续操作是可见的。

image

2.2.2 管程锁定规则(Monitor Lock Rule)

一个unlock操作先行发生于后面对同一个锁的lock操作。

image

2.2.3 volatile变量规则(Volatile Variable Rule)

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,先写后读。

image

2.2.4 线程启动规则(Thread Start Rule)

Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

image

2.2.5 线程加入规则((Thread Join Rule)

Thread 对象的结束先行发生于 join() 方法返回。

image

2.2.6 线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法和Thread.isAlive()的返回值等手段检测线程是否已经终止执行

2.2.7 线程中断规则( Thread Interruption Rule)

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

2.2.8 对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

2.2.9 传递性(Transitivity)

如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

3 线程安全性能讨论

在多线程环境中,一个类或者一个函数不管在何种运行时环境或交替执行方式,都能保证正确的行为,被安全的调用,就说明线程是安全的。这个“正确的行为”通常包括原子性、可见性和有序性。但是线程安全不是非真即假,共享数据按照安全程度的强弱顺序可以分成以下五类:

  • 不可变

  • 绝对线程安全

  • 相对线程安全

  • 线程兼容

  • 线程对立

按照线程安全性的强弱顺序,不可变 > 绝对线程安全 > 相对线程安全 > 线程兼容 > 线程对立。

3.1 不可变(Immutable)

不可变的对象在创建后其状态就不能被修改,因此它们自然是线程安全的。任何线程在任何时候访问这些对象,都会看到相同的数据。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。不可变的类型包括:

  • final 关键字修饰的基本数据类型

  • String

  • 枚举类型

  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

XXX 可以是Map、List、Set

public class ImmutableClass {
    public static void main(String[] args) {
        Map<String, Integer> testMap = new HashMap<>();
        Map<String, Integer> testUnmodifiable = Collections.unmodifiableMap(testMap);
        testUnmodifiable.put("input-a", 1);
    }
}

执行时抛出异常

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$testUnmodifiable.put(Collections.java:1523)
    at ImmutableExample.main(ImmutableClass.java:9)

不可变状态还可以这么理解,外部无法对数据状态进行修改,比如

public class ImmutableClass {  
    private final int value;  
  
    public ImmutableClass(int value) {  
        this.value = value;  
    }  
  
    public int getValue() {  
        return value;  
    }  
}

在这个例子中,ImmutableClass是不可变的,因为它的构造函数是私有的,外部无法修改其状态。因此,多个线程同时访问和获取ImmutableClass对象的值时,不会出现数据不一致的问题。

3.2 绝对线程安全(Absolute Thread Safety)

绝对线程安全的对象无论运行时环境如何,调用者都不需要任何额外的同步措施。这通常需要付出较大的代价来实现。

public class ThreadSafeClass {  
    private int value;  
  
    public synchronized void setValue(int value) {  
        this.value = value;  
    }  
  
    public synchronized int getValue() {  
        return value;  
    }  
}

在这个例子中,ThreadSafeClass的每个方法都使用了synchronized关键字进行同步。这保证了无论多少个线程同时访问ThreadSafeClass的对象,每个线程的操作都会被串行执行,不会出现数据竞争的问题。

3.3 相对线程安全(Relative Thread Safety)

相对线程安全的对象需要保证单个操作是线程安全的,在调用的时候不需要做额外的保障措施。但在连续调用时可能需要额外的同步措施来保证调用的正确性。Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。以Hashtable为例,因为它的每个方法都是同步的。但是,如果多个线程连续调用Hashtable的不同方法(如put和get),仍然可能出现竞态条件。为了避免这种情况,调用者需要在外部进行额外的同步。

在下面代码中,如果Vector中的一个元素被线程A删除,而线程B试图获取一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。

public class VectorUnsafeExample {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 100; i++) {
                vector.add(i);
            }
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            });
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.get(i);
                }
            });
            executorService.shutdown();
        }
    }
}

Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
    at java.util.Vector.remove(Vector.java:831)
    at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
    at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。

# 独立线程A执行删除操作
executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
        }
    }
});
# 独立线程B执行读取操作
executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.get(i);
        }
    }
});

3.4 线程兼容(Thread Compatibility)

线程兼容的对象本身不是线程安全的,但可以通过在调用端添加额外的同步措施来保证在多线程环境下的安全使用。Java API 中大部分的类都是属于线程兼容的,比如ArrayList类就不是线程安全的。如果多个线程同时修改ArrayList,可能会导致数据不一致。但是,如果调用者在修改ArrayList时使用synchronized块或其他同步机制进行同步,就可以保证线程安全。

public class ThreadCompatibleClass {  
    private int value;  
  
    public void setValue(int value) {  
        this.value = value;  
    }  
  
    public int getValue() {  
        return value;  
    }  
}

在这个例子中,ThreadCompatibleClass的方法没有使用synchronized关键字进行同步。因此,如果多个线程同时修改ThreadCompatibleClass的对象,可能会导致数据不一致。

3.5 线程对立(Thread Hostility)

线程对立的对象无论如何都无法在多线程环境下并发使用,即使采取了同步措施。一个典型的例子是Java中的ThreadLocalRandom类。这个类用于生成随机数,并且每个线程都有其自己的随机数生成器实例。由于每个线程使用不同的实例,因此无需担心线程安全问题。但是,如果尝试在没有正确初始化ThreadLocalRandom的情况下跨线程使用它,就可能导致问题。

这种情况下,即使添加了同步措施也无法保证线程安全。

4 如何实现线程安全

4.1 synchronized关键字/ReentrantLock特性

  • synchronized关键字

在Java中,synchronized关键字是一种内置的同步机制,用于控制多个线程对共享资源的访问。它用于在并发环境中保护代码块,确保同一时刻只有一个线程可以执行该代码块。synchronized关键字可以应用于方法或代码块。当它应用于方法时,它将锁住该方法的对象。当它应用于代码块时,它将锁住指定的锁对象。

public class SynchronizedExample {  
    private int count = 0;  
  
    public synchronized void incrementCount() {  
        count++;  
    }  
}

上面这个例子中,incrementCount()方法使用了synchronized关键字。这意味着在任何时刻,只有一个线程可以执行该方法。如果有其他线程试图同时执行该方法,它们将会被阻塞,直到当前线程完成该方法的执行。

  • ReentrantLock特性

ReentrantLock 是 Java 中的一个可重入锁,它是一种比 synchronized 关键字更灵活的线程同步机制。ReentrantLock 允许一个线程多次获取同一个锁,而不会产生死锁。它也支持公平锁和非公平锁,可以根据实际需求进行选择。下面是一个使用 ReentrantLock 的示例:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockExample {  
    private final ReentrantLock lock = new ReentrantLock();  
    private int count = 0;  
  
    public void incrementCount() {  
        lock.lock();  
        try {  
            count++;  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    public int getCount() {  
        return count;  
    }  
}

在上面的这个例子中,我们定义了一个 ReentrantLock 和一个计数器 count。incrementCount() 方法使用 lock.lock() 获取锁,然后增加计数器的值,最后使用 lock.unlock() 释放锁。getCount() 方法直接返回计数器的值,无需获取锁。这种方式比使用 synchronized 关键字更灵活,因为它可以细粒度地控制需要同步的代码块,而不是整个方法。

★ 后续的章节会详细的介绍 synchronized关键字和ReentrantLock特性,敬请期待

4.2 非阻塞同步

在JAVA中,互斥同步最主要的问题就是线程阻塞和唤醒所带来的开销导致的性能问题,这种同步也称为阻塞同步,是一种悲观的并发策略,无论共享数据是否真的会出现竞争,它都要进行加锁,这样 用户态核心态转换、维护锁计数器和阻塞检查、线程唤醒等操作都会产生大量的开销。非阻塞同步是指在多线程环境下,不需要使用阻塞等待的方式来实现同步控制,线程可以一直进行计算操作,而不会被阻塞。下面介绍几种手段实现非阻塞同步。

  1. CAS随着硬件指令集水平的发展,我们经常使用基于冲突检测的乐观并发策略: 先执行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(始终重试,直至成功)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期原值A相匹配,则将内存位置的值更新为B,否则不进行任何操作。在并发环境中,CAS操作可以保证数据的一致性和线程安全性。

  1. AtomicInteger

AtomicInteger是Java中的一个原子整数类,它提供了原子操作的更新方法,可以在多线程环境下安全地更新共享的整数变量。AtomicInteger的更新方法包括incrementAndGet()、getAndIncrement()、decrementAndGet()、getAndDecrement()、compareAndSet()等,它们使用了 Unsafe 类的 CAS 操作,保证对共享变量的操作是原子性的。

以下代码使用了 AtomicInteger 执行了计数操作。

import java.util.concurrent.atomic.AtomicInteger;  
  
public class AtomicIntegerExample {  
    private static AtomicInteger counter = new AtomicInteger(0);  
  
    public static void main(String[] args) {  
        // 启动10个线程,每个线程将计数器加10  
        for (int i = 0; i < 10; i++) {  
            new Thread(() -> {  
                for (int j = 0; j < 10; j++) {  
                    counter.incrementAndGet();  
                }  
            }).start();  
        }  
  
        // 等待所有线程执行完毕  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        // 输出计数器的值  
        System.out.println("Counter: " + counter);  
    }  
}

在这个示例中,我们使用AtomicInteger来维护一个计数器的值,并启动了10个线程,每个线程将计数器加10次。由于AtomicInteger提供了原子操作的更新方法,因此即使多个线程同时更新计数器的值,也不会出现线程安全问题。最后,我们输出计数器的值,可以看到它应该是100(10个线程每个线程执行10次计数器加1操作)。

  1. ABA

如果某个线程将变量A更改为B后再更改为A,那么另一个等待CAS操作的线程会认为该变量没有发生过改变,仍然是A,然后执行CAS操作。这样就可能导致数据的不一致。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

另外,Java 8引入了一种新的原子类:LongAdder和LongAccumulator,它们内部采用了分段化的思想来解决高并发下的ABA问题。它们将内部变量分为一个数组,每个线程更新自己的分段,最后再合并结果。这种方式既解决了ABA问题,又提高了并发性能。

4.3 无同步方案

换一个思路,如果没有方法的计算不涉及共享数据,不需要进行同步,是不是就不需要任何同步措施去保证正确性,也就没有线程安全的问题。

  • 栈封闭:多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

  • 线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

  • 可重入代码(Reentrant Code):可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

这块简单介绍,后续会有专门的章节进行学习

5 总结

  • 了解了多线程产生的原因,以及线程不安全的原因

  • 从 可见性,原子性和有序性 来阐述并发状态下线程不安全的原因

  • 分析了Java是怎么解决并发问题的

文章转载自:Hello-Brand

原文链接:https://www.cnblogs.com/wzh2010/p/17840659.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux多线程:线程池(单例),读写锁

目录 一、线程池&#xff08;单例模式&#xff09;1.1 makefile1.2 LockGuard.hpp1.3 log.hpp1.4 Task.hpp1.5 Thread.hpp1.6 ThreadPool.hpp1.7 main.cc 二、STL,智能指针和线程安全2.1 STL中的容器是否是线程安全的?2.2 智能指针是否是线程安全的? 三、其他常见的各种锁四、…

Mac OS 13+,Apple Silicon,删除OBS虚拟摄像头(virtual camera),

原文链接: https://www.reddit.com/r/MacOS/comments/142cv OBS为了捕获摄像头视频,将虚拟摄像头插件内置为系统插件了.如下 直接删除没有权限的,要删除他,在mac os 13以后,需要关闭先关闭苹果系统的完整性保护(SIP) Apple 芯片(M1,....)的恢复模式分为两种,回退恢复模式,和…

支持TrustZone®的R7FA4M2AC3CFM、R7FA4M2AD3CFM、R7FA4M2AD3CFP、R7FA4M2AC3CFP高性能32位微控制器

产品简介 RA4M2 32 位微控制器 (MCU) 产品群使用支持 TrustZone 的高性能 Arm Cortex-M33 内核。 与片内的 Secure Crypto Engine (SCE) 配合使用&#xff0c;可实现安全芯片的功能。 RA4M2 采用高效的 40nm 工艺&#xff0c;由灵活配置软件包 (FSP) 这个开放且灵活的生态系统…

计算机网络(5):运输层

这一章应该是整个计算机网络对我们来说最重要的&#xff0c;也是用的最多的一部分。 运输层协议 进程之间的通信 从通信和信息处理的角度看&#xff0c;运输层向它上面的应用层提供通信服务&#xff0c;它属于面向通信部分的最高层&#xff0c;同时也是用户功能中的最低层。…

【MySQL工具】pt-online-schema-change源码分析

通过阅读源码 更加深入了解原理&#xff0c;以及如何进行全量数据同步&#xff0c;如何使用触发器来同步变更期间的原表的数据更改。(&#xff3e;&#xff0d;&#xff3e;)V 目录 源码分析 Get configuration information. Connect to MySQL. Create --plugin. Setup la…

3D数学--矢量

矢量是具有大小和方向的有向线段 矢量大小&#xff08;结果&#xff1a;标量&#xff09; 矢量与标量乘法&#xff08;结果&#xff1a;矢量&#xff09; 矢量加减法&#xff08;结果&#xff1a;矢量&#xff09; 矢量点积&#xff08;结果&#xff1a;标量&#xff09; 1.矢量…

2024年【天津市安全员C证】新版试题及天津市安全员C证模拟考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 天津市安全员C证新版试题是安全生产模拟考试一点通总题库中生成的一套天津市安全员C证模拟考试题库&#xff0c;安全生产模拟考试一点通上天津市安全员C证作业手机同步练习。2024年【天津市安全员C证】新版试题及天津…

SpringMVC基础知识(持续更新中~)

笔记&#xff1a; https://gitee.com/zhengguangqq/ssm-md/blob/master/ssm%20md%E6%A0%BC%E5%BC%8F%E7%AC%94%E8%AE%B0/%E4%B8%89%E3%80%81SpringMVC.md 细节补充&#xff1a;

Vue 实现响应式布局

实现响应式布局是工作中必不可少 客户需要 若是使用vue element ui 的方式实现 浏览器宽度为760的情况 浏览器宽度为360的情况 手机上的显示的情况 一、对于屏幕尺寸的定义 element UI参照Bootstrap的解决方案提供了五种屏幕大小尺寸&#xff1a;xs、sm、md、lg 和 xl。并对…

CAD制图

CAD制图 二维到三维 文章目录 CAD制图前言一、CAD制图二、机械设计三、二维图纸四、三维图纸总结前言 CAD制图可以提高设计效率和准确性,并方便文档的存档和交流,是现代工程设计中不可或缺的一部分。 一、CAD制图 CAD(Computer-Aided Design)是利用计算机技术辅助进行设计…

web3风险投资公司之Electric Capital

文章目录 什么是 Electric CapitalElectric团队 Electric Capital 开发者报告参考 什么是 Electric Capital 官网&#xff1a;https://www.electriccapital.com/ 官方github&#xff1a;https://github.com/electric-capital Electric Capital 是一家投资于加密货币、区块链企…

自学华为鸿蒙开发?一般人我还是劝你算了吧!!!

本人纯屌丝一枚&#xff0c;在学编程之前对电脑的认知也就只限于上个网&#xff0c;玩个办公软件。这里不能跑题&#xff0c;我为啥说自学鸿蒙开发&#xff0c;一般人我还是劝你算了吧。因为我就是那个一般人。 基础真的很简单&#xff0c;是个人稍微认点真都能懂&#xff0c;…

如何要做好年终绩效

年终绩效&#xff0c;每年12月份&#xff0c; 都是“绩效”这个词比较热门的阶段&#xff0c; 各个企业各个部门避不开的话题。 那如何能做好呢&#xff0c;一起来看看关于 要做好年终绩效考核、绩效面谈和绩效环评的建议吧&#xff01; 明确目标和指标&#xff1a;在考核开…

轻松搭建知识付费小程序:让知识传播更便捷

明理信息科技saas知识付费平台 在当今数字化时代&#xff0c;知识付费已经成为一种趋势&#xff0c;越来越多的人愿意为有价值的知识付费。然而&#xff0c;公共知识付费平台虽然内容丰富&#xff0c;但难以满足个人或企业个性化的需求和品牌打造。同时&#xff0c;开发和维护…

C# 基于MQTT创建客户端的可靠数据传输

C# 基于MQTT创建客户端的可靠数据传输 引言MQTT简介C# MQTT库引用代码和描述1、 代码2、 描述 引言 MQTT是tcpip的应用层协议&#xff0c;这里我们简单介绍一下MQTT的基本概念&#xff0c;并用C# 描述客户端的订阅和发布。 MQTT简介 MQTT(Message Queuing Telemetry Transpor…

AcWing125. 耍杂技的牛(贪心+推公式)

题目链接AcWing125. 耍杂技的牛 分析: 这是一道贪心问题&#xff0c;我们假设牛最终的摆放顺序(从上大小)为1,2,3,...i,i1,...,n&#xff0c;当存在相邻的两头牛i,i1如果 w i s i > w i 1 s j 1 w_is_i> w_{i1}s_{j1} wi​si​>wi1​sj1​ 那么交换两头牛i,i1的…

ansibe的脚本---playbook剧本(1)

playbook剧本组成部分&#xff1a; 1、task 任务&#xff1a; 主要是包含要在目标主机上的操作&#xff0c;使用模块定义操作。每个任务都是模块的调用。 2、variables变量&#xff1a;存储和传递数据。变量可自定义&#xff0c;可以在playbook中定义为全局变量&#xff0c;可…

centos7服务器安装 mysql

centos7服务器安装 mysql 一、下载 官网&#xff1a;https://dev.mysql.com/downloads/mysql/ 二、安装 1.查看有没有需要卸载的SQL包&#xff0c;一般系统的mariadb是自带的&#xff0c;需要卸载 rpm -qa | grep mysql rpm -qa | grep mariadb例如&#xff1a;安装过MySQL…

ubuntu保存分辨率失效解决办法

在VM虚拟机中&#xff0c;遇到修改ubuntu分辨率后&#xff0c;重启后又重置的解决办法。 目前我的ubuntu版本是&#xff1a;ubuntu 18.04.6 版本。 1.首先&#xff0c;在你喜欢的目录建立一个.sh 脚本文件。 终端执行命令&#xff1a;sudo vim xrandr.sh 2.按 i 进入编辑状…

开放原子线下训练营---STM32H7搭载TobudOS开发心得

导语&#xff1a; 本次线下活动是以STM32H7为核心的一个功能强大的开发板&#xff0c;不仅支持Audio&#xff0c;HDMI&#xff0c;还支持4G或者WiFi模块&#xff0c;也可以外接屏幕&#xff0c;本次线下训练营是以4G模块进行开发。 线下的实物开发板如下所示&#xff1a; 注意…