ReentrantReadWriteLock(可重入读写锁)源码解读与使用

news2024/11/24 3:18:59

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java源码解读-专栏

🏷️个人学习笔记,若有缺误,欢迎评论区指正 

目录

1. 前言 

2. 读写锁是什么

3. ReentrantReadWriteLock是什么

4. 源码解读

4.1. ReadLock 

4.2. WriteLock

5. 基本使用

6.性能测试

7. 总结


1. 前言 

最近还在持续阅读JUC包下各种类的源码,JUC包下的每个类设计都十分巧妙,推荐小伙伴们去阅读下,一定会有不少收获的。

假如有人问你用过哪些读写锁,你会怎么回答呢,ReentrantLock和synchronized?实际上ReentrantLock和synchronized是互斥锁而不是读写锁,主要是为了确保对共享资源的互斥访问。

如果对读写锁部署,以下是一个参考回答:
在我的项目中,我主要使用 ReentrantLock 来确保对共享资源的互斥访问。虽然我没有直接使用过 ReentrantReadWriteLock,但我了解到它是 Java 提供的一个高级同步机制,特别适用于读多写少的场景。它维护了一对锁,一个用于读操作,允许多个线程同时读取资源,另一个用于写操作,确保在写入时独占访问。如果在未来遇到适合的场景,我会考虑使用 ReentrantReadWriteLock 来提高系统的并发性能

Java提供了两个读写锁类,分别是ReentrantReadWriteLock和StampedLock,ReentrantReadWriteLock就是我们今天要注重讲解的内容,StampedLock下次再更(鼠鼠肝不动了)。

2. 读写锁是什么

我们以往的学习旅程中,我们已经接触了如 synchronized 和 ReentrantLock 这样的互斥锁。这类锁的主要优势在于它们确保了线程的安全性,但它们的局限性在于同一时间仅允许一个线程持有锁,这在一定程度上降低了处理效率。另一方面,我们之前探讨的 Semaphore 虽然允许多个线程同时获取许可,但在保障线程安全方面表现不足。我们寻求的是一种既高效又安全的同步机制。

在实际应用场景中,数据读取操作的频率往往远高于写入操作。因此,富有远见的开发者们设计了一种新型锁——读写锁。在这种锁的设定下,读取数据时采用共享模式,允许多个线程同时持有读锁;而在写入数据时,为了确保线程安全,则切换到独占模式,确保同一时刻只有一个线程能够持有写锁。这样的设计理念催生了读写锁,它旨在提高并发性能的同时,不牺牲安全性。

3. ReentrantReadWriteLock是什么

ReentrantReadWriteLock是ReadWriteLock 接口的默认实现类,从名字可以看得出它也是一种具有可重入性的锁,同时也支持公平与非公平的配置,底层有两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有,也是基于AQS实现的底层锁获取与释放逻辑。

ReentrantReadWriteLock类内部的组成架构图如下:

4. 源码解读

我首先抽取了ReentrantReadWriteLock类中的核心源码,如下:

// 内部结构
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/*1、用以继承AQS,获得AOS的特性,以及AQS的钩子函数*/
abstract static class Sync extends AbstractQueuedSynchronizer {
    // 具体实现
}
/*非公平模式,默认为这种模式*/
static final class NonfairSync extends Sync {
    // 具体实现
}
/*公平模式,通过构造方法参数设置*/
static final class FairSync extends Sync {
    // 具体实现
}
/*读锁,底层是共享锁*/
public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
    }
    // 具体实现
}
/*写锁,底层是独占锁*/
public static class WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
    }
    // 具体实现
}

// 构造方法,初始化两个锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

// 获取读锁和写锁的方法
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

上面为底层的主要构造内容,ReentrantReadWriteLock中共写了5个静态内部类,各有功效,在上面的注释中也有提及。 

