ReentrantLock详解

news2024/11/19 11:17:13

JUC中的锁API

在juc中有一个Lock接口他的作用和synchronized相似都是为了保证线程安全性提供的解决方案 Lock中定义了一系列释放锁和抢占锁相关的API
lock()
抢占锁资源 如果当前线程没有抢占到锁 则阻塞
tryLock()
尝试抢占锁资源 如果抢占成功则返回true 否则返回false
unlock()
释放锁

Lock的实现类

Lock是一个接口他只提供抽象方法 所有的实现由不同的子类去实现
ReentrantLock:
重入锁 属于排它锁类型 和synchronized相似
ReentrantReadWriteLock:
可重入读写锁 它一共提供了两把锁 一把是读锁(ReadLock)和一把写锁WriteLock
StampedLock:
java8新引入的锁机制 是ReentrantReadWriteLock的改进版本

ReentrantLock的基本应用

ReentrantLock是一把可以支持重入的排它锁 同一时刻只允许一个线程获得锁资源 而重入就是如果某个线程已经获得了锁资源那么该线程后续再去抢占锁资源时 不需要再加锁只需要记录重入次数

package com.alipay.alibabademo.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    private Lock lock = new ReentrantLock();

    private int count = 0;

    public void incr() {
        lock.lock();
        try {
            count++;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        Thread[] threads = new Thread[2];
        for (int i = 0; i <2 ; i++) {
            threads[i] = new Thread(()->{
                for (int j = 0; j <100000 ; j++) {
                    reentrantLockDemo.incr();
                }
            });
            threads[i].start();
        }
        threads[0].join();
        threads[1].join();
        System.out.println(reentrantLockDemo.count);
    }
}

上述案例代码中通过ReentrantLock的lock保证count++这个非原子操作加锁保证count++在多线程访问情况下的线程安全性

ReentrantReadWriteLock应用

package com.alipay.alibabademo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantReadWriteLockDemo {

    private Lock lock = new ReentrantLock();
    private List<String> dataList = new ArrayList<>();

    public void add(String value) {
        try {
            lock.lock();
            dataList.add(value);
        }finally {
            lock.unlock();
        }
    }
    
    public String get(int index) {
        lock.lock();
        try {
            return dataList.get(index);
        }finally {
            lock.unlock();
        }
    }

}

上述案例中提供的add 和get方法 由于ArrayList是线程不安全的类 所以分别在add和get上加了ReentrantLock的lock方法保证原子性(当然也可以通过别的方式比如CopyOnWriteArrayList)但是我们发现当一个线程访问get方法查询数据时 如果其他线程抢占了锁 则会使得该线程阻塞在get方法上 然而读取数据又不会对数据造成任何的影响所以这一操作时多余的 ,我们需要达到的目的是允许多个线程同时调用 get方法 但是只有一个任何一个线程在写 其他线程如果想写必须阻塞 这样大大的提升了读写的性能所以引入了ReentrantReadWriteLock(读写锁)

读写锁的特性

1.读/读不互斥 ,如果多个线程访问读方法 那么这些线程不会阻塞
2.读/写互斥 如果一个线程在访问读方法 另外一个线程访问写方法 那么为了保证数据的一致性调用写方法的线程要阻塞
3.写/写互斥 ,如果多个线程同时访问写方法 则必须要按照互斥规则进行同步

package com.alipay.alibabademo.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDemo {

    private ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    
    private Lock readLock   = reentrantReadWriteLock.readLock();
    
    private Lock  writeLock  = reentrantReadWriteLock.writeLock();
    private List<String> dataList = new ArrayList<>();

    public void add(String value) {
        try {
            writeLock.lock();
            dataList.add(value);
        }finally {
            writeLock.unlock();
        }
    }

    public String get(int index) {
        readLock.lock();
        try {
            return dataList.get(index);
        }finally {
            readLock.unlock();
        }
    }

}

ReentrantReadWriteLock通过读写两把锁的思想,从而减少了读操作带来的锁竞争提升了性能

StampedLock 应用

