您现在的位置是:首页 > 正文

多线程进阶

2024-02-01 05:34:35阅读 2

多线程进阶

乐观锁 vs 悲观锁

乐观锁与悲观锁描述的是两种不同的加锁的态度

锁冲突: 多个线程竞争同一把锁,就会发生锁冲突,就会发生阻塞等待

乐观锁: 预测锁冲突的概率不高,所以做的工作可以简单一点

悲观锁 : 预测锁冲突的概率比较高,所以做到工作要复杂一点

举一个例子: A和B合租,A洗澡的时候,不喜欢关门,他认为B来卫生间的概率比较低,所以A洗澡不锁卫生间门了,A就类似于乐观锁

B相反,B为了防止意外发生,每一次洗澡都会锁上卫生间的门,B就类似于悲观锁

读写锁 vs 普通互斥锁

普通的互斥锁,比如synchronized , 多个线程会竞争同一把锁,只有一个线程能获得这把锁,其他的线程只能等待

读写锁分为两种,加读锁 加写锁

度读与读之间不会产生锁竞争

读与写之间会有竞争

写与写之间会有竞争

在实际写代码的时候,读的场景很多,写的场景很少,所以读写锁相比于 普通互斥锁,就少了很多的锁竞争,优化了执行效率

重量级锁 vs 轻量级锁

重量级锁和轻量级锁也是一组锁策略

重量级锁 加锁解锁开销比较大 一个典型: 进入内核的加锁逻辑,开销比较大

轻量级锁 加锁解锁开销比较小 一个典型:纯用户态的加锁逻辑的开销比较小

重量级锁 轻量级锁 和 乐观锁 悲观锁的辨析

乐观锁 悲观锁是站在 加锁的过程的角度上看的, 加锁解锁过程中工作的多还是少

重量级锁 轻量级锁 是站在结果的角度上看的, 最终加锁解锁消耗的时间是多还是少

在通常的情况下, 干的工作多,消耗的时间也就多,所以一般情况下,乐观锁一般比较轻量, 悲观锁一般比较重量,但是,这不绝对 !

自旋锁(spin lock) vs 挂起等待锁

自旋锁是一种 轻量级锁[消耗的时间更短] 的典型实现

当某个线程没有没有申请到锁的时候,该线程不会被挂起, 会一直检测锁的情况, 一旦锁被释放就竞争锁,要是锁没有被释放, 每过一会在来检测

挂起等待锁是一种 重量级锁[消耗的时间更长]

当某个线程没有申请到锁, 该线程就会被挂起, 让出资源, 让给别的线程 , 直到锁 被释放了 ,该线程才开始重新竞争锁

举一个例子: 要是A约了B去打球,A已经到了B的楼下, B还没有下来 , 此时A就有两种选择

  1. A每过一分钟,就给 B 打电话催他, 直到B下楼

  2. A就在B的楼下开始看书,一直到B下楼

    选择1 就是自旋锁 选择2 就是挂起等待锁

公平锁 和 不公平锁

t1 t2 t3线程竞争同一把锁, 谁先来的,谁就拿到锁,这就叫做公平, 要是三个线程随机拿到锁,有可能后来的线程拿到了锁,这就是不公平

操作系统默认的锁的调度是不公平的,要想实现公平锁,需要引入额外的数据结构,来记录线程加锁的顺序, 这需要一定的额外开销

可重入锁 vs 不可重入锁

可重入锁: 同一个线程对同一把锁,连续加锁两次,不会死锁

不可重入锁: 同一个线程对同一把锁, 连续加锁两次,会导致死锁

总结

