java 锁 及 线程

2020-07-16  本文已影响0人  _大叔_

一、基础感念

在了解锁之前,有很多的基础感念需要先理解以下,方便以后我们对各种情况的锁的问题,有更好的认识。

同步 和 异步

   同步就是多个任务一个一个执行,即你在学习的时候不可能会打游戏,打游戏的时候不可能在学习。
   异步就是我洗衣服可以用洗衣机洗,边洗边打电话,而且打电话的同时还是能在做其他的事情,这个就是异步执行,但异步执行是一种,即做即完的

并发 和 并行

   并行 和 异步看似很像,但感念是完全不一样的。如果上述说异步是一个人可以做很多事情,那并行可以说多个人做不同的事情,即多个CPU处理不同的指令才能叫做并行,一个CPU处理多线程并不能称之为并行,而是并发。

临界区

   临界区用来表示一种公共资源或共享数据,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
   在并行程序中,临界区资源是要被保护的对象,如果资源同时被两个线程操作,则会得到破坏。

阻塞 和 非阻塞

   阻塞是当临界资源被抢占,其他线程则需要在外等待资源释放,这种等待的过程称为阻塞。
   非阻塞是不会受因为资源被抢占,而不去做其他事情。

死锁 饥饿 活锁

   死锁、饥饿、活锁都属于多线程活跃性问题。
   死锁是一个严重的程序上设计出现的问题,当一个资源被占用,因程序的意外问题,导致资源无法被释放,则其他线程就一直等待造成的情况被称为死锁。
   饥饿是指某一个或者多个线程因为种种原因无法获得所需的资源,导致一直无法执行。比如他的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致底优先级线程无法工作。
   活锁是多个线程之间互相谦让而导致的,你让我我让你,或者说他们级别一样导致。

并发的级别

   由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致可以分为阻塞、无饥饿、无障碍、无锁、无等待几种。

无饥饿

   饥饿的产生是因为底优先级在临界区被高优先级的线程插队而导致一直无法获取资源(也可以称未非公平锁),解决饥饿就是让锁变得公平,要想获得资源,就必须乖乖排队,管你优先级高低,先到先得。

无障碍

   无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。也就是说大家都可以大摇大摆的进入临界区,那么如果一起修改共享数据,把数据修改坏了怎么办?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所作的的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。
   如果说阻塞的控制方式是悲观策略。也就是说,系统认为两个线程之间很有可能发生不幸的冲突,因此,以保护共享数据为第一优先级。相对来说,非阻塞的调度就是一种乐观的策略。它认为多个线程之间很有可能不会发生冲突,或者说概率不大,因此大家都应该无障碍的执行,但是一旦检测到冲突,就应该回滚。
   从这个策略中可以看到,无障碍的多线程程序不一定能顺畅的运行。因为当临界区中存在严重的冲突时,所有的线程都可能不断的回滚自己的操作,而没有一个线程可以走出临界区,这种情况会影响系统的正常执行。所以,我们可能会非常希望在这一堆线程中,至少可以有一个线程在有限的时间内完成自己的操作,而退出临界区。这样至少可以保证系统不会再临界区中无限的等待。
   一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果说两者是一致的,则说明资源区没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。

无锁

   无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限时间内完成操作离开临界区。
   在无锁的调用中,一个典型的特点是可能会包含一个去穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,程序退出 ,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程可以胜出的,不至于全军覆没。至于临界区中竞争失败的线程,他们则必须不断重试,直到自己胜利。如果运气不好,总是尝试不成功,则会出类似饥饿的现象,线程会停止不前。

无等待

   无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果再进行优化,还可以进一步分解为有限无等待和线程数无关的无等待几种,他们之前的区别只是对循环次数的限制不同。
   一种典型的无等待结构就是RCU(read-copy-update)。它的基本思想是,对数据的读可以不加控制。因此所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机回写数据。

原子性

   是指一个操作是不可被中断的,即使多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

可见性

   指当一个线程修改了共享变量的值,其他线程是否能够立即知道这个修改。

