Java核心---线程进阶

Posted 一位懒得写博客的小学生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心---线程进阶相关的知识,希望对你有一定的参考价值。

乐观锁 VS 悲观锁

  • 乐观锁:(CAS(比较并且交换)、ABA、JUC) 乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发生冲突了,则让返回用户错误的信息,让用户决定如何去做。
  • 悲观锁:(synchronized) 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 悲观锁的问题: 总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高;
  • 乐观锁的问题: 并不总是能处理所有问题,所以会引入一定的系统复杂度。

什么是CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作

  • 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
    1. 比较 A 与 V 是否相等。(比较)
    1. 如果比较相等,将 B 写入 V。(交换)
    1. 返回操作是否成功

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性

实现

    private static AtomicInteger count = new AtomicInteger(0);
    private static final int MAXSIZE = 100000;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    count.getAndIncrement();
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    count.getAndDecrement();
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);

CAS的缺点:
ABA问题 -> AtomicInteger 是存在ABA问题
ABA 统一解决方案:增加版本号,每次修改之后更新版本号。AtomicStampedReference()

ABA问题

    private static AtomicReference money = new AtomicReference(100);

    public static void main(String[] args) throws InterruptedException {
        Thread m1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转出100元
                boolean res = money.compareAndSet(100,0);
                System.out.println("第一次" + res);
            }
        });
        m1.start();
        m1.join();

        Thread m3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转入100元
                boolean res = money.compareAndSet(0,100);
                System.out.println("第三次" + res);
            }
        });
        m3.start();
        m3.join();
        Thread m2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转出100元
                boolean res = money.compareAndSet(100,0);
                System.out.println("第二次" + res);
            }
        });
        m2.start();


    }

//执行结果
第一次true
第三次true
第二次true

ABA问题解决

    //解决ABA问题
    private static AtomicStampedReference money = new AtomicStampedReference(100,0);

    public static void main(String[] args) throws InterruptedException {
        Thread m1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转出100元
                boolean res = money.compareAndSet(100,0,0,1);
                System.out.println("第一次" + res);
            }
        });
        m1.start();
        m1.join();

        Thread m3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转入100元
                boolean res = money.compareAndSet(0,100,1,2);
                System.out.println("第三次" + res);
            }
        });
        m3.start();
        m3.join();
        Thread m2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转出100元
                boolean res = money.compareAndSet(100,0,0,1);
                System.out.println("第二次" + res);
            }
        });
        m2.start();


    }

//执行结果
第一次true
第三次true
第二次false

共享锁/非共享锁

共享锁:一把锁可以被多个线程拥有;读写锁中的读锁就是共享锁。
非共享锁:一把锁只能被一个线程拥有。(synchronized)

读写锁

就是将一把锁分为2个,一个用于读数据的锁,另一把锁叫做写锁。读锁是可以被多个线程同时拥有的,而写锁只能被一个线程拥有。

读写锁的具体实现ReentrantReadWriteLock
读写锁的优势: 锁的粒度更加地小,性能也更高。
注意事项: 读写锁中的读锁和写锁是互斥的。(防止同时读写所产生的脏数据)。

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
        executor.execute(new Runnable() {
            @Override
            public void run() {
                //加锁
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " " + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }
            }
        });

        executor.execute(new Runnable() {
            @Override
            public void run() {
                //加锁
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " " + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }
            }
        });

        executor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try{
                    System.out.println(Thread.currentThread().getName() + " " + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

        executor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try{
                    System.out.println(Thread.currentThread().getName() + " " + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

    }

//执行结果
pool-1-thread-2 Fri May 28 16:10:41 CST 2021
pool-1-thread-1 Fri May 28 16:10:41 CST 2021
pool-1-thread-3 Fri May 28 16:10:44 CST 2021
pool-1-thread-4 Fri May 28 16:10:47 CST 2021

公平锁/非公平锁

公平锁(new ReentrantLock(true)): 锁的获取顺序必须和线程的先后顺序保持一致。
优点:性能比较高
非公平锁(new ReentrantLock()/new ReentrantLock(false)/synchronized):锁的获取顺序和线程获取顺序的前后顺序无关(默认锁策略);
优点:执行时有序的,结果也是可以预期的。

自旋锁

按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。

//只要没抢到锁,就死等
while (抢锁(lock) == 失败) {}

缺点:如果发生死锁则会一直自旋(循环),所以会带来一定的额外开销。

可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

    private static Object lock = new Object();
    public static void main(String[] args) {
        //第一次进入锁
        synchronized (lock) {
            System.out.println("第一次进入锁");
            synchronized (lock) {
                System.out.println("第二次进入锁");
            }
        }
    }

//执行结果
第一次进入锁
第二次进入锁

怎么理解乐观锁和悲观锁的

  1. 乐观锁 -> CAS -> Atomic*,CAS 是由V(内存值)A(预期旧值)B(新值)组成,然后执行的时候是使用 V == A对比,如果结果为 true 则表明没有发生冲突,则可以直接修改,否则不能修改。CAS 是通过调用C++ 实现提供的 Unsafe 中的本地方法(CompareAndSwapXXX)来实现的,C++是通过操作系统中 Atomic::cmpxchg(原子指令)来实现的。
  2. 悲观锁 -> synchronized 在java 中将锁的 ID 存放在对象头来实现的,synchronized 在JVM 层面是通过监视器锁来实现的,synchronized 在操作系统层面是通过互斥锁 mutex 实现。

synchronized (独占锁)锁优化(锁消除)

JDK 1.6 锁升级的过程

  • 无锁
  • 偏向锁(第一个线程第一次访问)将线程ID存储在对象头中的偏向标识。
  • 轻量级锁(自旋)
  • 重量级锁

java.util.concurrent 包下的常见类

  1. Reentrantlock
    lock 写在 try 之前
    一定要记得在 final 里面进行 unlock

信号量

    public static void main(String[] args) {
        //信号量
        Semaphore semaphore = new Semaphore(2);

        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));

        for (int i = 0; i < 4; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "到达停车场");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //试图获取锁
                    try {
                        semaphore.acquire();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //获取到锁了
                    System.out.println(Thread.currentThread().getName() + "进入停车场");
                    int num = 1 + new Random().nextInt(5);
                    try {
                        Thread.sleep(num * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "离开停车场");

                    //释放锁
                    semaphore.release();
                }
            });
        };

    }

//执行结果
pool-1-thread-2到达停车场
pool-1-thread-4到达停车场
pool-1-thread-3到达停车场
pool-1-thread-1到达停车场
pool-1-thread-4进入停车场
pool-1-thread-1进入停车场
pool-1-thread-4离开停车场
pool-1-thread-3进入停车场
pool-1-thread-1离开停车场
pool-1-thread-2进入停车场
pool-1-thread-2离开停车场
pool-1-thread-3离开停车场

计数器 CountDownLauth

CountDownLauth 是如何实现的?

在 CountDownLauth 里面有一个计数器,每次调用 countdown 方法时计数器的数量-1;直到减到0的时候就可以执行 await()后边的代码。

    public static void main(String[] args) throws InterruptedException {
        //计数器
        CountDownLatch latch = new CountDownLatch(5);
        for (int i = 1; i < 6; i++) {
            final int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() +
                            " 开始起跑");
                    try {
                        Thread.sleep(finalI * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() +
                            " 到达终点");
         

以上是关于Java核心---线程进阶的主要内容,如果未能解决你的问题,请参考以下文章

Java核心---线程进阶

Java核心---线程进阶

Java核心---线程进阶

java多线程进阶线程池

java多线程进阶线程池

Java进阶 线程安全