JUC并发编程第十三章——读写锁、邮戳锁

news2024/12/23 22:40:02

本章路线总纲

无锁——>独占锁——>读写锁——>邮戳锁

1 关于锁的面试题

  • 你知道Java里面有那些锁
  • 你说说你用过的锁,锁饥饿问题是什么?
  • 有没有比读写锁更快的锁
  • StampedLock知道吗?(邮戳锁/票据锁)
  • ReentrantReadWriteLock有锁降级机制,你知道吗?

2 简单聊聊ReentrantReadWriteLock

类图:

读写锁的演变情况:

2.1 是什么?

读写锁说明

  • 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

演变

  • 无锁无序->加锁->读写锁->邮戳锁

读写锁意义和特点

  • 读写锁只允许读读共存,而读写和写写依然是互斥的,恰好大多实际场景是”读/读“线程间不存在互斥关系,只有”读/写“线程或者”写/写“线程间的操作是需要互斥的,因此引入了 ReentrantReadWriteLock
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但是不能同时存在写锁和读锁,也即资源可以被多个读操作访问,或一个写操作访问,但两者不能同时进行。
  • 只有在读多写少情景之下,读写锁才具有较高的性能体现。

2.2 特点

可重入、读写兼顾

结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得

ReentrantReadWriteLock的缺点:

1. 锁饥饿问题:

  • ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。

2. 锁降级:

  • 将写锁降级为读锁------>遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁
  • 如果一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
  • 如果释放了写锁,那么就完全转换为读锁
  • 如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

2.3 读写锁案例

  • 使用读写锁之前,使用synchronized的情况
public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        //开启10个线程,写入数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }

        //开启10个线程,读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}

//模拟一个缓存资源类,有读写两种功能
class MyCache {

    HashMap<String, String> map = new HashMap<>();

     ReentrantLock lock = new ReentrantLock();

    //读写都加锁
    public void write(String key, String value) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
            //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
            TimeUnit.MILLISECONDS.sleep(500);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void read(String key) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
            String val = map.get(key);
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
运行结果:
1线程开始写入数据...
1线程完成写入数据!
2线程开始写入数据...
2线程完成写入数据!
3线程开始写入数据...
3线程完成写入数据!
4线程开始写入数据...
4线程完成写入数据!
5线程开始写入数据...
5线程完成写入数据!
6线程开始写入数据...
6线程完成写入数据!
7线程开始写入数据...
7线程完成写入数据!
9线程开始写入数据...
9线程完成写入数据!
8线程开始写入数据...
8线程完成写入数据!
10线程开始写入数据...
10线程完成写入数据!
1线程开始读取数据...
1线程读取到的数据是:	1
2线程开始读取数据...
2线程读取到的数据是:	2
3线程开始读取数据...
3线程读取到的数据是:	3
4线程开始读取数据...
4线程读取到的数据是:	4
5线程开始读取数据...
5线程读取到的数据是:	5
6线程开始读取数据...
6线程读取到的数据是:	6
7线程开始读取数据...
7线程读取到的数据是:	7
8线程开始读取数据...
8线程读取到的数据是:	8
9线程开始读取数据...
9线程读取到的数据是:	9
10线程开始读取数据...
10线程读取到的数据是:	10

说明:可以看出,开始写入/读取和完成写入/读取,都是成对出现的。这说明这写入/读取期间,其他线程不能执行写入/读取。读写/读读/写写都互斥了。

问题:我们希望的情况应该是,读写/写写都互斥,但读读可以并发读取。从而引出了读写锁(对写独占,对读共享)

  • 使用读写锁
public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        //开启10个线程,写入数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }

        //开启10个线程,读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}

//模拟一个缓存资源类,有读写两种功能
class MyCache {

    HashMap<String, String> map = new HashMap<>();