一共有这些锁策略:

  1. 乐观锁 vs 悲观锁

  2. 读写锁 vs 普通互斥锁

  3. 轻量级锁 vs 重量级锁

  4. 自旋锁 vs 挂起等待锁

  5. 公平锁 vs 非公平锁

  6. 可重入锁 vs 不可重入锁

    对于synchronized

    1. 既是乐观锁也是悲观锁

    2. 既是轻量级锁也是重量级锁

    3. 乐观锁的部分是基于自旋锁实现的, 悲观锁是基于挂起等待锁实现的

      synchronized是自适应的 :

      要是锁竞争不激烈,它就是乐观锁/轻量级锁/自旋锁

      注意: 自旋锁是纯用户态实现的,相比于内核态,它的工作量是比较少的

      要是锁竞争比较激烈, synchronized就会自动升级悲观锁/重量级锁/挂起等待锁

      所以说synchronized是自适应的

      1. 不是读写锁,是普通互斥锁
      2. 是非公平锁
      3. 是可重入锁

CAS

CAS就是compare and swap 比较并交换

把内存中的某个值和CPU寄存器A中的值进行比较, 如果两个值相同, 就把另一个寄存器B中的值与内存的值进行交换(把内存中的值放到寄存器B中, 同时把寄存器B的值写给内存)

CAS主要关心的是内存中的值

CAS最厉害的就是它是通过一个CPU指令完成的, 原子的

所以使用CAS 是线程安全的,高效的,不加锁,也能保证线程安全

但是CAS只能在特定的场景中使用,加锁的适用性更好,而且,加锁的可读性更好

CAS主要是有两个使用场景:

  1. 实现原子类

    要想实现多线程count++,就不安全,加上锁就能保证线程安全,但是效率就会大打折扣,此时基于CAS就能实现"原子"的++ , 既能保证线程安全, 也能保证高效

​ 2.实现自旋锁

自旋锁是一种纯用户态的轻量级锁,当发现锁被其他的线程持有的时候,线程不会挂起等待,而是会一直询问,看当前的锁是否被释放,是为了抢先执行,节省了进入内核和系统调度的开销

自选锁属于耗CPU的资源换来的是第一时间获取到锁,它预期可以在短时间内获得锁,并且锁竞争并不激烈,所以自旋锁是一个轻量级锁也还是一个乐观锁

CAS的ABA问题

在CAS中, 进行值的比较的时候,发现寄存器A和内存M的值相同, 此时是无法判定M是始终没变还是变了,但是又变回来了

线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。

上面的概念还是有点抽象,接下来就举一个例子

小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因阻塞了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50! (造成错误 ! )
此时可以看到,实际余额应该为100,但是实际上变为了50(多扣了50块钱)这就是ABA问题带来的成功提交。

如何解决ABA问题?

在变量前面加上版本号,每次变量更新的时候,版本号+1 , 或者记录"上次修改的时间"通过这两种办法来解决ABA问题

synchronized原理

锁升级/锁膨胀

通过之前的学习,我们已经知道synchronized的作用是加锁 , 当两个线程对同一个对象加锁的时候,就会出现锁竞争 ,没有抢到锁的线程就会阻塞等待,一直等到 另一个线程释放锁

synchronized是自适应的,它既是轻量级锁也是重量级锁

要是当前场景是锁竞争不激烈, 就以轻量级锁状态来工作(自旋锁—第一时间拿到锁)

要是当前场景的锁竞争比较激烈,就以重量级锁状态来工作(挂起等待锁— 不能第一时间拿到锁,但是节省了CPU的开销)

synchronized是偏向锁

有时候加上synchronized也不一定是真的加上了锁,不一定会造成锁竞争

有时候两个线程的调度恰好错开了,此时这两个线程也就没有锁竞争,所以此时是没有进行加锁的

当涉及到锁竞争的时候,再进行加锁 (轻量级锁)

总结一下, synchronized是自适应的

无竞争 , 偏向锁

有竞争 , 轻量级锁

竞争激烈 , 重量级锁

以上的锁状态转变,就叫做锁升级 / 锁膨胀

锁消除

JVM会自动判定,要是发现有些代码不需要加锁,哪怕你写了synchronized,也不会真的加锁

