Category: language

  • Why does the JVM consume less memory than -Xms specified?

    You’re looking at the resident memory – that is, the physical RAM consumed. See here for more info.

    The virtual memory, however, is the memory consumed by your application, including the memory swapped out (to disk). You’ll see there’s a closer correspondance with the virtual memory and your -Xms settings.

    why-does-the-jvm-consume-less-memory-than-xms-specified
    (more…)

  • java锁机制介绍

    java锁

    乐观锁与悲观锁

    乐观锁与悲观锁都是一种锁机制。用于实现线程同步机制。

    • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。适用于数据频繁修改的场景。

    • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

    java中的锁机制

    • synchronized

      在java5之前,使用关键字synchronized修饰一个方法或者代码块对临界资源的保护实现多线程并发同步。有四种不同的同步块:

      • 实例方法
      • 静态方法
      • 实例方法中的同步块
      • 静态方法中的同步块
      synchronized(lockObject) { 
          //需要访问的临界资源 
      }
      
    • ReentrantLock

      Java5 java.util.concurrent.locks 提供了Lock接口。Lock提供了更加灵活的锁实现。写法一般如下:

      lock.lock();
      try {
          //需要访问的临界资源 
      } finally {
          lock.unlock();
      }
      

      获取锁的方式:

      • lock() 阻塞持续等待直到获取锁
      • tryLock() 方式获得锁,默认不阻塞,不等待,如果能够获取锁返回true,可以设置等待时间。
      • lockInterruptibly() 中断锁, 使用中断锁在等待获取锁的时候,会响应线程中断。

      在Java1.5中,synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。到了Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。

      两种锁在低并发的情况下性能都差不多,更推荐使用synchronized,毕竟不用怕忘记在finnaly中执行unlock()。

      但在高并发量的条件下,synchronized性能会迅速下降几十倍,因为多了很多自旋锁操作,而ReentrantLock的性能却能依然维持一个水准。

      当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的

    • Semaphore

      • 使用方法与ReentrantLock相似。
      • acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致。
      • 支持多个临界资源,而ReentrantLock只支持一个临界资源
    • Atom系列原子类
      public class AtomicInteger extends Number implements java.io.Serializable {
          private static final long serialVersionUID = 6214790243416807050L;
          /**
           * Atomically adds the given value to the current value.
           *
           * @param delta the value to add
           * @return the updated value
           */
          public final int addAndGet(int delta) {
              return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
          }
      }
      

      通过CAS实现,是乐观锁的一种。所以性能比上面的高。

    • StampedLock
      java8 java.util.concurrent.locks新增的一个API。

      ReentrantReadWriteLock有很多缺点:

      • 有可能导致线程饥饿,在读多写少的模型,锁获取的时候是随机分配的,很可能导致写线程一直获取不到write lock。
      • 无法将读锁升级为写锁
      • 不支持乐观读

      StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

      method tryConvertToWriteLock(long)attempts to “upgrade” a mode, returning a valid write stamp if

      • (1) already in writing mode
      • (2) in reading mode and there are no other readers or
      • (3) in optimistic mode and the lock is available.
      //读锁升级为写锁
      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);
           }
      }
      

    非阻塞算法

    在轻度到中度的争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及线程挂起和上下文切换,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话肯定是真的,因为没有争用的锁涉及 CAS 加上额外的处理),而争用的 CAS 比争用的锁获取涉及更短的延迟。

    在高度争用的情况下(即有多个线程不断争用一个内存位置的时候),基于锁的算法开始提供比非阻塞算法更好的吞吐率,因为当线程阻塞时,它就会停止争用,耐心地等候轮到自己,从而避免了进一步争用。

    • 优势:以硬件代替JVM的锁,锁的粒度更加细,带来了良好的性能。
    • 劣势:需要冒险,用循坏来实现失败重试,甚至有可能因为内存复用机制,带来ABA问题。随着临界资源的增加,编程的复杂度急剧上升。
      //非阻塞算法堆栈
      public class ConcurrentStack<E> {
          AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();
          public void push(E item) {
              Node<E> newHead = new Node<E>(item);
              Node<E> oldHead;
              do {
                  oldHead = head.get();
                  newHead.next = oldHead;
              } while (!head.compareAndSet(oldHead, newHead));
          }
          public E pop() {
              Node<E> oldHead;
              Node<E> newHead;
              do {
                  oldHead = head.get();
                  if (oldHead == null) 
                      return null;
                  newHead = oldHead.next;
              } while (!head.compareAndSet(oldHead,newHead));
              return oldHead.item;
          }
          static class Node<E> {
              final E item;
              Node<E> next;
              public Node(E item) { this.item = item; }
          }
      }
      

    ##乐观锁ABA问题

    比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

    ABA问题是无锁结构实现中常见的一种问题,可基本表述为:

    1. 进程P1读取了一个数值A
    2. P1被挂起(时间片耗尽、中断等),进程P2开始执行
    3. P2修改数值A为数值B,然后又修改回A
    4. P1被唤醒,比较后发现数值A没有变化,程序继续执行。
    

    对于P1来说,数值A未发生过改变,但实际上A已经被变化过了,继续使用可能会出现问题。

    参考链接:

  • kmp匹配字符串

     

    kmp算法常用于优化字符串匹配,例如下面的问题:

    有一个字符串a = “BBC ABCDAB ABCDABCDABDE”,我想知道,里面是否包含另一个字符串b = “ABCDABD”

    一般的做法是循环比较。

    func contain(a string, b string) bool {
      for i := 0; i < len(a); i++ {
        k := i
        for j := 0; j < len(b); {
          if b[j] == a[k] {
            if j == len(b)-1 {
              return true
            }
            j++
            k++
          } else {
            //匹配失败
            j = 0
            break;
          }
        }
      }
      return false
    }
    

    可以看到耗时O(n*m),每一次失败都是从头逐个比较,特别对于ABCDAB多次重复匹配,kmp算法通过配置部分匹配表,减少这些重复匹配操作,从而达到优化效果。

    KMP算法的精髓在于对已知信息的充分利用,理解kmp算法首先要了解一些概念:

    1. 字符串前缀,真前缀,后缀,真后缀,及前缀函数

    举个例子,如字符串 ABCDABD,首先,不考虑空字符

    前缀有A,AB,ABC,ABCD,ABCDA,ABCDAB,ABCDABD

    真前缀有A,AB,ABC,ABCD,ABCDA,ABCDAB

    同理可以理解后缀,真前(后)缀就是指不包含自身的前(后)缀

    真后缀为BCDABD,CDABD,DABD,ABD,BD,D

    “真前缀”指除了最后一个字符以外,一个字符串的全部头部组合;

    “真后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

    这里指的是真前缀后缀
       - "A"的前缀和后缀都为空集,共有元素的长度为0;
      - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
      - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;zh
      - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
      - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
      - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
      - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

    前缀函数next[]是指某个字符串的最长真后缀同时也是它的前缀的子串长度。

    array = {A, B, C, D, A, B, D}

    next  = {-1, 0, 0, 0, 1, 2, 0}

    “部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置.

    /**
    部分匹配表
     */
    func prefixFunction(str string) []int {
      sLen := len(str)
      prefix := make([]int, sLen)
    
      i := 0
      for j := 1; j < sLen-1; j++ {
    
        if str[j] == str[i] {
          i++
          prefix[j] = i
        } else {
          i = 0
        }
      }
    
      return prefix
    }
    
    /**
    转移数组
     */
    func obtainMoveFromPrefix(prefix []int) [] int {
      sLen := len(prefix)
      move := make([]int, sLen)
      move[0] = -1;
      for i := 0; i < sLen; i++ {
        if i == 0 {
          move[0] = -1;
        } else {
          move[i] = prefix[i-1]
        }
      }
      return move
    }
    
    
    func contain(a string, b string) bool {
      m, n := len(a), len(b)
      move := obtainMoveFromPrefix(prefixFunction(b))
    
      j := 0
      for i := 0; i < m; i++ {
    
        //有匹配的字符
        for j > 0 && b[j] != a[i] {
          j = move[j]
          if j < 0 {
            j = 0
          }
        }
        if b[j] == a[i] {
          j++
        }
        if j == n {
          return true
        }
      }
    
      return false
    
    }
    

    参考文章:

    kmp算法-wiki

    字符串匹配的KMP算法

    KMP 算法详细解析

    从有限状态自动机的角度理解KMP算法

     

     

  • 闭包

    以前看js的时候,老是说,闭包闭包,但是一直搞不清楚,感觉就像个函数。最近看go,看到这篇文章,感觉有点明了。
    Go by Example 中文:闭包
    在我看来,闭包就是匿名函数使用了外部变量。

    在wiki里的描述:
    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
    在没有闭包的语言中,变量的生命周期只限于创建它的环境。但在有闭包的语言中,只要有一个闭包引用了这个变量,它就会一直存在。清理不被任何函数引用的变量的工作通常由垃圾回收完成

    package main
    
    import "fmt"
    
    func intSeq() func() int {
       i := 0
       return func() int {
          i = i + 1
          return i
       }
    }
    
    func main() {
       nextInt := intSeq();
       fmt.Println(nextInt())
       fmt.Println(nextInt())
       fmt.Println(nextInt())
       fmt.Println(nextInt())
       fmt.Println(nextInt())
    
       fmt.Println("-------")
       nextInt2 := intSeq();
       fmt.Println(nextInt2())
    
       fmt.Println("-------")
    }