其中Sync,FairSync,NonFairSync在我们前面的文章时经常涉及到,大概都是Sync继承AQS,获得AQS的特性,然后实现AQS的钩子函数来自定义获取锁和释放锁的逻辑。FairSync和NonFairSync就是在Sync基础上加入了公平和非公平的特性。这三个类我们就不细讲了,我们着重看 ReadLock 和 WriteLock ,也就是读锁和写锁。

4.1. ReadLock 

public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;

        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

        public void lock() {
            sync.acquireShared(1);
        }

        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }

        public boolean tryLock() {
            return sync.tryReadLock();
        }

         public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }

       public void unlock() {
            sync.releaseShared(1);
        }


    }

ReadLock借助Sync来实现锁的获取与释放,可以通过构造函数传参来判断使用FairSync还是NonFairSync。

lock方法通过acquireShared共享方式来获取资源,深入acquireShared方法,发现里面调用了AQS的钩子函数acquireShared()

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

钩子函数acquireShared() 的实现在Sync,如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    protected final int tryAcquireShared(int unused) {
            // 1.获取当前线程,当前锁的状态(state值,0即为没人持锁)
            Thread current = Thread.currentThread();
            int c = getState();
            // 2.如果锁被占了(state!=0),且持有锁的线程不是当前线程,返回-1,获取锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 3.获取共享锁持有数量
            int r = sharedCount(c);
            // 4.调用readerShouldBlock()判断是否要排队(如果非公平就返回false)
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                // 5.获取锁(CAS操作修改state值)
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果读锁计数从0变为1,记录当前线程为第一个读线程,并设置其持有计数为1。
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                // 如果当前线程已经是第一个读线程,增加其持有计数。
                    firstReaderHoldCount++;
                } else {
                // 如果当前线程不是第一个读线程,则更新持有计数。
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // 5.上述步骤中的任何条件都失败了,就进行完整的尝试获取读锁的循环,包括处理重入获取的情况。
            return fullTryAcquireShared(current);
        }
}
  1. 检查是否有写锁被其他线程持有

    • 使用getState()获取当前锁的状态。
    • 调用exclusiveCount(c)检查是否有独占锁(写锁)被持有(即状态字段的低16位是否为0)。
    • 如果有独占锁且持有独占锁的线程不是当前线程,则返回-1,表示获取读锁失败。
  2. 检查是否应该阻塞

    • 调用readerShouldBlock()方法来确定当前线程是否应该因为锁的队列策略而阻塞。这通常是基于公平性策略来决定的,如果是非公平模式,通常返回false。
    • 检查当前读锁的计数r是否小于最大值MAX_COUNT。
  3. 尝试更新状态

    • 使用compareAndSetState(c, c + SHARED_UNIT)尝试通过CAS操作增加读锁的计数(状态字段的高16位)。如果成功,则表示获取读锁成功。
    • 如果读锁计数从0变为1,记录当前线程为第一个读线程,并设置其持有计数为1。
    • 如果当前线程已经是第一个读线程,增加其持有计数。
    • 如果当前线程不是第一个读线程,则更新cachedHoldCounter或readHolds中的持有计数。
  4. 如果尝试失败

    • 如果上述步骤中的任何条件失败(如应该阻塞、CAS操作失败、读锁计数饱和等),则调用fullTryAcquireShared(current)方法,这个方法会进行完整的尝试获取读锁的循环,包括处理重入获取的情况。

总之,tryAcquireShared方法是一个尝试快速获取读锁的方法,它会尽可能地避免阻塞,并在可能的情况下立即返回。如果快速路径失败,它会调用fullTryAcquireShared方法进行更全面的尝试。

4.2. WriteLock