注意: 锁消除是一种编译器的优化方法, 当编译器100%确定代码不需要加锁,才会去掉锁, 要是编译器不确定,那么编译器是不会贸然去掉锁的,所以锁消除是比较保守的

锁粗化

锁的粒度

锁的粒度是synchronized里面的代码包含多少代码

包含的代码少 就是粒度细

包含的代码多 就是粒度粗

锁粗化: 细粒度的加速–>出粒度的加锁

前提: 要是对同一个对象加锁才能粗化到一起

粗化一种编译器的优化,所以要首先保证程序是线程安全的,粗化只是锦上添花

image-20220927195334758

粗化是锁的数目变少了,但是锁的代码变多了,也就完成了锁粗化

JUC

JUC是java.util.concurrent 关于并发的一个包 这个包里很多都是关于多线程的类 方法

Callable接口

Callable类似于Runnable, 但是Runnable描述任务,没有返回值

Callbale描述任务有返回值

创建一个线程来 计算1+2+3+…+1000的值

package Threading;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class Demo30 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer>  callable = new Callable<Integer>() {
            @Override
            //call方法里面是要执行的任务
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000 ; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //callable不能直接传给t,所以要借助futureTask
        //所以FutureTask存在的意义就就是接收Callable返回的结果,并把它传给线程
        FutureTask futureTask = new FutureTask(callable);
        Thread t =  new Thread(futureTask);
        t.start();
        //获取返回值
        System.out.println(futureTask.get());
    }
}

创建线程的方式:

  1. 继承Thread (用不用匿名内部类都行)
  2. 实现 Runnable (用不用匿名内部类都行)
  3. 使用lambda
  4. 使用线程池
  5. 使用Callable

ReentrantLock

Reentrant [riːˈɛntrənt] 词根是enter, ReentrantaLock是可重入锁的意思

synchronized也是可重入锁,但是synchronized有些操作是做不到的, 所以ReentrantLock 算是对synchronized的补充

ReentrantLock的主要方法

  1. lock() 加锁
  2. unlock() 解锁

ReentrantLock的加锁和解锁是 要自己手动写上的,synchronized是出了范围就会自动解锁的,所以有时候可能会忘记解锁(缺点)

import java.util.concurrent.locks.ReentrantLock;

public class Demo31 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        //加锁
        reentrantLock.lock();
        //解锁
        reentrantLock.unlock();
    }
}
import java.util.concurrent.locks.ReentrantLock;

public class Demo31 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        try{
            //加锁
            reentrantLock.lock();
        }finally{  ///使用finally就不会忘记解锁
            //解锁
            reentrantLock.unlock();
        }
    }
}

ReentrantLock有synchronized所没有的优势

1.tryLock 试试看能不能加上锁, 能锁成功就加锁成功,尝试失败就放弃加锁

tryLock还能指定加锁的等待超时时间(尝试失败了可以多等 s 时间,要是还是等不到锁就放弃 加锁)

2.ReentrantLock 可以实现公平锁, 默认是非公平锁, 构造的时候,传入一个参数就变成了公平锁 (多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁)

ReentrantLock reentrantLock = new ReentrantLock(true);

3.synchronized是搭配wait / notify实现等待通知机制的, 随机唤醒一个线程

​ ReentrantLock搭配Condition类实现, 能指定唤醒哪个等待线程

所以synchronized与ReentrantLock的区别是什么?

区别 =缺点 + 优点

原子类

在下面的类都是原子类的

image-20220927214201443

package Threading;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo32 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count  = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                //相当于是count++
                count.getAndIncrement();
                //count.incrementAndGet();  相当于是 ++count
                // count.getAndDecrement();   相当于是count--
                // count.decrementAndGet();  相当于是--count
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                //相当于是count++
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

​ 输出结果是10000 由于是原子类,所以已经是线程安全的了

信号量 Semaphore

Semaphore [ˈseməfɔː®]