有序性

   有序性的问题是因为在程序执行时,可能会进行指令的重排,重排后的指令与原指令的顺序未必一致。(这种情况会出现在并发程序设计中)

二、线程

状态

NEW 新建;
RUNNABLE 可运行状态;
BLOCKED 阻塞(遇到 synchronized,直到获得锁);
WAITING 无时间的等待( wait(),notify());
TIMED_WAITING 有时间的等待;
TERMINATED 结束。

suspend()暂停 resume()继续

   字面意思,但 suspend() 不会释放锁,必须调用 resume()才能释放锁,但是如果意外的 resume() 比 suspend() 提前执行,则其他线程永远等待,变为死锁。

stop() 强行终止线程

   Thread.stop(); 强行终止线程,会导致数据不一致,破坏数据。

interrupt() isInterrupted() interrupted() 中断线程

   Thread.interrupt() 中断线程,也就是设置中断标志位。Thread.isInterrupted() 判断当前线程是否被中断。 Thread.interrupted() 也是用来判断当前线程的中断状态。如果在线程中使用了 Thread.sleep(),那么要中断一个线程必须也在 Thread.sleep() 的catch 语句中 在执行一次当前线程的中断。

Thread.sleep() 方法由于中断而抛出异常,此时,他会清除中断标志,如果不加处理,那么在下次执行线程时,就无法判断这个中断标志,会继续执行线程,并不会达到中断线程。
中断是不会释放锁的。

    public static void main(String[] args) throws InterruptedException {
        String a = "1";
        Thread[] threads = new Thread[2];
        for(int i=0;i<2;i++){
            int b = i;
            threads[i] = new Thread(() -> {
                while (true) {
                    synchronized (a) {
                        try {
                            System.out.println("线程启动 " + b);
                            if (Thread.currentThread().isInterrupted()) {
                                System.out.println("线程中断" + b);
                                break;
                            }
                            Thread.sleep(5000);
                            System.out.println("执行完毕 " + b);
                        } catch (InterruptedException e) {
                            System.out.println("老子被中断了 " + b);
                            // 这里必须在中断一次,否则
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            });
        }
        threads[0].start();
        Thread.sleep(2000);
        threads[0].interrupt();
        threads[1].start();
    }
输出结果:
线程启动 0
老子被中断了 0
线程启动 0
线程中断0
线程启动 1
执行完毕 1
线程启动 1
执行完毕 1
线程启动 1
wait() notify() notifyAll() 等待和唤醒

   如果一个线程调用了 object.wait(),那么它就会进入object对象的等待队列,这个等待队列中可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当 object.notify() 被调用时,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。需要大家注意的是这个选择是不公平的,并不是先等待的线程会优先被选择,这个选择完全是随机的。object.notifyAll() 它和notify() 的功能基本一致,但不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。
   object.wait() 和 object.notify() 必须在对应的 synchronized 语句中,需要首先获得目标对象的一个监视器。

wait() 方法只会释放当前对象的锁,不会释放所有锁。
notify()不会立刻立刻释放sycronized(obj)中的obj锁,必须要等notify()所在线程执行完容synchronized(obj)块中的所有代码才会释放这把锁。

join() 等待线程结束,yield() 谦让
    public volatile static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for(i=0;i<100000;i++);
        });
        thread.start();
        thread.join();
        System.out.println(i);
    }

   join() 会一直阻塞线程直到目标线程执行完毕。如果不使用join() 等待 thread,那么得到的 i 很可能是0 或者一个非常小的数字。因为 thread 还没开始执行,i 的值就已经被输出了。
   yield() 会使当前线程让出CPU。但让出CPU并不代表当前线程不执行了。当前线程让出CPU后,会进行CPU资源的争夺,但是否能够再次被分配,就不一定了。如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用 yield() ,给予其他重要线程更多的工作机会。

yield 不会释放锁,需执行完毕

ThreadGroup 线程组
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup threadGroup = new ThreadGroup("订单组");
        Thread t1 = new Thread(threadGroup,() -> {
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名称 :" + name);
        },"下单");
        Thread t2 = new Thread(threadGroup,() -> {
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名称 :" + name);
        },"取消订单");
        t1.start();
        t2.start();
        System.out.println(threadGroup.activeCount());
        threadGroup.list();
    }