ReentrantReadWriteLock存在一个问题 如果当前有线程调用get方法 那么所有调用add方法的线程必须等待get方法的线程释放锁之后才能抢占锁进行写 也就相当于在读的过程中不允许 那么如果调用get方法的线程非常多 就会导致写线程一直被阻塞
为了解决ReentrantReadWriteLock中的问题 在java8引入了StampedLock 机制 提供了一种乐观锁策略 当线程调用get方法读取数据时 不会阻塞准备执行写操作的线程

 class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
 
    void move(double deltaX, double deltaY) { // an exclusively locked method
      long stamp = sl.writeLock();
      try {
        x += deltaX;
        y += deltaY;
      } finally {
        sl.unlockWrite(stamp);
      }
    }
 
    double distanceFromOrigin() { // A read-only method
      long stamp = sl.tryOptimisticRead();
      double currentX = x, currentY = y;
      if (!sl.validate(stamp)) {
         stamp = sl.readLock();
         try {
           currentX = x;
           currentY = y;
         } finally {
            sl.unlockRead(stamp);
        }
      }
      return Math.sqrt(currentX * currentX + currentY  currentY);
    }
 
    void moveIfAtOrigin(double newX, double newY) { // upgrade
      // Could instead start with optimistic, not read mode
      long stamp = sl.readLock();
      try {
        while (x == 0.0 && y == 0.0) {
          long ws = sl.tryConvertToWriteLock(stamp);
          if (ws != 0L) {
            stamp = ws;
            x = newX;
            y = newY;
            break;
          }
          else {
            sl.unlockRead(stamp);
            stamp = sl.writeLock();
          }
        }
      } finally {
        sl.unlock(stamp);
      }
    }
  }}

writeLock:获取写锁
readLock:获取读锁
tryOptimisticRead:获取读锁 当有线程获得读锁时 他不会阻塞其他线程的写操作,通过返回的stamp字段作为一个版本号 用来表示当前线程在读操作期间数据是否被修改过 。StampedLock提供了一个validate方法来验证stamp如果线程在读取过程中没有其他线程对数据进行修改 那么stamp的值不会发生变化 validate方法返回true 否则就验证失败但会false 在验证失败之后为了保证数据的一致性在通过readLock方法来获取阻塞机制的读锁

ReentrantLock实现原理

在这里插入图片描述
从上图可以看出 ReentrantLock定义了一个Sync同步类该类又有两个实现FairSync(公平同步)和NonfairSync(非公平同步)这两个类分别代表了ReentrantLock中的公平和非公平的特性而Sync又继承了AbstractQueuedSynchronizer 所以排它锁的一些逻辑应该是在AbstractQueuedSynchronizer 中完成的

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer 又被称为AQS是ReentrantLock实现同步锁的核心类
AQS中提供了两种锁的实现
1.独占锁 :同一时刻只能有一个线程获得锁
2.共享锁:同一时刻允许多个线程同时获得锁

AQS实现排它锁原理流程图

请添加图片描述

state字段表示互斥变量 当线程来抢占锁资源时 会基于该变量判断当前锁资源是否空闲
双向链表用于存储没抢占到锁资源的线程 每个队列中的线程都会有一个自旋的操作抢占锁
线程的阻塞和唤醒是通过LockSupport.park和unpark来实现

加锁流程

请添加图片描述

ReentrantLock源码解析

ReentrantLock.lock

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

sync是一个抽象的静态内部类通过继承AQS来实现重入锁的逻辑
sync有两个具体的实现
NonfairSync:非公平锁,允许在不排队的情况下直接尝试抢占锁默认使用非公平锁
FairSync:公平锁 必须按照FIFO的规则来访问锁资源

FairSync.lock

      final void lock() {
            acquire(1);
        }

NonfairSync.lock

先通过CAS抢占资源 如果成功就表示获得了锁 如果失败就调用acquire执行锁竞争

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

通过CAS乐观锁的方式 如果当前内存中seate的值和预期值expect相等则更新为update如果更新成功则返回true 否则返回false

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