信号量的基本操作有两个:

P操作,申请一个资源

V操作, 释放一个资源

信号量是一个计数器,表示可用资源的个数

P操作表示申请一个资源,可用资源就-1

V操作表示释放一个资源,可用资源就+1

当计数器为0的时候,要是继续进行P操作(申请资源),就会产生阻塞,阻塞等待到其他的线程V操作(释放资源)为止

这样子,锁就是一个特殊的信号量, 一个可用资源只有1 的信号量

java中提供了Semaphore类来给我们使用

public static void main(String[] args) throws InterruptedException {
    Semaphore semaphore = new Semaphore(3);
    semaphore.acquire();
    System.out.println("申请一个资源");
    semaphore.acquire();
    System.out.println("申请一个资源");
    semaphore.acquire();
    System.out.println("申请一个资源");
    semaphore.acquire();
    System.out.println("申请一个资源");
}

只会输出三句话, 并且进程不会结束

一共就只有3个资源,到四个的时候,就不能在申请资源了, 就会阻塞等待, 需要等待资源释放

使用release() 来释放资源

public static void main(String[] args) throws InterruptedException {
    Semaphore semaphore = new Semaphore(3);
    semaphore.acquire();
    System.out.println("申请一个资源");
    semaphore.acquire();
    System.out.println("申请一个资源");
    semaphore.acquire();
    System.out.println("申请一个资源");
    //释放资源   计数器+1
    semaphore.release();
    semaphore.acquire();
    System.out.println("申请一个资源");
}

此时就能输出四句话,并且进程能结束

P操作 acquire() 申请资源 计数器-1

V操作 release() 释放资源 计数器+1

一个冷知识: 这里的P操作 和 V操作是 荷兰语

CountDownLock

CountDownLock类似于考试计数 , 只要有一位考生交卷就记录一下, 直到最后一名考生交完卷之后,考试才算是真正结束, 当计数与实际的人数一致 的时候, 就结束

其实也类似 与IDM, 要下载一个很大的文件,IDM会将任务拆分成多个部分, 每个线程负责一个部分, 直至最后一个部分下载完成,整个文件才下载完成

package Threading;

import java.util.concurrent.CountDownLatch;

public class Demo33 {
    public static void main(String[] args) throws InterruptedException {
       //设定有10个考生
        CountDownLatch countDownLatch = new CountDownLatch(10);
        //创建10个线程
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(()->{
                System.out.println("开始考试!" + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("考试结束!"+ Thread.currentThread().getName());
                //只要有一个线程结束就记录一次
                countDownLatch.countDown();
            });
            t.start();
        }
        //await是阻塞等待,当所有人都交完卷子才算是考试真正结束
        countDownLatch.await();
        System.out.println("老师整理卷子,考试圆满结束!");
    }
}

image-20220928200901414

一个小技巧: IDEA全局搜索 Ctrl + Shift + R / F

ConcurrentHashMap

这是在多线程中很好用的HashMap,或者说在多线程下,使用ConcurrentHashMap是 很好的

ConcurrentHashMap做了很多的优化策略

1.锁粒度的控制

相比于HashTable是直接在方法上面加上使用synchronized,相当于是对this加锁, 也就是对哈希表对象加锁,也就是 说这个哈希表一共只有一个锁 ,所以HashTable很容易发生锁冲突,对性能的影响很大,所以连官方 都不推荐使用HashTable

相比之下, ConcurrentHashMap是每个哈希桶就有一个自己的锁, 大大降低了 锁冲突的概率,所以性能也就提升了

2.ConcurrentHashMap的加锁策略

ConcurrentHashMap只是给写操作加锁,读操作是不加锁的

两个线程同时读 ,不加锁

两个线程同时修改, 没有锁冲突

如果一个线程读,一个线程改, 也没有锁冲突 ----->要是一般情况下,这是可能出现线程不安全的, 主要担心的是读到的是一个修改了一半的数据