结果:
2
java.lang.ThreadGroup[name=订单组,maxpri=10]
    Thread[下单,5,订单组]
    Thread[取消订单,5,订单组]
当前线程名称 :取消订单
当前线程名称 :下单
setDaemon() 守护线程

   守护线程是一种特殊的线程,就和他的名字一样,它是系统的守护者,在后台默默的完成一些系统的任务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,这也意味着这个程序实际上无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,当一个java应用内,只有守护线程时,java虚拟机就会自然退出。

        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();

   设置守护线程必须在start()之前设置。如果上述例子两个都是守护线程,则不会等到线程里打印结果,程序直接结束。用户线程的话,会等到线程以上两个线程执行完成,再主线程结束。

setPriority() 线程优先级

   java中,使用1-10表示线程优先级,数字越大则越优先。

三、volatile

    当你用 volatile 去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够"看到"这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性、有序性、原子性。
   volatile 对于保证操作的原子性是有非常大的帮助的。但是需要注意的是,volatile 并不能代替锁,他也无法保证一些复合操作的原子性。比如 i++

四、synchronized

    synchronized 的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性。

    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = "aaa";
        String b = "aaa";
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    public static void add(){
        i++;
    }
结果:
200000
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = "aaa";
        String b = "bbb";
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    public static void add(){
        i++;
    }
结果:
112775
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = new String("aaa");
        String b = new String("aaa");
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
        System.out.println(a);
        System.out.println(b);
    }
    public static void add(){
        i++;
    }
结果:
106451
aaa
aaa

四、ReentrantLock 重入锁

  当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。特别注意,若一个线程多次获得锁,那么在释放所得时候,也必须释放相同次数。
   synchronized 也是重入锁,当一个类里的 A、B、C三个方法都被加上 synchronized 则A调用B,B调用C 会依次正确调用执行,如果 synchronized 不是重入锁,则这种调用方式会被 成为死锁,因为 A B C 三个方法持有的是同一个实例。

reentrantLock.lockInterruptibly() 中断处理

   在等待锁的过程中,程序可以根据需要取消对锁的申请。lockInterruptibly() 对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。

reentrantLock.tryLock() 锁申请等待限时

   tryLock() 有两种方法

ReentrantLock(true) 公平锁

   在大多情况下锁都是非公平的。也就是说,线程1 和 线程2 同时请求了锁A,那么当锁A可用时,是线程1可以获得锁还是线程2可以获得锁呢?这是不一定的,系统只是会从这个锁的等待队列种随机挑选一个。
   当 new ReentrantLock(true) 表示是公平的。但要实现一个公平锁,必然要求系统维护一个有序队列,因此公平锁的实现成本比较高了,如果没有特别的需要,也不需要使用公平锁。

reentrantLock.lock(); 获得锁,如果锁被占用则等待;
reentrantLock.tryLock(); 线程尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false
reentrantLock.tryLock(long timeout,TimeUnit unit); 线程如果在指定等待时间内获得了锁,就返回true,否则返回 false
reentrantLock.unlock(); 释放锁
reentrantLock.isHeldByCurrentThread() 当前线程是否持有该锁
reentrantLock.lockInterruptibly() 获得锁,但有线响应中断
reentrantLock.getHoldCount(); 当前线程调用 lock() 方法的次数
reentrantLock.getQueueLength(); 当前正在等待获取 Lock 锁的线程的估计数
reentrantLock.getWaitQueueLength(Condition condition); 当前正在等待状态的线程的估计数,需要传入 Condition 对象
reentrantLock.hasWaiters(Condition condition); 查询是否有线程正在等待与 Lock 锁有关的 Condition 条件
reentrantLock.hasQueuedThread(Thread thread); 查询指定的线程是否正在等待获取 Lock 锁
reentrantLock.hasQueuedThreads(); 查询是否有线程正在等待获取此锁定
reentrantLock.isFair(); 判断当前 Lock 锁是不是公平锁
reentrantLock.hasQueuedThread(Thread thread); 查询指定的线程是否正在等待获取 Lock 锁
reentrantLock.hasQueuedThread(Thread thread); 查询指定的线程是否正在等待获取 Lock 锁
reentrantLock.hasQueuedThread(Thread thread); 查询指定的线程是否正在等待获取 Lock 锁

