一篇文章带你深入理解JVM虚拟机读书笔记--锁优化

目录
  • 1. Java语言中的线程安全
    • 1.1 不可变
    • 1.2 绝对线程安全
    • 1.3 相对线程安全
    • 1.4 线程兼容
    • 1.5 线程对立
  • 2. 线程安全的实现方法
    • 2.1 互斥同步
  • 3. 锁优化
    • 3.1 自旋锁与自适应自旋
    • 3.2 锁消除
    • 3.3 锁粗化
    • 3.4 轻量级锁
    • 3.5 偏向锁
  • 总结

1. Java语言中的线程安全

按照线程安全的“安全程度”由强至弱来排序,可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1.1 不可变

不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。

Java语言中,如果多线程共享的数据是一个基本数据类型,那么只要定义时使用 final 关键字修饰他就可以保证它是不可变的,如果共享数据是一个对象类型,由于Java语言目前暂时还没有提供值类型的支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行。

例如:java.lang.String 类的对象实例,它是一个典型的不可变对象,用户调用它的 substring()、replace() 和 concat() 这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。

保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都声明为 final,这样在构造函数结束之后,它就是不可变的,例如下面代码中 java.lang.Integer 的构造函数,它通过将内部状态变量 value 定义为 final 来保障状态不变 :

/**
 * The value of the <code>Integer</code>.
 * @serial
 */
private final int value;
/**
 * Constructs a newly allocated <code>Integer</code> object that
 * represents the specified <code>int</code> value.
 *
 * @param value the value to be represented by the
 * <code>Integer</code> object.
 */
public Integer(int value) {
  	this.value = value;
}

在Java类库API中符合不可变要求的类型,除了上面提到的 String 之外,常用的还有枚举类型及 java.lang.Number 的部分子类,如 Long 和 Double 等数值包装类型、BigInteger 和 BigDecimal 等大数据类型。但同为Number子类型的原子类 AtomicInteger 和 AtomicLong 则是可变的。

1.2 绝对线程安全

Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。java.util.Vector 是个线程安全的容器,add()、get()、size() 等方法都是被synchronized修饰的,保证了具备原子性、可见性和有序性。不过,就算是所有方法都被synchronized修饰,那也不意味着调用它的时候就不需要同步手段了。

如下代码,对 Vector 线程安全性的测试 :

private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
  	while (true) {
    	for (int i = 0; i < 10; i++) {
        	vector.add(i);
      }
      Thread removeThread = new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
          }
        }
      };
      Thread printThread = new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < vector.size(); i++) {
            System.out.println((vector.get(i)));
          }
        }
      });
      removeThread.start();
      printThread.start();
      //不要同时产生过多的线程,否则会导致操作系统假死
      while (Thread.activeCount() > 20);
   }
}

运行结果如下:

Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException:
Array index out of range: 17
at java.util.Vector.remove(Vector.java:777)
at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21)
at java.lang.Thread.run(Thread.java:662)

尽管这里使用到的 Vector 的 get()、remove() 和 size() 方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用,再用i访问数组就会抛出一个 ArrayIndexOutOfBoundsException 异常。如果要保证这段代码能正确执行下去,我们不得不把 removeThread 和 printThread 的定义改成如下所示:

Thread removeThread = new Thread(new Runnable() {
  	@Override
  	public void run() {
    	synchronized (vector) {
      	for (int i = 0; i < vector.size(); i++) {
        	vector.remove(i);
      	}
    	}
  	}
});
Thread printThread = new Thread(new Runnable() {
  	@Override
  	public void run() {
    	synchronized (vector) {
      	for (int i = 0; i < vector.size(); i++) {
        	System.out.println((vector.get(i)));
      	}
    	}
  	}
});

假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。

1.3 相对线程安全

相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection() 方法包装的集合等。

1.4 线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类库API中大部分的类都是线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

1.5 线程对立

线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

2. 线程安全的实现方法

2.1 互斥同步

互斥同步(Mutual Exclusion & Synchronization)是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式。因此在“互斥同步”这四个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

在 Java 里面,最基本的互斥同步手段就是 synchronized 关键字,这是一种块结构(Block Structured)的同步语法。synchronized 关键字经过 Javac 编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。

如果 Java 源码中的 synchronized 明确指定了对象参数,那就以这个对象的引用作为 reference;如果没有明确指定,那将根据 synchronized 修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的 Class 对象来作为线程要持有的锁。

在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit 指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

由上面的描述,可得出 synchronized 锁的一些特性:

被 synchronized 修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。被 synchronized 修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。

从执行成本的角度看,synchronized 持有锁是Java语言中一个重量级(Heavy-Weight)的操作。主流 Java 虚拟机实现中,Java 的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转换需要耗费很多的处理器时间。

实际上,虚拟机本身会对 synchronized 持锁操作进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,以避免频繁地切入核心态之中(这个问题下面小节再讨论)。

3. 锁优化

3.1 自旋锁与自适应自旋