    ReentrantLock lock = new ReentrantLock();

    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    //读写都加锁
    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
            //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
            TimeUnit.MILLISECONDS.sleep(500);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
            String val = map.get(key);
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}
运行结果:
1线程开始写入数据...
1线程完成写入数据!
2线程开始写入数据...
2线程完成写入数据!
3线程开始写入数据...
3线程完成写入数据!
4线程开始写入数据...
4线程完成写入数据!
5线程开始写入数据...
5线程完成写入数据!
6线程开始写入数据...
6线程完成写入数据!
7线程开始写入数据...
7线程完成写入数据!
8线程开始写入数据...
8线程完成写入数据!
9线程开始写入数据...
9线程完成写入数据!
10线程开始写入数据...
10线程完成写入数据!
1线程开始读取数据...
9线程开始读取数据...
7线程开始读取数据...
6线程开始读取数据...
5线程开始读取数据...
3线程开始读取数据...
4线程开始读取数据...
2线程开始读取数据...
10线程开始读取数据...
8线程开始读取数据...
10线程读取到的数据是:10
4线程读取到的数据是:	4
2线程读取到的数据是:	2
8线程读取到的数据是:	8
3线程读取到的数据是:	3
7线程读取到的数据是:	7
6线程读取到的数据是:	6
5线程读取到的数据是:	5
1线程读取到的数据是:	1
9线程读取到的数据是:	9

说明:可以看出,所有写操作还是跟之前一样,全部互斥。但读操作可以并发读取。

结论

使用ReadWriteLock实现读写操作,一体两面,读写互斥,读读共享,但是读没有完成时候其它线程写锁无法获取


2.4 锁降级

ReentrantReadwriteLock锁降级:

  • 将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。

ReentrantReadwriteLock的特性:

写锁降级成为读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
  3. 如果释放了写锁,那么就完全转换为读锁。

总之:

  • 如果一个线程先获取写锁,在获取写锁和释放写锁之间可以再获取读锁,如果获取了读锁,之前获取的写锁且被释放了。那么之前的写锁,就降级为现在的读锁了。

why?要有这么个特性?

----后面解释,大概目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性


2.5 写锁可以降级为读锁,但读锁不可升级为写锁

重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁,但是从读锁升级到写锁是不可能的

锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

样例1

锁降级:获取写锁 ——> 获取读锁 ——> 释放写锁 ——> 释放读锁      ✔ 可以完成

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDownGradingDemo {

    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        // 例一:正常两个A、B线程
//        new Thread(() -> {
//            readLock.lock();
//            System.out.println("---A线程读取---");
//            readLock.unlock();
//        }, "A").start();
//
//        new Thread(() -> {
//            writeLock.lock();
//            System.out.println("---B线程写入---");
//            writeLock.unlock();
//        }, "B").start();


        // 例二:only one 同一个线程
        writeLock.lock();
        System.out.println("---写入---");
        // 一些其它的业务操作...

        readLock.lock();
        System.out.println("---读取---");
        // 一些其它的业务操作...

        writeLock.unlock();
        readLock.unlock();
    }

}

输出结果:
---写入---
---读取---

说明: 

  • 同一个线程的写后立刻读是可以的,即将写入锁降级为读锁是支持的,这种就是锁降级

样例2

锁降级:获取读锁 ——> 获取写锁 ——> 释放读锁 ——> 释放写锁      X 不可以完成

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDownGradingDemo2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        // 例二:only one 同一个线程
        readLock.lock();
        System.out.println("---读取---");
        // 一些其它的业务操作...

        writeLock.lock();
        System.out.println("---写入---");
        // 一些其它的业务操作...

        readLock.unlock(); // 这个位置和下面那个位置效果一样

        writeLock.unlock();
//        readLock.unlock();
    }

}


输出结果:
---读取---
// ...程序未结束

说明:

  • 如果有线程读没有完成的时候,写线程无法获取锁,必须要等着读锁释放所锁后才有机会写,这是悲观锁的策略

