Java中常用锁


Java中常用锁

1 各种锁概念及使用

1.1 synchronized

非公平锁
? JDK早期 重量级锁,向OS申请系统锁
? 锁升级改进概念:syschronized锁开始只记录线程ID(偏向锁,偏向锁只记录线程的ID,实际不是真正的加锁 只记录状态),如果线程争抢,升级为自旋锁(自旋锁占用CPU不经过内核态,自旋锁适用于执行时间短,线程少),10次自旋之后升级为重量级锁-向OS申请系统锁(进入等待队列)。
错误的锁对象
synchronized不能用string常量(常量拼接后默认使用Stringbuild.toString创建新对象,导致锁的不是同一个对象)、Integer(在-128~127范围使用栈赋值方式声明变量,超过该范围会创建一个新对象) 、Long(与Integer范围相同)。
为了防止锁对象发生改变,通常需要在锁定的值上面加final修饰。

1.2 volatile

  1. 保证线程可见
    volatile可见性是针对引用,引用内部发生了改变对其他线程依旧不可见。
    MESI CPU缓存一致性
public class ThreadVolatile {
    private volatile static boolean running = true;
    public static void m1() {
        System.out.println("T1 start!!");
        while (running) {
//            System.out.println("hello");
        }
        System.out.println("T1 end!!");
    }

    public static void main(String[] args) {
        try {
            new Thread(ThreadVolatile::m1).start();
            Thread.sleep(500);
            running = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 输出:
     *  T1 start!!
     * 结论:
     * 1)当m1方法中System.out.println被注释掉,且running没有被   
         volatile修饰时,
     *   程序出现死锁.running=false线程间不可见;
     * 2)当m1方法中System.out.println输出后,且running没有被
         volatile修饰时,
     *   线程正常执行完成.原因是println方法从使用了synchronized修饰;
     * 3)当running被volatile修饰时,程序正常执行
     */
}

  1. 禁止指令重排(CPU)
    指令重排即CPU和编译器为了提高执行效率会进行指令重排。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条内存屏障则会告诉编译器和CPU,不管什么指令都不能和这条指令重排,也就是说通过插入内存屏障,就能禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
    volatile禁止的是JVM级别的指令重排,不是CPU级别的。
public class VolatileDemo {
    private /*volatile*/ boolean statu = true;
    void m(){
        System.out.println("m==start");
        while (statu){

        }
        System.out.println("m==end");
    }

    public static void main(String[] args) {
        ValatileDemo t1 = new ValatileDemo();
        new Thread(()->{
            t1.m();

        },"t2").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.statu = false;
    }
}
/**
 * 1)没有加volatile
 * 执行结果: m==start
 * 程序一直卡住,没有按我们期望t1.statu = false 后停止
 *
 * 2)加了volatile后程序按期望停止
 *
 */
  1. volatile不保证原子性
    volatile不能保证多个线程共同修改变量时所带来的不一致问题
public class VolatileDemo2 {
    volatile int count = 0;
//    int count = 0;