但是ConcurrentHashMap设计的时候考虑过 这一点,所以同时进行读和写是不会锁冲突的,并且ConcurrentHashMap广泛的使用了volatile来保证读到的数据是及时的

3 . 充分地利用了CAS的特性

在能使用CAS的地方尽量都使用了CAS,尽量避免使用synchronized, 减少加锁的数量 ,以此来提高代码的性能和效率

所以, ConcurrentHashMap的核心思路就是尽量降低 锁冲突的概率

4 . ConcurrentHashMap的扩容策略

当put元素的时候,发现当前的负载因子已经达到了阈值,就要开始扩容

HashTable的扩容策略是申请一个更大的数组,然后把旧的数据搬运过去, 但是这就会有一个很大的问题: 要是原本的元素很多,搬运的开销就会很大,就可能会导致请求超时

ConcurrentHashMap的扩容策略: 不是一次性搬运完,每次 进行哈希表的操作就往新的上插入一些数据, 要是删除元素,直接就删了不用搬运了,这样子积少成多 , 扩容 的效率就会变高

HashMap HashTable ConcurrentHashMap 的区别?

首先要知道,HashMap是线程不安全的,另外两个是线程安全的

ConcurrentHashMap相比于HashTable的优点:

  1. HashTable只会有一把大锁,容易锁冲突, ConcurrentHashMap在每个哈希桶里面都有锁,不容易发生锁冲突
  2. ConcurrentHashMap只是给写操作加锁,读操作是不加锁的
  3. ConcurrentHashMap使用了CAS
  4. ConcurrentHashMap的扩容策略是积少成多

死锁

所谓的死锁就是线程加上锁之后,解不开了,直接就僵住了

场景一: 一个线程,一把锁,连续加锁两次, 要是这个锁是不可重入锁, 就是死锁了

要是这个锁是可重入锁(比如synchronized) 就不会出现死锁

场景二: 两个线程, 两把锁, 相互申请,结果最后谁都解不了,僵住了,也变成了死锁

举一个具体的例子, 房间钥匙锁在车上了,车钥匙所在房间里了

一个相互加锁的死锁代码:

package Threading;