1、2例子对比小结:

  • 其实想想很容易理解:同一个线程,先读,还没有读完(读锁readLock没有unlock),我又去写。那么我之前的不就是脏数据了?因此应该先全部读完,才能执行写操作。
  • 而例子1中,先写,就算没写完(写锁没有释放),我立马去读,由于读操作不会导致数据不一致。因此,这是合理的。


2.6 写锁和读锁是互斥的

        写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作

因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:

  • 即ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁也就是写入必须等待,这是一种悲观的读锁,人家还在读着那,你先别去写,省的数据乱。

2.7 Oracle公司ReentrantReadWriteLock使用样例

* <p><b>Sample usages</b>. Here is a code sketch showing how to perform
* lock downgrading after updating a cache (exception handling is
* particularly tricky when handling multiple locks in a non-nested
* fashion):
*
* <pre> {@code
* class CachedData {
*   Object data;
*   volatile boolean cacheValid;
*   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
*
*   void processCachedData() {
*     rwl.readLock().lock();// 1
*     if (!cacheValid) {
*       // Must release read lock before acquiring write lock
*       rwl.readLock().unlock();// 2
*       rwl.writeLock().lock();// 3
*       try {
*         // Recheck state because another thread might have
*         // acquired write lock and changed state before we did.
*         if (!cacheValid) {
*           data = ...//在此做一些写操作
*           cacheValid = true;
*         }
*         // Downgrade by acquiring read lock before releasing write lock
*         rwl.readLock().lock();// 4
*       } finally {
*         rwl.writeLock().unlock(); // 5 Unlock write, still hold read
*       }
*     }
*
*     try {
*       use(data);
*     } finally {
*       rwl.readLock().unlock();// 6
*     }
*   }
* }}</pre>

代码解读:

  • 1-6 六个加锁/释放锁的操作。1-2对应读锁、3-5对应写锁、4-6对应读锁。volatile类型的cacheValid变量,保证其可见性
  • 首先,线程第一次进来,资源类CacheData是没有被修改过的。先加读锁1,if判断 ( !cacheValid ) 的值为true。在2的位置释放读锁。
  • 接着准备写操作,先获取写锁3。并进行双端检索 (防止其它线程恰好修改了)。做完写操作后,把cacheValid改为true。为了立刻读取到我刚刚修改的数据data,必须发生锁降级,在释放写锁5之前获取读锁4。原因:如果我先把写锁释放了,再获取读锁,出现了没有锁的空档期。在此期间锁可能被其他线程获取并修改数据,无法保证读锁立马能被同一个线程获取,可能在我使用data数据的期间,data数据又被修改了!
  • 在4的位置已经获取了读锁,代码运行到5的位置释放写锁。发生锁降级。之后在use(data)这行使用刚刚修改的data数据,最后在6位置释放读锁。让其他线程继续抢锁。

        这里只有锁降级才能保证,同一个线程我先执行写操作,再继续读我刚刚写的数据。在整个线程执行业务的过程中,一直是加锁(不是写锁就是读锁)状态,没有出现空档期,因此整个操作保证了原子性。

如果违背锁降级的步骤,如果违背锁降级的步骤, 如果违背锁降级的步骤

  • 如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

4 邮戳锁StampedLock

4.1 是什么?

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化

stamp 代表了锁的状态。当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值。

4.2 它是由饥饿问题引出

锁饥饿问题:

  • ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。

如何解决锁饥饿问题:

  • 使用”公平“策略可以一定程度上缓解这个问题
  • 使用”公平“策略是以牺牲系统吞吐量为代价的
  • StampedLock类的乐观读锁方式--->采取乐观获取锁,其他线程尝试获取写锁时不会被阻塞,在获取乐观读锁后,还需要对结果进行校验