当state =0时表示无锁状态
当state >0时表示已经有线程获得了锁 因为ReentrantLock可以重入所以当同一个线程多次获得同步锁的时候 state会底层 比如重入了三次 那么state = 3 而释放的时候也需要释放3次 直到state = 0其他线程才有资格获得锁

在非公平锁中如果CAS操作未成功 则说明已经有线程持有锁 此时会调用tryAcquire(1)

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

1.通过tryAcquire方法尝试获取独占锁 如果成功则返回true 否则返回false
2.如果tryAcquire方法返回false 这说明当前锁被占用 只能通过addWaiter方法将当前线程封装成Node并添加到AQS的同步队列中
3.acquireQueued方法将Node作为参数 通过自旋去尝试获取锁

tryAcquire(int arg)

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        final boolean nonfairTryAcquire(int acquires) {
          //获取当前获得锁的线程
            final Thread current = Thread.currentThread();
            //获取State状态
            int c = getState();
            //0表示无锁状态
            if (c == 0) {
                //通过cas替换State的值如果成功表示抢占到锁
                if (compareAndSetState(0, acquires)) {
                    //保存当前获得锁的线程   
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //current == getExclusiveOwnerThread()表示获得锁的线程是同一个表示线程可以重入 
            else if (current == getExclusiveOwnerThread()) {
                //如果是同一个线程来获得锁 则直接增加重入次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

addWaiter(Node mode)

当尝试获取锁失败之后会调用addWaiter把当前线程封装成一个Node加入同步队列中

    private Node addWaiter(Node mode) {
       //把当前线程封装成Node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //tail是AQS中的尾部 默认为null
        Node pred = tail;
        //在tail 不为空的情况下 队列中会存在节点
        if (pred != null) {
            //把当前线程的Node的prev指向tail
            node.prev = pred;
            //通过CAS把node加入AQS队列 
            if (compareAndSetTail(pred, node)) {
                //把原tail节点的next指向当前node
                pred.next = node;
                return node;
            }
        }
        //当tail = null时 把node添加到同步队列
        enq(node);
        return node;
    }

enq

   private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                 //如果尾节点 = = null 用cas 构建一个节点 
                if (compareAndSetHead(new Node()))
                    //把头节点赋值给尾节点
                    tail = head;
            } else {
                //如果尾节点不等于空 把当前节点当成尾节点 然后把prev指针指向上一个节点 把新进来的节点改成尾节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    //把上一个节点的next 指针指向刚进来的节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued

addWaiter方法把线程组装链表后把当前线程的Node节点作为参数传递给acquireQueued加入阻塞队列

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前一个节点
                final Node p = node.predecessor();
                //如果当前节点的前一个结点是头节点 则说明有资格去争夺锁
                if (p == head && tryAcquire(arg)) {
                    //把当前节点设置成头节点
                    setHead(node);
                    
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

检查当前节点的前置节点状态 如果是SIGNAL则表示可以放心的阻塞 否则需要通过compareAndSetWaitStatus修改前直接点的状态为SIGNAL

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前置节点的 waitStatus
        int ws = pred.waitStatus;
        //等待其他前直接点的线程被释放
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            //说明可以挂起
            return true;
        // 大于0 说明prev节点取消了排队 直接移除这个节点
        if (ws > 0) { 
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            设置prev 节点状态为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

Node的五种状态

CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0:新结点入队时的默认状态。

parkAndCheckInterrupt

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

cancelAcquire

 private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        //当前节点的前一个节点
        Node pred = node.prev;
        //前一个节点的  waitStatus> 0 (结束状态)
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        //将 node.waitStatus  设置成 CANCELLED 状态
        node.waitStatus = Node.CANCELLED;
        //4. 如果node是tail,更新tail为pred,并使pred.next指向null
        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
             //5. 如果node既不是tail,又不是head的后继节点
             //则将node的前继节点的waitStatus置为SIGNAL
             //并使node的前继节点指向node的后继节点
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                 //6. 如果node是head的后继节点,则直接唤醒node的后继节点
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

释放锁

当获得锁的线程需要释放锁的时候 调用ReentrantLock的unlock方法

 public void unlock() {
        sync.release(1);
    }
   public final boolean release(int arg) {
        //释放锁成功
        if (tryRelease(arg)) {
            Node h = head;
            //如果头节点不为空 并且状态不为0 
            if (h != null && h.waitStatus != 0)
                //唤醒
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {
            //state -1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                 //如果c =0 表示当前是无锁状态 把线程iq清空
                free = true;
                setExclusiveOwnerThread(null);
            }
            //重新设置 state
            setState(c);
            return free;
        }
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            //设置head节点的状态为0 
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        //拿到head节点的下一个节点
        Node s = node.next;
        //如果下一个节点为null 或者 status>0则表示是 CANCELLED 状态
        //听过尾部节点开始扫描  找到距离 head最近的一个 waitStatus<=0的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //如果next 节点不等于空直接唤醒这个线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

判断当前节点的状态 如果节点状态已失效 则从tail节点开始扫描找到离head节点最近且状态为SIGNAL的节点
通过LockSupport.unpark方法唤醒该节点
被唤醒的线程会再次去抢占锁资源

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

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

相关文章

简单的深度活体智能记忆模型

🍿*★,*:.☆欢迎您/$:*.★* 🍿 正文

基于Antd Input组件自定义Input的成功状态

前言 Ant Design的Input组件的有一个状态的Api 需求 公司自研UI组件&#xff0c;在Antd的基础上进行开发。其中Input组件除了警告与错误状态外&#xff0c;还增加了成功的状态。如下图⬇️ 开发实现 方案一&#xff1a;覆盖CSS样式 一开始准备通过判断状态来增加类名&am…

软件工程习题

软件工程第一章 软件与软件工程作业第二章 可行性研究作业第三章 需求分析作业第四章 总体设计作业第五章 详细设计作业第六章 软件编码测验第七章 软件测试作业选择判断简答题第一章 软件与软件工程作业 一、单选题&#xff08;共7题&#xff0c;58.1分&#xff09; 1、软件是…

刷题13-左右两边子数组的和相等

题目012-左右两边子数组的和相等 思路&#xff1a;用到了三个循环&#xff0c;从头到尾遍历数组&#xff0c;比较左右两边数组的和是否相等&#xff0c;当然这种思路时间复杂度也比较高 核心代码&#xff1a; class Solution {public int pivotIndex(int[] nums) {int sum1,…

6.2 、MyBatis 高级映射(resultMap 标签多表联查 , 一对多,多对一关系)

文章目录一、实现多表联查&#xff08;association 标签&#xff09;1、实现多对一关系结果集映射二、实现多表联查&#xff08;collection 标签&#xff09;一、实现多表联查&#xff08;association 标签&#xff09; association 标签&#xff1a; 实现一对一&#xff0c;多…

因果推断1--基本方法介绍(个人笔记)

目录 一、因果推断介绍 1.1 什么是因果推断 1.2为什么研究因果推断 1.3因果推断阶梯 1.4因果推断问题分类 二、因果推断理论框架 2.1 定义&#xff08;这些定义后面会经常用到&#xff09; 2.2 Assumptions&#xff08;三大基本假设&#xff09; 三、因果效应估计 3.1 因果效应…

JavaEE【Spring】:SpringBoot 配置文件

文章目录一、配置文件的作用二、配置文件的格式1、注意2、说明三、properties 配置文件说明1、基本语法2、读取配置文件① 注意3、优缺点四、yml 配置文件说明1、基本语法2、yml 使用进阶① yml 配置不同数据类型及 nullⅠ. yml 配置读取Ⅱ. 练习a. 值为 null 的配置b. 根本不存…

利用云服务器发布项目

前言 平时开发我会写一些小demo&#xff0c;我自己觉得有用的会集中起来形成一个项目&#xff0c;本来想利用gitee的gitee page直接部署出来&#xff0c;但后面了解了下&#xff0c;它只支持官网之类的静态页面&#xff0c;无法与后台数据交互&#xff0c;想要完整的服务还是得…

数据分析业务场景 | 用户画像

一.概况 定义 是根据用户的一系列行为和意识过程建立起来的多维度标签&#xff1b;是根据用户人口学特征&#xff0c;网络浏览内容&#xff0c;网络社交活动和消费行为等信息而抽象出的一个标签化的用户模型&#xff1b;首要任务&#xff1a;根据业务需求整理和数据情况分析建…

Springboot redirect重定向使用RedirecAtrributes传递数据

参考资料 【转载】关于重定向RedirectAttributes的用法RedirectAttributes 的使用 目录前期准备一. RedirecAtrributes重定向传参三. 重定向目标页面接收参数前期准备 ⏹配置文件 server:servlet:context-path: /jmw⏹访问url http://localhost:8080/jmw/test16/init?name…

NX二次开发(C#)-UI Styler-选择对象TaggedObject转换为Body、Face等对象

文章目录 1、前言2、选择对象的过滤器2、选择对象类型为TaggedObject3、TaggedObject转换为Face类型1、前言 前面的博客中已经写过了UI Styler中选择对象(selection)的一些内容,但是依然有读者不知道运用,本文将在前文的基础上更加深入的介绍选择对象的应用(建议与https:/…

DevExpress Universal添加对.NET 7的支持

DevExpress Universal添加对.NET 7的支持 DevExpress已经发布了整个产品系列的主要更新。 CodeRush Ultimate 22.2-为许多重构添加了核心性能优化和增强。 DevExpress.NET MAUI 22.2-添加了对Material Design 3指南的支持&#xff0c;以及对数据网格的自定义过滤、排序和分组。…

PCB封装

目录 1.PCB元器件库分类及命名 1.2PCB封装图形要求 2.封装制作 手工制作封装的操作步骤 1.PCB元器件库分类及命名 元器件采用大写字母表示&#xff0c;PCB元器件库分类及命名如表。 2.PCB封装图形要求 &#xff08;1&#xff09;外形&#xff1a;指元器件的最大外形尺寸。封…

【微电网优化】基于粒子群实现微网经济调度,环境友好调度附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Java 中的不同参数类型

是不是还傻傻分不清参数配置到底怎么写&#xff0c;写在哪个位置&#xff0c;那么这篇文章就让你学会。 目录 1、Program arguments 2、VM options 3、Environment variables 最佳实践 打开 IDEA 的 Run Configuration&#xff0c;可以看到以下参数配置 VM optionsProgram…

springboot+vue美食网站idea maven

目 录 摘 要 I 1 绪论 1 1.1研究背景 1 1. 2研究现状 1 1. 3研究内容 2 2 系统关键技术 3 2.1 springboot框架 3 2.2 JAVA技术 3 2.3 MYSQL数据库 4 2.4 B/S结构 4 3 系统分析 5 3.1 可行性分析 5 3.1.1 技术可行性 5 3.1. 2经济可行…

12.11哈希表

目录 一.哈希表 1.概念 2 冲突-概念 3 冲突-避免 4 冲突-避免-哈希函数设计 直接定制法--(常用) 2. 除留余数法--(常用) 3. 平方取中法--(了解) 4. 折叠法--(了解) 5. 随机数法--(了解) 6. 数学分析法--(了解) 5 冲突-避免-负载因子调节&#xff08;重点掌握&#…

151-160-mysql-高级篇-设计规范及其他调优策略

151-mysql-高级篇-设计规范以及其他调优策略&#xff1a; 1、数据库的设计规范 1. 范 式 1.1 范式简介 **在关系型数据库中&#xff0c;关于数据表设计的基本原则、规则就称为范式。**可以理解为&#xff0c;一张数据表的设计结构需要满足的某种设计标准的级别。要想设计一…

iOS 组件二进制与源码查看及调试方案

好久没有写文章了这里记录一下把项目代码二进制化提高编译效率的整个过程中碰到的问题和解决方案 先提一下优化编译速度的基本方向基本就是从不同的编译阶段来出主意&#xff0c;比如&#xff1a; 预编译阶段的头文件查找&#xff1a; 一款可以让大型iOS工程编译速度提升50%的…

[附源码]Node.js计算机毕业设计大学生心理咨询系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…