Java核心---线程进阶

3 篇文章 0 订阅

乐观锁 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() +
                            " 到达终点");
                    // 将计数器 -1
                    latch.countDown();
                }
            }).start();
        }
        // 阻塞等待
        latch.await();
        System.out.println("所有人都到达终点了,公布排名");
    }

//执行结果
Thread-0 开始起跑
Thread-4 开始起跑
Thread-2 开始起跑
Thread-3 开始起跑
Thread-1 开始起跑
Thread-0 到达终点
Thread-1 到达终点
Thread-2 到达终点
Thread-3 到达终点
Thread-4 到达终点
所有人都到达终点了,公布排名

Process finished with exit code 0

CountDownLauth 缺点:
计数器的使用是一次性的,用完一次之后就不能再用了。

循环屏障

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("执行了Runnable");
            }
        });

        for (int i = 1; i < 11; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开始起跑");
                    try {
                        Thread.sleep(200 * finalI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        //计数器-1,判断计数器是否为 0
                        System.out.println(Thread.currentThread().getName() + "等待人");
                        barrier.await();//计数器-1,判断计数器是否为0,为0执行代码,不为0阻塞等待,当为0时,首先会执行await之后的代码,将计数器重置。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    //已经有一组比赛结束了
                    System.out.println(Thread.currentThread().getName() + "执行结束");
                }
            }).start();
        }
    }

//执行结果
Thread-0开始起跑
Thread-7开始起跑
Thread-4开始起跑
Thread-5开始起跑
Thread-8开始起跑
Thread-1开始起跑
Thread-2开始起跑
Thread-3开始起跑
Thread-9开始起跑
Thread-6开始起跑
Thread-0等待人
Thread-1等待人
Thread-2等待人
Thread-3等待人
Thread-4等待人
执行了Runnable
Thread-4执行结束
Thread-0执行结束
Thread-1执行结束
Thread-2执行结束
Thread-3执行结束
Thread-5等待人
Thread-6等待人
Thread-7等待人
Thread-8等待人
Thread-9等待人
执行了Runnable
Thread-9执行结束
Thread-5执行结束
Thread-6执行结束
Thread-8执行结束
Thread-7执行结束

CyclieBarrier 和 CountDownLatch 区别

  • CountDownLatch 计数器只能使用一次,CyclieBarrier 计数器可以循环使用。

HashMap 底层实现结构、哈希冲突的解决…线程问题

JDK 1.8 锁优化 读的时候不加锁,写的时候加锁。使用了大量的CAS、Voiltail。

HashMap、Hashtable、 ConcurrentHashMap 区别:

  1. HashMap 是非线程安全的容器,它在 jDK 1.7 会造成死循环,JDK 1.8 会造成数据覆盖;Hashtable 与ConcurrentHashMap 都是线程安全的。
  2. Hashtable 实现线程安全的手段比较简单,它是再 put 方法整体加了一把锁,使用 synchronized 修饰,性能不高,使用频率低;ConcurrentHashMap 是HashMap 在多线程下的替代方案,它在JDK1.7的时候使用的lock 加分段锁的方案来实现线程安全问题的保障,而在1.8 的时候使用大量的CAS、volatile 来实现线程的,并且在 JDK 1.8 的时候读取的时候不加锁(读取的数据可能不是最新,读取和写入可以同时进行),只有在写的时候才加锁。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值