Condition 条件
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            try{
                System.out.println("进入测试");
                lock.lock();
                System.out.println("获取锁");
                condition.await();
                System.out.println("等待结束");
                Thread.sleep(5000);
                System.out.println("这是对我的一次测试");
            }catch(Exception e){
                lock.unlock();
            }
        }).start();
        Thread.sleep(3000);
        System.out.println("等待三秒结束");
        lock.lock();
        condition.signal();
        lock.unlock();
    }
结果:
进入测试
获取锁
等待三秒结束
等待结束
这是对我的一次测试

   和Object 里waite() notify() 一样,当线程使用 condition.await()时,要求线程持有相关的重入锁,在 condition.await() 调用后,这个线程会释放这把锁。同理,在 condition.signal() 方法调用时,也要求线程先获得相关锁,在 condition.signal() 方法调用后,系统会从当前 Condition 对象的等待队列中,唤醒一个线程,一旦线程唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行。因此,在 condition.signal() 方法调用后,一般需要释放相关的锁,让给被唤醒的线程,让它继续执行。

五、信号量 Semaphore

   信号量为多线程写作提供更为强大的控制方法。广义上讲,信号量是对锁的扩展。无论是内部 synchronized 还是 ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问摸一个资源。在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。

Semaphore semaphore = new Semaphore(3);
Semaphore semaphore1 = new Semaphore(3,true); // 第二个参数指定是否公平
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for(int i=0;i<20;i++){
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    Thread.sleep(2000);
                    System.out.println("结束 -> "+ Thread.currentThread().getId());
                    semaphore.release();
                }catch (Exception e){

                }
            }).start();
        }
    }
结果:
结束 -> 13
结束 -> 14
结束 -> 12
结束 -> 15
...

六、ReetrantReadWriteLock 读写锁

   ReetrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。

七、CountDownLatch 倒计时器

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i<5;i++){
            int b = i;
            new Thread(() -> {
                System.out.println("i 已准备 = "+ b);
                countDownLatch.countDown();
            }).start();
        }
        // 等待装载完毕
        countDownLatch.await();
        System.out.println("结束");
    }
结果:
i 已准备 = 0
i 已准备 = 1
i 已准备 = 2
i 已准备 = 3
i 已准备 = 4

   为什么没有输出 "结束",是因为我们给 CountDownLatch 的任务为10个,但是循环只有5个任务,所以在 countDownLatch.await(); 会一直等待装载够才会继续执行,所以阻塞在那里。如果循环大小比 CountDownLatch 的任务大,则一旦装载够,则会立马继续执行。countDownLatch.countDown() 告诉CountDownLatch实例,已近准备好一个。

   public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i<12;i++){
            int b = i;
            new Thread(() -> {
                countDownLatch.countDown();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("i 已准备 = "+ b);
            }).start();
        }
        // 等待装载完毕
        countDownLatch.await();
        System.out.println("结束");
    }
结果:
结束
i 已准备 = 0
i 已准备 = 11
i 已准备 = 6
i 已准备 = 1
i 已准备 = 8
i 已准备 = 5
i 已准备 = 9
i 已准备 = 2
i 已准备 = 3
i 已准备 = 10
i 已准备 = 4
i 已准备 = 7