public class Demo34 {
    public static void main(String[] args) {
       Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() ->{
            System.out.println("t1尝试获取locker1");
            synchronized (locker1){
                System.out.println("t1获取locker1成功");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1尝试获取locker2");
                synchronized (locker2){
                    System.out.println("两把锁获取成功");
                }
            }
        });
        Thread t2 = new Thread(() ->{
            System.out.println("t2尝试获取locker2");
            synchronized (locker2){
                System.out.println("t2获取locker2成功");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2尝试获取locker1");
                synchronized (locker1){
                    System.out.println("两把锁获取成功");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

注意: 在每个线程中的synchronized是嵌套的, 并且 两个线程最先加的锁是不一样的,这样子才会导致死锁

输出结果:

image-20220929151426261

t1 t2最后都无法获取到对方的锁, 就变成了死锁

明确了死问题,该怎么解决?

死锁的四个必要条件:

1.互斥使用 锁A被线程1占用,线程2就使用不了

2.不可抢占 锁A被线程1占用, 线程2不能把锁抢过来, 一直要到线程1释放锁

3 . 请求和保持 有多把锁,线程1拿道锁A之后,不想释放锁A,还想要拿锁B,此时就要看获取锁B的时候是不是先释放了锁A

4 . 循环等待 线程1等待线程2释放锁,线程2要等待线程3释放锁,线程3要等待线程1释放锁(这样子就会导致死锁)

解决方法: 约定, 加多个锁的时候,必须先加编号小的, 后加编号大的

上面的代码进行修改:

package Threading;

public class Demo34 {
    public static void main(String[] args) {
       Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() ->{
            System.out.println("t1尝试获取locker1");
            synchronized (locker1){
                System.out.println("t1获取locker1成功");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1尝试获取locker2");
                synchronized (locker2){
                    System.out.println("两把锁获取成功");
                }
            }
        });
        Thread t2 = new Thread(() ->{
            System.out.println("t2尝试获取locker2");
            synchronized (locker1){
                System.out.println("t2获取locker2成功");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2尝试获取locker1");
                synchronized (locker2){
                    System.out.println("两把锁获取成功");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

其实只是修改了,加锁的顺序,第二个线程也是现申请locker1再申请locker2

此时,当线程1抢到了locker1,线程2就会阻塞等待, 此时就不会出现死锁

image-20220929170109375

到此为止,多线程的课时就结束了,多线程还是很难的,需要多看课和练习

网站文章

  • LeetCode·每日一题·1851. 包含每个查询的最小区间·优先队列(小顶堆)

    LeetCode·每日一题·1851. 包含每个查询的最小区间·优先队列(小顶堆)

    优先队列

    2024-02-01 05:34:27
  • Rest_Assured接口测试-配置环境信息

    整合代码 ,多环境测试 切换测试环境地址

    2024-02-01 05:33:57
  • typescript基本数据类型

    ts共有7种基本数据类型分别为: Boolean Number String Array Enum Any Void对各种数据类型的声明以及注意事项都浓缩在如下代码块,为了方便您可以可自行粘贴运行:如果您还不会编译ts文件请点击这里看我的另一篇文章《typescript安装及如何编译运行》/** * ts基本数据类型 * Boolean * Number *...

    2024-02-01 05:33:50
  • Linux——设备树 最新发布

    Linux——设备树 最新发布

    包括linux设备树的由来,使用设备树的目的,怎样去使用设备树以及用一个简单小例子进行举例。

    2024-02-01 05:33:42
  • 深度学习&amp;图像处理(色彩编辑4)

    深度学习&amp;图像处理(色彩编辑4)

    1.YUV转换 YUV,是一种颜色编码方法。“Y”表示明亮度(Luminance或Luma),也就是灰阶值,“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱...

    2024-02-01 05:33:14
  • 单链表与双链表(C语言)

    单链表的创建(头插法、尾插法),单链表的插入、删除等双链表的创建(头插法、尾插法),单链表的插入、删除等#include <stdio.h>#include <iostream>using names...

    2024-02-01 05:33:07
  • FTP的工作方式:Active FTP 及 Passive FTP

    FTP的工作方式:Active FTP 及 Passive FTP

    <br />為何常常連上 FTP 站台後,進去後就停留且無法列表?<br /><br />防火牆有很多種,其中有一些會禁止那些不是從內部網路IP發出的連接請求。而FTP協議是個很老的東東,沒有考慮這個...

    2024-02-01 05:33:00
  • /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.9' not found

    问题:./a.out: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.9' not found (required by /home/ycai/x10/stdlib/lib/libx10.so)可能的解决方案:第一种方案:执行strings /usr/lib64/libstdc++.so.6 | grep GLIBC

    2024-02-01 05:32:33
  • 相对(relative)定位和绝对(absolute)定位

    相对(relative)定位和绝对(absolute)定位

    首先,position的这两个属性一般是不使用的,因为有了浮动,所以我们才需要position属性来实现我们想要的布局。 1.相对定位(relative):相对于原来位置(原来位置指在文档流中默认的位置,若加上了浮动时,那么这个原来位置就是你设定浮动时的位置)的偏移,原来位置依然占据空间。 (1)box1和box2都没有设置position属性(没有设置float属性时) 效果图:我...

    2024-02-01 05:32:28
  • 威胁驱动的网络安全方法论

    威胁驱动的网络安全方法论

    本文主要内容取自洛克希德·马丁公司的论文:A Threat-Driven Approach to Cyber Security,想要全面准确了解论文内容的朋友建议阅读原文。希望能够抛砖引玉,为相关领域...

    2024-02-01 05:32:20