    private /*synchronized*/ void c() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        VolatileDemo2 demo = new VolatileDemo2();
        List threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                demo.c();
            }).start();
        }
        threads.forEach(t -> {
            try {

                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(demo.count);
    }
}
/**
 * 测试结果:每次运算结果都不是我们期望的10万
 * 原因:volatile保证了可见性但是没有保证原子性
 * 解决方案:去掉volatile 给累加方法添加synchronize
 */

1.3 CAS

无锁优化 自旋 乐观锁
java.util.concurrent.atomic.Atomic…类都是通过CAS实现来保证线程安全,cas(期望值,原来值,更新值)。
CAS底层:CompareAndSet(比较并交换)。CAS是CPU原支持,执行判断过程是不能不打乱。
CAS在极端情况下可能会出现ABA问题
什么是ABA问题?
考虑如下操作:
并发1:获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功
并发2:将数据修改成B
并发3:将数据修改回A
并发1:CAS乐观锁,检测发现初始值还是A,进行数据修改
上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。
ABA问题的优化
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。
优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。
常见实践:“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。

1.4 Lock

可中断锁 公平锁/非公平锁
Lock需手动开启和释放锁,可以使用tryLock尝试获取锁,不管锁定与否,方法都将继续执行,可以通过tryLock返回值判断是否锁定,也可以根据tryLock的时间判断是否成功获取锁。使用synchronize遇到异常JVM会自动释放锁,底层也是CAS实现,当进入等待队列后也会存在锁升级使用LockSupport。
Lock的锁定后可以允许被打断
Lock默认非公平锁,可以设置未公平锁

// 非公平锁
Lock unFairLock = new ReentrantLock();
// 公平锁
Lock fairLock = new ReentrantLock(true);

1.5 CountDownLatch与CyclicBarrier

  • CountDownLatch
    减计数方式,计算为0时释放所有等待的线程,计数为0时无法重置。调用counDown()方法计数减一,调用await()方法只进行阻塞,对计数没有任何影响。不可重复利用。
  • CyclicBarrier
    加计数方式,计数达到指定值时释放所有等待线程,计划达到指定值时,计算置为0重新开始。调用await()方法计数加1,若加1后的值不等于构造方法指定的值,则线程阻塞。可重复利用。

1.6 ReadWriteLock

读写锁 写锁为排他锁,读锁为共享锁
读写锁只有读操作的时候不会上锁,当有写操作的时候读操作会被上锁。可以提升读的操作。如果用Re

public class ReadWriteLockDeme {
    private int value;
    // 读
    public void read(Lock lock){
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("Read over!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    //写
    public void write(Lock lock,int v){
        try {
            lock.lock();
            this.value = v;
            Thread.sleep(1000);
            System.out.println("write over!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        //独占锁
//        Lock reentrantLock = new ReentrantLock();
        //读写锁
        ReadWriteLock lock = new ReentrantReadWriteLock();
        Lock readLock = lock.readLock();
        Lock writeLock = lock.writeLock();

        ReadWriteLockDeme deme = new ReadWriteLockDeme();

        for (int i = 0; i < 2; i++) {
            int value = i;
            new Thread(()->{
//                deme.write(reentrantLock, value);
                deme.write(writeLock, value);
            }).start();
        }
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
//                deme.read(reentrantLock);
                deme.read(readLock);
            }).start();
        }
    }
}
/**
 * 运行结果:
 * 1) 当读写使用ReentrantLock时,无论线程读或写其他线程都将进入等待状态;
 * 2) 当使用ReadWriteLock时,当有一个写线程执行写操作时其他所有线程等待,
 *    只有读操作时,所有读线程可以共享读,读效率非常高.
 */

1.7 Semaphore

凭证、信号灯。Semaphore创建时会指定凭证数量,当线程获取到凭证时开始执行,未获取凭证的线程进入等待状态。
可用于限流场景。

//默认非公平,传true为公平锁
Semaphore semaphore = new Semaphore(1,false);
    new Thread(()->{
        try {
            //未获取凭证会进入阻塞状态
            semaphore.acquire();
            System.out.println("T1 Start");
            System.out.println("T1 End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            semaphore.release();
        }
    }).start();
    new Thread(()->{
        try {
            //未获取凭证会进入阻塞状态
            semaphore.acquire();
            System.out.println("T2 Start");
            System.out.println("T2 End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            semaphore.release();
        }
    }).start();
/**
 * 执行结果:
 * T1 Start
 * T1 End
 * T2 Start
 * T2 End
 */

1.8 Exchanger

2个线程之间数据交换。当线程执行到exchange时,都会进入阻塞队列。

Exchanger exchanger = new Exchanger<>();
        new Thread(() -> {
            String value = "T1";
            try {
                value = exchanger.exchange(value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + value);
        }, "t1").start();
        new Thread(() -> {
            String value = "T2";
            try {
                value = exchanger.exchange(value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + value);
        }, 
/**
 * 执行结果:
 * t2 T1
 * t1 T2
 */

1.9 LockSupport

用于创建锁和其他同步类的基本线程阻塞原语。
这个类与使用它的每个线程关联一个许可证(类似信号)。如果许可证可用,将执行,否则阻塞。许可证可以提前获取。但与Semaphore信号灯不同,许可证不会累积,最多有一个

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 5; i++) {
                System.out.print(i+" ");
                if(i==3){
                    //阻塞
                    LockSupport.park();
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        /**
         * 输出结果:0 1 2 3 4 
         * 结论1:
         *    提前颁发凭证,t1线程在遇到LockSupport.park()时不会停止.
         */
        LockSupport.unpark(t1);  
        //2秒后唤醒t1线程继续执行
//        Thread.sleep(2000);
//        System.out.print(" 5秒后 ");
//        LockSupport.unpark(t1);
        /**
         * 输出结果:0 1  5秒后 2 3 4
         * 结论2:
         *   LockSupport.park()线程进入阻塞状态,
         *   当遇到LockSupport.unpark(t1)后,t1获取了凭证继续唤醒执行
         */
    }

1.10 AQS

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
ReentrantLock跟常用的Synchronized进行比较

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。主要原理图如下:

AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。