public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

        public void lock() {
            sync.acquire(1);
        }

        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }

        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }

        public void unlock() {
            sync.release(1);
        }


        public Condition newCondition() {
            return sync.newCondition();
        }

       public String toString() {
            Thread o = sync.getOwner();
            return super.toString() + ((o == null) ?
                                       "[Unlocked]" :
                                       "[Locked by thread " + o.getName() + "]");
        }

        public boolean isHeldByCurrentThread() {
            return sync.isHeldExclusively();
        }

         public int getHoldCount() {
            return sync.getWriteHoldCount();
        }
    }

lock方法通过acquire独占方式来获取资源,深入acquire方法,里面调用了AQS的钩子函数tryAcquire()

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

同上,钩子函数acquireShared() 的实现在Sync,如下: 

abstract static class Sync extends AbstractQueuedSynchronizer {
    protected final boolean tryAcquire(int acquires) {
            // 1.获取当前线程和锁的状态
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            // 2.如果锁被持有(state != 0)
            if (c != 0) {
                // 3.如果写锁计数w为0,则表示读锁被持有,
                // 此时获取写锁失败,返回false。
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 4.重入次数加上acquires参数后超过了最大计数MAX_COUNT,则抛出错误
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 5.当前线程持有锁,state+1即可,保持锁的重入性
                setState(c + acquires);
                return true;
            }
            // 6.如果writerShouldBlock()方法返回true,表示当前线程应该因为锁的队列策略而阻塞,则返回false。否则通过CAS操作获取锁
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
}
  1. 检查锁的状态

    • 使用getState()获取当前锁的状态。
    • 使用exclusiveCount(c)获取状态字段的低16位,即写锁的计数。
    • 如果锁的状态c不为0,则表示锁已经被持有。
  2. 处理锁已被持有的情况

    • 如果写锁计数w为0,则表示读锁被持有,或者写锁被其他线程持有,此时获取写锁失败,返回false。
    • 如果当前线程不是持有写锁的线程,则获取写锁失败,返回false。
    • 如果当前线程已经持有写锁,并且重入次数加上acquires参数后超过了最大计数MAX_COUNT,则抛出错误,因为超过了锁的最大重入次数。
    • 如果当前线程已经持有写锁,则增加写锁的计数(重入锁),更新状态,并返回true。
  3. 尝试获取锁

    • 如果writerShouldBlock()方法返回true,表示当前线程应该因为锁的队列策略而阻塞,则返回false。
    • 使用compareAndSetState(c, c + acquires)尝试通过CAS操作更新状态来获取写锁。如果成功,则表示获取写锁成功。
    • 如果CAS操作成功,设置当前线程为锁的持有者setExclusiveOwnerThread(current),并返回true。

5. 基本使用

那么这个读写锁如何使用呢?我们接下来通过一个小小的案例来示范下。

public class Test {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private int data = 0;

    /**
     * 写方法
     * @param value
     */
    public void write(int value) {
        //注意,获取锁的操作要在try/finally外面
        lock.writeLock().lock(); // 获取写锁
        try {
            data = value;
            System.out.println("线程:"+Thread.currentThread().getName() + "写" + data);
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }

    public void read() {
        lock.readLock().lock(); // 获取读锁
        try {
            System.out.println("线程:" + Thread.currentThread().getName() + "读" + data);
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        // 创建读线程
        Thread readThread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.read();
            }
        });