4.3 StampedLock的特点

  • 所有获取锁的方法,都返回一个邮戳,stamp为零表示失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳,这个stamp必须是和成功获取锁时得到的stamp一致
  • StampedLock是不可重入的危险(如果一个线程已经持有了写锁,在去获取写锁的话会造成死锁)
  • StampedLock有三种访问模式:
    • Reading(悲观读模式):功能和ReentrantReadWriteLock的读锁类似
    • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
    • Optimistic reading(乐观读模式):无锁机制,类似与数据库中的乐观锁,支持读写并发,很乐观认为读时没人修改,假如被修改在实现升级为悲观读模式
  • 一句话:读的过程中也允许写锁介入

4.4 邮戳锁StampedLock代码演示

4.4.1 传统的读写锁模式——读的时候写锁不能获取

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    // 悲观锁,读没有完成时候写锁无法获得锁
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "come in readLock code Block, 4 second continue...");
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
        }

        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量值result: " + result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }

    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> resource.read(), "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "come in");
            resource.write();
        }, "writeThread").start();
        
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t" + "number:" + number);
    }

}

输出结果:
readThread	come in readLock code Block, 4 second continue...
readThread	正在读取中
writeThread	come in
readThread	正在读取中
readThread	正在读取中
readThread	正在读取中
readThread	获得成员变量值result: 37
写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
writeThread	写线程准备修改
writeThread	写线程结束修改
main	number:50

这和之前的读写锁ReentrantLock使用类似,但邮戳锁不可重入


4.4.2 乐观读模式——读的过程中也允许写锁介入

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo2 {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    // 乐观读,读的过程中也允许写锁介入
    public void read() {
        long stamp = stampedLock.tryOptimisticRead();

        int result = number;

        // public boolean validate(long stamp)方法官方解释
        // 如果自发出给定标记后未完全获取锁,则返回true。如果标记为零,则始终返回false。如果图章代表当前持有的锁,则始终返回true。 使用未从tryOptimisticRead()获取的值或此锁定的锁定方法调用此方法没有定义的效果或结果
        System.out.println("4秒前 stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp));

        // 故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取...." + i + "秒后,stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp));
        }

        if (!stampedLock.validate(stamp)) {
            System.out.println("有人修改----------有写操作");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读升级为悲观读");
                result = number;
                System.out.println("重新悲观读后result:" + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result);

    }


    public static void main(String[] args) {
        StampedLockDemo2 resource = new StampedLockDemo2();
        new Thread(resource::read, "readThread").start();

        // 暂停2秒线程,演示读过程可以写介入
        // 如果暂停>4秒线程,这样读过程没有写介入,此时输出finally value:37
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + " come in");
            resource.write();
        }, "writeThread").start();
    }

}

输出结果:
4秒前 stampedLock.validate方法值(true 无修改 false有修改)	true
readThread	 正在读取....0秒后,stampedLock.validate方法值(true 无修改 false有修改)	true
writeThread	 come in
writeThread	写线程准备修改
writeThread	写线程结束修改
readThread	 正在读取....1秒后,stampedLock.validate方法值(true 无修改 false有修改)	false
readThread	 正在读取....2秒后,stampedLock.validate方法值(true 无修改 false有修改)	false
readThread	 正在读取....3秒后,stampedLock.validate方法值(true 无修改 false有修改)	false
有人修改----------有写操作
从乐观读升级为悲观读
重新悲观读后result:50
readThread	finally value: 50

4.5 StampedLock的缺点

  • StampedLock不支持重入,没有Re开头
  • StampedLock的悲观读锁和写锁都不支持条件变量,这个也需要主要
  • 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

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

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

相关文章

部署大模型LLM

在autodl上部署大模型 windows运行太麻烦&#xff0c;环境是最大问题。 选择云上服务器【西北B区 / 514机】 cpp (c c plus plus) 纯 C/C 实现&#xff0c;无需外部依赖。针对使用 ARM NEON、Accelerate 和 Metal 框架的 Apple 芯片进行了优化。支持适用于 x86 架构的 AVX、…

跟着刘二大人学pytorch(第---10---节课之卷积神经网络)