自旋锁:即,当一个线程去竞争共享资源时,该资源已被其他线程占用,这时该线程不会立刻进入挂起阻塞状态,而是通过不断自旋,等待共享资源使用权被持锁线程释放。当然,不可能让一个线程一直自旋等待下去,自旋等待的时间是必须有一定的限度的,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认值是 10 次,用户也可以使用参数 -XX:PreBlockSpin 来自行更改。

在JDK 6中对自旋锁的优化,引入了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。

3.2 锁消除

锁消除:是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享资源竞争的锁进行消除。

锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。

示例如下:(示例2-1)

public String concatString(String s1, String s2, String s3) {
  return s1 + s2 + s3;
}

由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。

在JDK 5之前,字符串加法会转化为 StringBuffer 对象的连续 append() 操作,在JDK 5及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作。

优化为如下所示:(示例2-2)

public String concatString(String s1, String s2, String s3) {
  StringBuffer sb = new StringBuffer();
  sb.append(s1);
  sb.append(s2);
  sb.append(s3);
  return sb.toString();
}

现在大家还认为这段代码没有涉及同步吗?每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,经过逃逸分析后会发现它的动态作用域被限制在 concatString() 方法内部。也就是 sb 的所有引用都永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。

客观地说,既然谈到锁消除与逃逸分析,那虚拟机就不可能是JDK 5之前的版本,所以实际上会转化为非线程安全的 StringBuilder 来完成字符串拼接,并不会加锁。但是这也不影响笔者用这个例子证明 Java 对象中同步的普遍性。

3.3 锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。

大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

(示例2-2)所示连续的 append() 方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以(示例2-2)为例,就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。

总之,如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

3.4 轻量级锁

轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。

例如在32位的HotSpot虚拟机中,对象未被锁定的状态下,Mark Word的32个比特空间里的25个比特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有 1 个比特固定为 0(这表示未进入偏向模式)。

对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种不同状态,这些状态下对象头的存储内容如下表所示:

在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,这时候线程堆栈与对象头的状态如下图所示:

然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如下图所示:

如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

3.5 偏向锁

偏向锁也是JDK 6中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。

偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