八、CyclicBarrier 循环栅栏

   这货比 CountDownLatch 牛逼一点的就是,我集齐 7 棵龙珠,许了愿,还可以再等集齐 7 棵龙珠,再许愿。只要我集齐 1 颗就必须等 7棵全部集齐,否则一直等待。但召唤神龙也是会有上限的,什么时候才能彻底结束呢?就是你在 CyclicBarrier 构造函数传入 7,一旦集齐 7 棵那就结束了。

    public static void main(String[] args) throws InterruptedException {
        int parties = 7;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(parties);
        for(int i = 0;i<8;i++){
            int b = i;
            if(b%parties ==0) {
                Thread.sleep(2000);
            }
            new Thread(() -> {
                System.out.println("已集齐 "+ (b%parties +1));
                try {
                    cyclicBarrier.await();
                    if(b%parties ==0) {
                        System.out.println("召唤神龙");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
结论是:
已集齐 1
已集齐 2
已集齐 3
已集齐 7
已集齐 5
已集齐 6
已集齐 4
召唤神龙
已集齐 1

   不要在乎以上结果的顺序,可以看到它已经集齐了7棵龙珠,召唤了神龙,但是召唤完了之后又在去集齐,这样就造成了等待,势必要再次集齐召唤神龙,且召唤了之后不再去集齐了,才能结束进程。

九、LockSupport 线程阻塞工具

   LockSupport 是一个非常方便实用线程阻塞工具,它可以在线程内任意位置让线程阻塞。和 Thread.suspend() 相比,它弥补了由于 resume() 在前发生,导致线程无法继续执行的情况。和Object.wait() 相比,它不需要先获得某个对象锁,也不会抛出 中断异常,中断异常可以在线程中获取 Thread.currentThread().isInterrupted() 来得知。

    public static void main(String[] args) throws InterruptedException {
        String a = "1";
        Thread[] threads = new Thread[2];
        for(int i=0;i<2;i++){
            int b = i;
            threads[i] = new Thread(() -> {
                try {
                    System.out.println("线程启动 " + b);
//                  提前使用解锁
                    LockSupport.unpark(Thread.currentThread());
                    LockSupport.park();
                    Thread.sleep(3000);
                    System.out.println("执行完毕 " + b);
                } catch (Exception e) {
                    System.out.println("老子被中断了 " + b);
                    // 这里必须在中断一次,否则
                    Thread.currentThread().interrupt();
                }
            });
        }
        threads[0].start();
//        LockSupport.unpark(threads[0]);
        threads[1].start();
    }
输出结果:
线程启动 0
执行完毕 0
线程启动 1
执行完毕 1

十、无锁

   对于并发控制而言,锁是一种悲观策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时访问临界区资源,就宁可牺牲让线程等待,所以说锁会阻塞线程执行。而无锁是一种乐观的策略,它会假设对资源的访问没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办?无锁的策略使用一种叫做比较交换的技术(CAS compare and Swap) 来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突位置。
   与锁相比,使用比较交换(CAS) 会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
   CAS 的算法过程是这样的:它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作时抱着乐观的态度进行,他总是认为自己可以完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并且成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

AtomicInteger 无锁的系统安全整数
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        for(int i=0;i<100;i++){
            new Thread(() -> {
                for(int j=0;j<100;j++){
                    if(atomicInteger.incrementAndGet() == 100){
                        System.out.println("卧槽");
                    }
                }
            }).start();
        }
        Thread.sleep(4000);
        System.out.println(atomicInteger.get());
    }
输出结果:
卧槽
10000
AtomicReference 无锁对象引用 和 AtomicStampedReference 带有时间戳的对象引用

   AtomicReference 和 AtomicInteger 非常类似,不同之处就在于 AtomicInteger 是对整数的封装,而 AtomicReference 则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

@Data
@Accessors(chain = true)
class Account{
    private Integer amount = 0;
}

public static void main(String[] args) throws InterruptedException {
    Account account = new Account();
    account.setAmount(10);
    AtomicReference<Account> accountAtomicReference = new AtomicReference<Account>();
    accountAtomicReference.set(account);
    // 模拟充值
    for(int i=0;i<3;i++){
        new Thread(() -> {
            Account clientAccount = accountAtomicReference.get();
            System.out.println("充值前查询越还有 "+ clientAccount.getAmount());
            if(clientAccount.getAmount() < 20){
                if(accountAtomicReference.compareAndSet(clientAccount,clientAccount.setAmount(clientAccount.getAmount() + 20) )){
                    System.out.println("余额小于20元,充值成功,余额:"+ clientAccount.getAmount());
                }
            }
        }).start();
    }
}
输出结果:
充值前查询越还有 10
充值前查询越还有 10
充值前查询越还有 10
余额小于20元,充值成功,余额:30

   以上列子可以看到,多线程间操作同一个实例对象,只会有一个成功。但这种模式存在一个 ABA 问题,就是,在线程操作前,这个值很可能被其他线程用去做其他的,导致值被使用后又换回来,当前线程一查看值没问题继续使用,造成数据被借用,我们还傻傻的不知道,这也是安全性问题。但这种情况就需要看我们的业务是否需要解决。
   解决办法呢就是使用 AtomicStampedReference 带有时间戳的对象引用,与其说时间戳,更像是一个修改标记,每次消费的时候,或者充值的时候我都给修改标记+1,一旦和我的原始标记不一样,我就不让其继续充值,只让其消费。

public static void main(String[] args) throws InterruptedException {
    Account account = new Account();
    account.setAmount(10);
    AtomicStampedReference<Account> accountAtomicReference = new AtomicStampedReference<Account>(account,0);
    // 模拟充值
    for(int i=0;i<3;i++){
        int stamp = accountAtomicReference.getStamp();
        new Thread(() -> {
            while (true) {
                Account clientAccount = accountAtomicReference.getReference();
                System.out.println("充值前查询余额还有 " + clientAccount.getAmount());
                if (clientAccount.getAmount() < 20) {
                    if (accountAtomicReference.compareAndSet(clientAccount, clientAccount.setAmount(clientAccount.getAmount() + 20),stamp,stamp+1)) {
                        System.out.println("余额小于20元,充值成功,余额:" + clientAccount.getAmount());
                    }
                }else {
                    System.out.println("当前用户 充值过不能再充值");
                    break;
                }
            }
        }).start();
    }
    // 模拟消费
    for(int i=0;i<3;i++){
        new Thread(() -> {
            while (true) {
                int stamp = accountAtomicReference.getStamp();
                Account clientAccount = accountAtomicReference.getReference();
                System.out.println("消费前查询余额还有 " + clientAccount.getAmount());
                if (clientAccount.getAmount() >= 10) {
                    if (accountAtomicReference.compareAndSet(clientAccount, clientAccount.setAmount(clientAccount.getAmount() - 10),stamp,stamp+1)) {
                        System.out.println("成功消费10元,余额还有:" + clientAccount.getAmount());
                    }
                }else{
                    System.out.println("余额不够");
                    break;
                }
            }
        }).start();
    }
}
输出结果:
充值前查询余额还有 10
余额小于20元,充值成功,余额:30
充值前查询余额还有 30
当前用户 充值过不能再充值
消费前查询余额还有 30
成功消费10元,余额还有:20
消费前查询余额还有 20
成功消费10元,余额还有:10
消费前查询余额还有 10
成功消费10元,余额还有:0
消费前查询余额还有 0
余额不够
消费前查询余额还有 0
余额不够
充值前查询余额还有 0
充值前查询余额还有 20
当前用户 充值过不能再充值
消费前查询余额还有 20
成功消费10元,余额还有:10
消费前查询余额还有 10
成功消费10元,余额还有:0
消费前查询余额还有 0
余额不够
充值前查询余额还有 10
充值前查询余额还有 20
当前用户 充值过不能再充值

   如果说在充值的时候加一个条件,让其只能充值1次,如果我们用 AtomicReference 是完全做不到的,因为他不会记录,需要我们自己去添加一个全局变量去维护,但使用 AtomicStampedReference 就可以做到,因为它本身就维护了一个标记,而且还帮我们解决了 ABA 问题,如果说值被其他线程冒用,标记就会+1,使得和当前线程的标记不一样,则保留值退出。

   出了AtomicInteger 和 AtomicReference 还有 AtomicReferenceArray AtomicIntegerArray等,具体的API都是差不多的。

本文大部分内容均来自 《Java高并发程序设计》--葛一鸣,郭超

上一篇下一篇

猜你喜欢

热点阅读