文章目录 0 前言0.1 课程链接&#xff1a;0.2 课件下载地址&#xff1a; 回忆卷积卷积过程&#xff08;以输入为单通道、1个卷积核为例&#xff09;卷积过程&#xff08;以输入为3通道、1个卷积核为例&#xff09;卷积过程&#xff08;以输入为N通道、1个卷积核为例&#xff09…

浅谈赚钱的四个级别,你在哪一层呢

一谈到赚钱&#xff0c;很多人都会扯到&#xff1a;智商、情商、人脉、资源、背景等等&#xff0c;类似“小钱靠勤&#xff0c;中钱靠智&#xff0c;大钱靠德”这样的经典语录都会脱口而出&#xff0c;其实从本质上来讲&#xff0c;都没有错&#xff0c;但这样的说法太缥缈&…

算法工程师 | 如何快速 了解,掌握一个算法!脚踏实地,迎着星辰,向前出发 ~

本文是一些碎碎念 希望对正在迈向 算法工程师道路的你 有所裨益 一般来说&#xff0c;代码 中会有很多 算法实现的细节&#xff0c;但论文可能并没有体现&#xff0c;所以能够尝试自己 仔细阅读论文&#xff0c;手动复现代码&#xff0c;基本上来说对 这个 算法 你有了全…

UltraISO制作U盘系统盘安装openEuler22.03和搭建cuda环境

1.下载openEuler镜像 https://repo.openeuler.org/openEuler-22.03-LTS/ISO/x86_64/ 选择下载&#xff1a;openEuler-22.03-LTS-x86_64-dvd.iso 2.用软碟通将 U 盘制作为启动盘 点击左上方 文件 –> 打开 然后找到下载的 ISO 镜像后&#xff0c;打开 打开后如下 找到菜单…

面试题 17.07. 婴儿名字

链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题解&#xff1a; class Solution { public:vector<string> trulyMostPopular(vector<string>& names, vector<string>& synonyms) {UnionFind uf;for (auto& syn : synonyms) {//c…

USB2.0高速转接芯片CH347应用开发手册

CH347应用开发手册 V1.3 一、简介 CH347是一款USB2.0高速转接芯片&#xff0c;以实现USB-UART(HID串口/VCP串口)、USB-SPI、USB-I2C、USB-JTAG以及USB-GPIO等接口&#xff0c;分别包含在芯片的四种工作模式中。 CH347DLL用于为CH347芯片提供操作系统端的UART/SPI/I2C/JTAG/B…

零散的面试题