假设当前虚拟机启用了偏向锁(启用参数-XX:+UseBiased Locking,这是自JDK 6起HotSpot虚拟机的默认值),那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作 等)。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。偏向锁、轻量级锁的状态转化及对象Mark Word的关系如下图所示:

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java JVM虚拟机运行机制

    一:JVM基础概念 JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现.编译虚拟机的指令集与编译微处理器的指令集非常类似.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. Java虚拟机(JVM)是可运行Java代码的假想计算机.只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行. 下面看下Jvm的体系结构图: 二:解释型语言和编译型语言的联系与区别. 编译型语言是通过

  • 浅谈Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM给的内存不够用,怎么办? 此时,堆外内存登场!利用堆外内存,不仅可以随意操控内存,还能提高网络交互的速度. 背景1:JVM内存的分配 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里

  • Java 虚拟机(JVM)之基本概念详解

    1.类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间. 2.方法区:就是存放类信息.常量信息.常量池信息.包括字符串字面量和数字常量等.方法区是辅助堆栈的块永久区,解决堆栈信息的产生,是先决条件. 3.Java堆:再java虚拟机启动的时候建立Java堆,它是java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享的.堆解决的是数据存储问题,即数据怎么放.放在哪儿. 4.直接内存:Java的NIO库允许Ja

  • 详解JVM系列之对象的锁状态和同步

    java对象头 Java的锁状态其实可以分为三种,分别是偏向锁,轻量级锁和重量级锁. 在Java HotSpot VM中,每个对象前面都有一个class指针和一个Mark Word. Mark Word存储了哈希值以及分代年龄和标记位等,通过这些值的变化,JVM可以实现对java对象的不同程度的锁定. 还记得我们之前分享java对象的那张图吗? javaObject对象的对象头大小根据你使用的是32位还是64位的虚拟机的不同,稍有变化.这里我们使用的是64位的虚拟机为例. Object的对象头,

  • JVM---jstack分析Java线程CPU占用,线程死锁的解决

    本文章主要演示在Windows环境,Linux环境也差不多. 一.分析CPU占用飙高 首先写一个Java程序,并模拟一个死循环.让CPU使用率飙高.CPU负载过大的话,新的请求就处理不了了,这就是很多程序变慢了甚至不能访问的原因之一. 下面是我这里的Controller,启动程序之后,开多个请求访问这个方法.死循环代码就不贴了,自己构造.我这里模拟的一个截取字符串的死循环. /** * 演示死循环导致cpu使用率飙高 * */ @RequestMapping("/loop") publ

  • elasticsearch启动警告无法锁定JVM内存

    elasticsearch启动警告 Unable to lock JVM memory (ENOMEM). This can result in part of the JVM being swapped out. Increase RLIMIT_MEMLOCK (ulimit). 内存锁定值的限制(max locked memory) 这个值只对普通用户起作用,对超级用户不起作用,这个问题是由于CAP_IPC_LOCK造成的.linux对内存是分页管理的,这意味着有不需要时,在物理内存的数据会

  • 一篇文章带你深入理解JVM虚拟机读书笔记--锁优化

    目录 1. Java语言中的线程安全 1.1 不可变 1.2 绝对线程安全 1.3 相对线程安全 1.4 线程兼容 1.5 线程对立 2. 线程安全的实现方法 2.1 互斥同步 3. 锁优化 3.1 自旋锁与自适应自旋 3.2 锁消除 3.3 锁粗化 3.4 轻量级锁 3.5 偏向锁 总结 1. Java语言中的线程安全 按照线程安全的"安全程度"由强至弱来排序,可以将Java语言中各种操作共享的数据分为以下五类:不可变.绝对线程安全.相对线程安全.线程兼容和线程对立. 1.1 不可变

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你顺利通过Python OpenCV入门阶段

    目录 1. OpenCV 初识与安装 2. OpenCV 模块简介 3. OpenCV 图像读取,显示,保存 4. 摄像头和视频读取,保存 5. OpenCV 常用数据结构和颜色空间 6. OpenCV 常用绘图函数 7. OpenCV 界面事件操作之鼠标与滑动条 8. 图像像素.通道分离与合并 9. 图像逻辑运算 10. 图像 ROI 与 mask 掩膜 11. 图像几何变换 12. 图像滤波 13. 图像固定阈值与自适应阈值 14. 图像膨胀腐蚀 15. 边缘检测 16. 霍夫变换 17.

  • 一篇文章带你了解Java Spring基础与IOC

    目录 About Spring About IOC Hello Spring Hello.java Beans.xml Test.java IOC创建对象的几种方式 Spring import settings Dependency Injection 1.构造器注入 2.set注入 3.拓展注入 P-namespcae&C-namespace Bean scopes singleton prototype Bean的自动装配 byName autowire byType autowire 小结

  • 一篇文章带你吃透Vue生命周期(结合案例通俗易懂)

    目录 1.vue生命周期 1.0_人的-生命周期 1.1_钩子函数 1.2_初始化阶段 1.3_挂载阶段 1.4_更新阶段 1.5_销毁阶段 2.axios 2.0_axios基本使用 2.1_axios基本使用-获取数据 2.2_axios基本使用-传参 2.3_axios基本使用-发布书籍 2.4_axios基本使用-全局配置 3.nextTick和refs知识 3.0$refs-获取DOM 3.1$refs-获取组件对象 3.2$nextTick使用 3.3$nextTick使用场景 3.

  • 一篇文章带你吃透Vuex3的状态管理

    目录 一. Vuex是什么 Vue全局事件总线 Vuex状态管理 何时使用Vuex 二. 纯vue组件案例 计算总数案例 添加人员案例 三. Vuex工作原理和流程 第一种工作流程 第二种工作流程 生活化的Vuex工作原理 四. 在项目中引入Vuex 安装Vuex 创建store 在Vue中挂载store 五. Vuex的核心属性用法 单一数据源(state) 状态更新方式(mutations) store中的计算属性(getters) 异步更新状态(actions) 同步增加总数 异步增加总数

  • 一篇文章带你搞懂Java线程池实现原理

    目录 1. 为什么要使用线程池 2. 线程池的使用 3. 线程池核心参数 4. 线程池工作原理 5. 线程池源码剖析 5.1 线程池的属性 5.2 线程池状态 5.3 execute源码 5.4 worker源码 5.5 runWorker源码 1. 为什么要使用线程池 使用线程池通常由以下两个原因: 频繁创建销毁线程需要消耗系统资源,使用线程池可以复用线程. 使用线程池可以更容易管理线程,线程池可以动态管理线程个数.具有阻塞队列.定时周期执行任务.环境隔离等. 2. 线程池的使用 /** *

  • 一篇文章带你弄清楚Redis的精髓

    目录 一.Redis的特性 1.1 Redis为什么快? 1.2 Redis其他特性 1.3 Redis高可用 二.Redis数据类型以及使用场景 2.1 String 2.1.1 基本指令 2.1.2 应用场景 2.2 Hash 2.2.1 基本指令 2.2.2 应用场景 2.3 List 2.3.1 基本指令 2.3.2 应用场景 2.4 Set 2.4.1 基本指令 2.4.2 应用场景 2.5 ZSet(SortedSet) 2.5.1 基本指令 2.5.2 应用场景 三.Redis的事

  • 一篇文章带你使用Typescript封装一个Vue组件(简单易懂)

    一.搭建项目以及初始化配置 vue create ts_vue_btn 这里使用了vue CLI3自定义选择的服务,我选择了ts.stylus等工具.然后创建完项目之后,进入项目.使用快捷命令code .进入Vs code编辑器(如果没有code .,需要将编辑器的bin文件目录地址放到环境变量的path中).然后,我进入编辑器之后,进入设置工作区,随便设置一个参数,这里比如推荐设置字号,点下.这里是为了生成.vscode文件夹,里面有个json文件. 我们在开发项目的时候,项目文件夹内的文件很

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

随机推荐