Java中的锁

从不同的角度看,Java中有许多类型的锁,下面是它们的简单介绍。

  • 从是否锁住同步资源来看

  • 1.1乐观锁

乐观锁认为所有拿到共享数据的线程都不会修改数据,只会查看数据,因此在获取共享数据时不会加锁,适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

  • 1.1.1 CAS

CAS全称 Compare And Swap。在不使用锁的情况下实现多线程之间的变量同步。

涉及到三个操作数:需要读写的内存值 oldValue,进行比较的值 oldValue2,要写入的新值 newValue。

JDK通过CPU的cmpxchg指令,去比较寄存器中的 oldValue2 和 内存中的值 oldValue。如果相等,就把要写入的新值 newValue 存入内存中。如果不相等,就将内存值 oldValue 赋值给寄存器中的值 oldValue2。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

  • 1.2 悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,适合写操作多的场景,先加锁可以保证写操作时数据正确。

  • 从同步资源的竞争程度来看

  • 2.1 偏向锁

当一段同步代码长时间只被同一个线程访问时,说明该块同步代码竞争程度非常低,没必要频繁的加锁解锁消耗系统资源,所以可以让线程来访问同步代码自动获取锁,这便是偏向锁。

只有存在其他线程竞争偏向锁的时候,持有偏向锁的线程才会释放掉偏向锁;偏向锁在JDK1.6之后是自动开启的,可以通过-XX:-UseBiasedLocking来进行操作。

  • 2.2 轻量级锁

当偏向锁被其他线程竞争时,便会升级为轻量级锁;另外,竞争偏向锁未成功时,该线程会进行自旋操作而不是直接阻塞,从而提高系统性能。

  • 2.3 重量级锁

当竞争偏向锁的线程自旋达到一定次数,或者在它自旋的时候,又来了一个竞争锁的线程,这时,轻量级锁会升级为重量级锁;升级成重量级锁之后,会将除了持有锁之外的线程都阻塞(自旋的停止自旋)。

  • 2.4 各量级锁的转换规则

锁升级的顺序是偏向锁-->轻量级锁-->重量级锁,锁只可以升级不可以降级。

  • 从申请锁的顺序来看

  • 3.1 公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

例如:你去食堂吃饭,你看到有人排列,你便自觉的去队尾排列,打饭的阿姨在打完一个饭的时候会按照顺序叫下一个同学来打饭。

  • 3.2 非公平锁

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

例如:你去食堂打饭,虽然已经有人在排队了,但是阿姨刚打完上一个同学的饭,还没来得及叫下一个人,你可以直接去插队打饭,阿姨就不会给队首的同学打饭,而是先给你打饭。

  • 从锁的共享程度来看

  • 4.1 共享锁

共享锁是指该锁可被多个线程所持有。JDK中的ReentrantReadWriteLock的ReadLock是共享锁。

  • 4.2 排他锁

排他锁,是指该锁一次只能被一个线程所持有。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

  • 从锁是否可以被一个线程重复获取来看

  • 可重入锁

可重入锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

示例代码

/**
 * 重入锁示例代码
 */
public class ReentrantLockDemo {

    public synchronized void getLock1(){
        System.out.println("获取对象锁的method1");
        getLock2();
    }

    public synchronized void getLock2(){
        System.out.println("获取对象锁的method2");
    }

    public static void main(String[] args) {
        new ReentrantLockDemo().getLock1();
    }
}

在执行getLock1方法时,已经获取了对象锁,执行getLock2方法时并未释放锁;而getLock2方法也是需要获取对象锁的,方法却可以正常执行,这便可以说明synchronized是可重入锁。

  • 不可重入锁

不可重入锁则不会让一个线程获取两次,相对可重入锁来说,更容易造成死锁。

volatile关键字

volatile关键字可以保证顺序性以及可见性,无法保证原子性,所以单纯的使用volatile关键字是无法实现多线程安全的,但是如果设计巧妙的话,也可以较为轻量的解决多线程安全问题,典型就是以“双重校验锁”的方式实现单例模式。

/**
 * 双重校验锁 实现单例模式
 */
public class SinglePattern {

    private static volatile SinglePattern singlePattern;

    private SinglePattern(){}

    public static SinglePattern getInstance(){
        if(singlePattern == null){
            synchronized(SinglePattern.class){
                if(singlePattern == null){
                    singlePattern = new SinglePattern();
                }
            }
        }
        return singlePattern;
    }
}

java内存模型

java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性,下面简单介绍下与java内存模型相关的happen-before原则。

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作
  • happen-before的传递性原则:如果A操作happen-before B操作,B操作happen-before C操作,那么A操作happen-before C 操作
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其他方法。
  • 线程中断的happen-before原则:对县城interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则:对象的初始化完成先于他的finalize方法的调用。

ThreadLocal

ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

Java的锁的理念是“时间换空间”,而ThreadLocal的理念是“用空间换时间”,下面写个例子演示下TheadLocal的用法。

public class ThreadLocalDemo implements Runnable{

    //设置一个threadLocal变量存储Integer
    private ThreadLocal<Integer> formatter = new ThreadLocal();

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getId()+" 当前取到的值为 "+formatter.get());

        formatter.set(new Integer(new Random().nextInt(100)));

        System.out.println("Thread Name= "+Thread.currentThread().getId()+" 设置后的值为 = "+formatter.get());


    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        Thread thread1 = new Thread(threadLocalDemo);
        Thread thread2 = new Thread(threadLocalDemo);
        Thread thread3 = new Thread(threadLocalDemo);
        Thread thread4 = new Thread(threadLocalDemo);
        Thread thread5 = new Thread(threadLocalDemo);
        Thread thread6 = new Thread(threadLocalDemo);
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();
        thread4.start();
        thread4.join();
        thread5.start();
        thread5.join();
        thread6.start();
    }
}

完整工程代码链接:https://github.com/youzhihua/Java-training