★1.java常见的引用类型 强:普通的变量引用 软:内存够时,GC不会主动删除,内存不够时,GC会删除 弱:一旦执行GC就会被删除 虚:用了感觉没用 ★2.JDK1.8新特性 lambda表达式(极大简化了匿名内部类的创建&#xff0c;促进函数式编程的风格)函数式接口(只能有一个抽象方法的接口 )日…

什么是On-die Termination(ODT,片上端接)

在深入研究“片上端接”之前&#xff0c;让我们先了解一下“端接”的概念。为什么需要“端接”&#xff1f; 高速的数据传输速率和实时功能是电子设备的目标。这些共同的目标推动了电子设备不仅需要高速运行&#xff0c;而且还需要提供快速响应和实时性能&#xff0c;以满足各种…

欧阳修,仕途波澜中的文坛巨匠

欧阳修&#xff0c;字永叔&#xff0c;号醉翁、六一居士&#xff0c;生于北宋真宗景德四年&#xff08;公元1007年&#xff09;&#xff0c;卒于北宋神宗熙宁五年&#xff08;公元1072年&#xff09;&#xff0c;享年65岁。他是北宋时期著名的文学家、史学家&#xff0c;也是唐…

嘻嘻我是图床倒霉蛋

嘻嘻花了将近两个小时的时间配了一个小小的图床 手把手教你搭建阿里云图床(PicGoTypora阿里云OSS)&#xff0c;新手小白一看就会-阿里云开发者社区 (aliyun.com) 大体上按照这篇配置就好 七牛云因为测试域名30天到期,用自己的得备案,所以比较麻烦,建议直接上阿里云 我买了一…

SVM-SMO算法

SMO算法思想 上面这个优化式子比较复杂&#xff0c;里面有m个变量组成的向量α&#x1d6fc;需要在目标函数极小化的时候求出。直接优化时很难的。SMO算法则采用了一种启发式的方法。它每次只优化两个变量&#xff0c;将其他的变量都视为常数。由于 ∑ i 1 m α i y i 0 \su…

springboot与flowable(6):任务分配(监听器)

一、创建流程模型 制作如下流程 给审批用户1一个值表达式。 二、给用户审批2添加监听器 创建一个监听器类 package org.example.flowabledemo2.listener;import org.flowable.engine.delegate.TaskListener; import org.flowable.task.service.delegate.DelegateTask;/*** 自定…

【Spine学习05】之网格权重概念,以及让小臂动起来

上一节绑定好骨骼设置好了父级之后呢&#xff0c; 会发现操纵只有大臂能摆动&#xff0c;但是小臂以及手部无法K帧动起来。 所以这时候处理需要引入网格和权重的概念&#xff1a; 网格&#xff1a;在图片内设置多边形&#xff0c;操纵多边形顶点让图片变形 &#xff1a;启用按…

分布式技术导论 — 探索分析从起源到现今的巅峰之旅(分布式技术)

分析探索从起源到现今的巅峰之旅 背景介绍数据可伸缩性案例 计算可伸缩性案例 代价和权衡分布式的代价分布式的权衡权衡策略 分布式技术方向数据系统运算系统 分布式数据系统Partition&#xff08;分区&#xff09;Round-Robin&#xff08;轮询&#xff09;局限性 Range&#x…

【QT5】<重点> QT串口编程

目录 前言 一、串口编程步骤 0. 添加串口模块 1. 自动搜索已连接的串口 2. 创建串口对象 3. 初始化串口 4. 打开串口 5. 关闭串口 6. 发送数据 7. 接收数据 二、简易串口助手 1. 实现效果 2. 程序源码 3. 实现效果二 前言 本篇记录QT串口编程相关内容&#xff0…

半导体芯片结构以及译码驱动

一.半导体芯片结构 可能并不是只有一个芯片&#xff0c;有多个芯片就需要片选线了。 二.半导体存储芯片的译码驱动 主要有两种方式&#xff1a;线选法和重合法 线选法&#xff1a;每一个存储单元都用一根字选择线选中&#xff0c;直接选中存储单元的各位。&#xff08;一维…

Codeforces Round 953 (Div. 2)(A~D题解)

这次比赛是我最顺利的一次比赛&#xff0c;也是成功在中途打进前1500&#xff0c;写完第三道题的时候也是保持在1600左右&#xff0c;但是后面就啥都不会了&#xff0c;还吃了点罚时&#xff0c;虽说如此也算是看到进步了&#xff0c;D题学长说很简单&#xff0c;但是我当时分析…

增强大型语言模型(LLM)可访问性:深入探究在单块AMD GPU上通过QLoRA微调Llama 2的过程

Enhancing LLM Accessibility: A Deep Dive into QLoRA Through Fine-tuning Llama 2 on a single AMD GPU — ROCm Blogs 基于之前的博客《使用LoRA微调Llama 2》的内容&#xff0c;我们深入研究了一种称为量化低秩调整&#xff08;QLoRA&#xff09;的参数高效微调&#xff0…

非常好用的7个Vue3组件库!!【送源码】

说到Vue&#xff0c;怎能不提Vue3呢&#xff1f; 它的大名鼎鼎主要归功于一项革命性的创新——Composition API。 这个新功能为逻辑复用带来了前所未有的友好性和灵活性&#xff0c;让开发者们在构建应用时如鱼得水。 如果你现在正在使用Vue3&#xff0c;或者在新的一年考虑…