        Thread readThread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.read();
            }
        });

        // 创建写线程
        Thread writeThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.write(i);
            }
        });

        readThread1.start();
        readThread2.start();
        writeThread.start();

        try {
            readThread1.join();
            readThread2.join();
            writeThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果为:

线程:Thread-0读0
线程:Thread-1读0
线程:Thread-2写0
线程:Thread-2写1
线程:Thread-2写2
线程:Thread-2写3
线程:Thread-2写4
线程:Thread-0读4
线程:Thread-1读4
线程:Thread-0读4
线程:Thread-1读4
线程:Thread-0读4
线程:Thread-1读4
线程:Thread-0读4
线程:Thread-1读4

 通过输出内容,我们进一步得证,在ReentrantReadWriteLock在使用读锁时,可以支持多个线程获取读资源,而在调用写锁时,其他读线程和写线程均阻塞等待当前线程写完。

6.性能测试

既然都说读写锁能够提高并发性能,接下来我们就测试以下,测试代码已同步到仓库:Concurrent-MulThread/7-lock-performance-test(github.com)

public class LockPerformanceTest {

    private static final int READ_THREADS = 10; //读操作线程数
    private static final int WRITE_THREADS = 2; //写操作线程数
    private static final int ITERATIONS = 100000; // 操作次数

    private static final Lock reentrantLock = new ReentrantLock();
    private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static final Lock readLock = readWriteLock.readLock();
    private static final Lock writeLock = readWriteLock.writeLock();

    private static int sharedResource = 0;

    public static void main(String[] args) throws InterruptedException {
        long startTime, endTime;

        // 测试 ReentrantLock
        startTime = System.currentTimeMillis();
        testReentrantLock();
        endTime = System.currentTimeMillis();
        System.out.println("ReentrantLock time: " + (endTime - startTime) + " ms");

        // 重置共享资源
        sharedResource = 0;

        // 测试 ReentrantReadWriteLock
        startTime = System.currentTimeMillis();
        testReentrantReadWriteLock();
        endTime = System.currentTimeMillis();
        System.out.println("ReentrantReadWriteLock time: " + (endTime - startTime) + " ms");
    }

    private static void testReentrantLock() throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(READ_THREADS + WRITE_THREADS);

        for (int i = 0; i < READ_THREADS; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                    for (int j = 0; j < ITERATIONS; j++) {
                        reentrantLock.lock();
                        // 读取共享资源
                        int value = sharedResource;
                        reentrantLock.unlock();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        for (int i = 0; i < WRITE_THREADS; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                    for (int j = 0; j < ITERATIONS; j++) {
                        reentrantLock.lock();
                        // 写入共享资源
                        sharedResource++;
                        reentrantLock.unlock();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    private static void testReentrantReadWriteLock() throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(READ_THREADS + WRITE_THREADS);

        for (int i = 0; i < READ_THREADS; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                    for (int j = 0; j < ITERATIONS; j++) {
                        readLock.lock();
                        // 读取共享资源
                        int value = sharedResource;
                        readLock.unlock();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        for (int i = 0; i < WRITE_THREADS; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                    for (int j = 0; j < ITERATIONS; j++) {
                        writeLock.lock();
                        // 写入共享资源
                        sharedResource++;
                        writeLock.unlock();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

 输出结果为:

ReentrantLock time: 31 ms
ReentrantReadWriteLock time: 1 ms

可以看到在都保证了线程安全的情况下,ReentrantReadWriteLock比ReentrantLock快了不少,ReentrantReadWriteLock性能这么快,那么有啥缺点呢?答案是有的:

  • 不支持分布式:单机锁的通病,这个没办法
  • 线程饥饿问题:在写的时候,是独占模式,其他线程不能读也不能写,这时候若有大量的读操作的话,那这些线程也只能等待着,从而带来写饥饿。

在另一个读写锁工具类StampedLock中就解决了饥饿问题,下次再讲解

7. 总结

ReentrantReadWriteLock是 Java 提供的一个高级同步机制,特别适用于读多写少的场景。它维护了一对锁,一个用于读操作,允许多个线程同时读取资源,另一个用于写操作,确保在写入时独占访问。相比于ReentrantLock 直接锁读写会有更细的锁粒度,提高读写的并发性能,但也存在线程饥饿问题,也就是在写的时候,其他线程不能读也不能写,这时候若有大量的读操作的话,就会让很多线程等待,造成饥饿问题,在StampedLock中解决了这个问题,下次讲解。

此外,博主祝您五一小长假快乐~~

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

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

相关文章

每日OJ题_贪心算法二⑥_力扣409. 最长回文串

目录 力扣409. 最长回文串 解析代码 力扣409. 最长回文串 409. 最长回文串 难度 简单 给定一个包含大写字母和小写字母的字符串 s &#xff0c;返回 通过这些字母构造成的 最长的回文串 。 在构造过程中&#xff0c;请注意 区分大小写 。比如 "Aa" 不能当做一个…

spring框架学习记录(1)

前半个月一直在应付期中考试&#xff0c;快被折磨似了orz 文章目录 SpringIoC(Inversion of Control) 控制反转与DI(Dependency Injection)依赖注入bean相关bean配置bean实例化bean的生命周期 依赖注入相关依赖注入方式依赖自动装配 容器创建容器获取bean Spring IoC(Inversi…

场景文本检测识别学习 day08(无监督的Loss Function、代理任务)

无监督的Loss Function&#xff08;无监督的目标函数&#xff09; 根据有无标签&#xff0c;可以将模型的学习方法分为&#xff1a;无监督、有监督两种。而自监督是无监督的一种无监督的目标函数可以分为以下几种&#xff1a; 生成式网络的做法&#xff0c;衡量模型的输出和固…

网络安全审计

一、什么叫网络安全审计 网络安全审计是按照一定的安全策略&#xff0c;利用记录、系统活动和用户活动等信息&#xff0c;检查、审查和检验操作时间的环境及活动&#xff0c;从而发现系统漏洞、入侵行为或改善系统性能的过程&#xff0c;它是提高系统安全性的重要手段。 系统…

巨人网络发布2023年年报:全力拥抱AI浪潮,开启游戏产业新篇章

易采游戏网5月3日消息&#xff0c;国内知名游戏公司巨人网络发布了其2023年度财务报告&#xff0c;报告显示&#xff0c;公司在过去一年中积极拥抱AI技术&#xff0c;实现了业绩的稳步增长&#xff0c;为游戏产业带来了新的活力与机遇。 在报告中&#xff0c;巨人网络详细阐述了…

XYCTF2024 RE Trustme 复现

但是只得到用户名 admin 法一&#xff1a;猜Sql注入&#xff0c;直接万能密码 法二&#xff1a;正常逆向 jadx中的AndroidManifest.xml有奇怪之处 怀疑有加壳&#xff0c;进ProxyApplication看看 大量安卓一代壳的特征 Android第一代壳加固原理及实现 - 知乎 GitHub - Huye…

零基础学习数据库SQL语句之定义数据库对象的DDL语句

DDL语句 DDL Date Definition Language 数据定义语言&#xff0c;用来定义数据库对象&#xff08;数据库&#xff0c;表&#xff0c;字段&#xff09; 基本操作 数据库操作 查询所有数据库 SHOW DATEBASES查询当前数据库 SELECT DATEBASE() 创建 CREATE DATEBASE [IF …

张大哥笔记:付费进群创业项目玩法及详细操作教程

今天给大家分享一个赚钱小项目&#xff0c;它就是付费进群系统&#xff0c;这个项目应用范围很广。比如表情包&#xff0c;知识付费&#xff0c;美女写真&#xff0c;虚拟资料&#xff0c;交友扩列等都可以。今天我们就来说一下最近爆火的交友搭子付费进群做个演示案例&#xf…

你不可不知的数字可视化的未来。

10年UI设计和前端开发接单经验&#xff0c;完工项目1000&#xff0c;持续为友友们分享有价值、有见地的干货观点&#xff0c;有项目外包需求的老铁&#xff0c;欢迎关注发私信。 数据可视化是一个不断发展和创新的领域&#xff0c;未来的发展方向可能包括以下几个方面&#xff…

深度学习Day-15:LSTM实现火灾预测

&#x1f368; 本文为&#xff1a;[&#x1f517;365天深度学习训练营] 中的学习记录博客 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制] 要求&#xff1a; 了解LSTM是什么&#xff0c;并使用其构建一个完整的程序&#xff1b;R2达到0.83&#xff1b; 一、…

小米电脑回收站已清空?别急,恢复数据有妙招

在数字时代&#xff0c;电脑已经成为我们日常生活和工作中不可或缺的工具。对于小米电脑用户而言&#xff0c;回收站是一个方便我们管理文件的工具&#xff0c;但有时候&#xff0c;误操作或不小心清空回收站可能会让我们面临数据丢失的风险。那么&#xff0c;当小米电脑的回收…

Linux学习之路 -- 文件 -- 文件操作

在学习C语言时&#xff0c;我们就学习过文件相关的内容&#xff0c;但是由于知识储备尚且不足&#xff0c;无法深入的了解文件&#xff0c;下面我们就要重新认识一下文件。 <1> 简单介绍(铺垫) 1.前面我们说过&#xff0c;文件 内容 属性&#xff0c;所以我们对文件的…

ThreeJS:坐标辅助器与轨道控制器

ThreeJS与右手坐标系 使用ThreeJS创建3D场景时&#xff0c;需要使用一个坐标系来定位和控制对象的位置和方向。 ThreeJS使用的坐标系是右手坐标系&#xff0c;即&#xff1a;X轴向右、Y轴向上、Z轴向前&#xff0c;如下图所示&#xff0c; ThreeJS-右手坐标系 Tips&#xff1a;…

java技术栈快速复习05_基础运维(linux,git)

Linux知识总览 linux可以简单的理解成和window一样的操作系统。 Linux和Windows区别 Linux是严格区分大小写的&#xff1b;Linux中一切皆是文件&#xff1b;Linux中文件是没有后缀的&#xff0c;但是他有一些约定俗成的后缀&#xff1b;Windows下的软件一般是无法直接运行的Li…

JDK14特性

JDK14 1 概述2 语法层面的变化1_instanceof的模式匹配(预览)2_switch表达式(标准)3_文本块改进(第二次预览)4_Records 记录类型(预览 JEP359) 3 API层面的变化4 关于GC1_G1的NUMA内存分配优化2_弃用SerialCMS,ParNewSerial Old3_删除CMS4_ZGC on macOS and Windows 4 其他变化1…

《Git---Windows Powershell提交信息中文乱码解决方案》

解释&#xff1a; Windows PowerShell中的Git乱码通常是因为字符编码不正确或Git配置不支持Windows系统的默认编码导致的。Git在处理文件时可能使用UTF-8编码&#xff0c;而Windows系统的命令行工具&#xff08;如PowerShell&#xff09;默认使用的是Windows-1252或GBK编码。 …

Unity Animation--动画剪辑

Unity Animation--动画剪辑 动画剪辑 动画剪辑是Unity动画系统的核心元素之一。Unity支持从外部来源导入动画&#xff0c;并提供创建动画剪辑的能力使用“动画”窗口在编辑器中从头开始。 外部来源的动画 从外部来源导入的动画剪辑可能包括&#xff1a; 人形动画 运动捕捉…

3.4 无关、基和维度

这一节是关于子空间的真实大小。对于 m n m\times n mn 的矩阵&#xff0c;它有 n n n 个列&#xff0c;但是它真正的维数不一定为 n n n&#xff0c;维数可以由无关列的个数来得到。列空间的实际维度就是秩 r r r。 无关的概念是用于向量空间中的任意向量 v 1 , . . . ,…

C++入门第二节--关键字、命名空间、输入输出

点赞关注不迷路&#xff01;本节涉及c入门关键字、命名空间、输入输出... 1. C关键字 C总计63个关键字&#xff0c;C语言32个关键字 asmdoifreturntrycontinueautodoubleinlineshorttypedefforbooldynamic_castintsignedtypeidpublicbreakelselongsizeoftypenamethrowcaseen…

【Java EE】CAS原理和实现以及JUC中常见的类的使用

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…