zoukankan      html  css  js  c++  java
  • 谈谈 Java 中的那些“琐”事

    一、公平锁&非公平锁

    是什么

    • 公平锁:线程按照申请锁的顺序来获取锁;在并发环境中,每个线程都会被加到等待队列中,按照 FIFO 的顺序获取锁。

    • 非公平锁:线程不按照申请锁的顺序来获取锁;一上来就尝试占有锁,如果占有失败,则按照公平锁的方式等待。

    通俗来讲,公平锁就相当于现实中的排队,先来后到;非公平锁就是无秩序,谁抢到是谁的;

    优缺点

    公平锁

    • 优:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。
    • 缺:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒线程的开销比非公平锁要大。

    非公平锁

    • 优:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。
    • 缺:在高并发环境下可能造成线程优先级反转和饿死现象。

    Java中的公平&非公平锁

    在 Java 中,synchronized 是典型的非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁,可以在初始化的时候指定。

    查看 ReentrantLock 的源码会发现,初始化时可以传入 true 或 false,来得到公平或非公平锁。

    //源码
    //默认为非公平
    public ReentrantLock() {
    	sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
    	sync = fair ? new FairSync() : new NonfairSync();
    }
    
    public class FairLockDemo {
        public static void main(String[] args) {
            //公平锁
            Lock fairLock = new ReentrantLock(true);
            //非公平锁
            Lock unFairLock = new ReentrantLock(false);
        }
    }
    

    二、可重入锁

    是什么

    可重入锁也叫递归锁,是指线程可以进入任何一个它已经拥有的锁所同步的代码块。通俗来讲,就好比你打开了你家的大门,就可以随意的进入客厅、厨房、卫生间......

    如下图,线程 M1 和 M2 是被同一把锁同步的方法,M1 中调用了 M2,那么线程 A 访问 M1 时,再访问 M2 就不需要重新获取锁了。

    优缺点

    • 优:可以一定程度上避免死锁
    • 缺:暂时不知道

    Java中的可重入锁

    synchronized和ReentrantLock都是典型的可重入锁

    synchronized

    public class ReentrantDemo1 {
        public static void main(String[] args) {
            Phone phone = new Phone();
    
            new Thread(() -> {
                phone.sendSMS();
            }).start();
    
            new Thread(() -> {
                phone.sendSMS();
            }).start();
        }
    }
    class Phone {
        public synchronized void sendSMS() {
            System.out.println(Thread.currentThread().getId() + ":sendSMS()");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sendEmail();
        }
    
        public synchronized void sendEmail() {
            System.out.println(Thread.currentThread().getId() + ":sendEmail()");
        }
    }
    

    ReentrantLock

    public class ReentrantDemo2 {
        public static void main(String[] args) {
            User user = new User();
    
            new Thread(() -> {
                user.getName();
            }).start();
    
            new Thread(() -> {
                user.getName();
            }).start();
        }
    }
    
    class User {
        Lock lock = new ReentrantLock();
    
        public void getName() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getId() + ":getName()");
                TimeUnit.SECONDS.sleep(1);
                getAge();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void getAge() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getId() + ":getAge()");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    八锁问题

    点击查看我之前的博客 多线程之8锁问题,搞懂八锁问题,可以更深刻的理解 synchronized 锁的范围

    实现一个不可重入锁

    public class UnReentrantLockDemo {
    
        private AtomicReference<Thread> atomicReference = new AtomicReference<>();
    
        public void lock() {
            Thread current = Thread.currentThread();
            //自旋
            while(!atomicReference.compareAndSet(null, current)) {
    
            }
        }
    
        public void unlock() {
            Thread current = Thread.currentThread();
            atomicReference.compareAndSet(current, null);
        }
    }
    

    三、自旋锁

    是什么

    尝试获取锁的线程不会立即阻塞,而是以循环的方式不断尝试获取锁

    优缺点

    • 优:减少线程上下文切换的消耗
    • 缺:循环消耗CPU

    Java中的自旋锁

    CAS:CompareAndSwap,比较并交换,它是一种乐观锁。

    CAS 中有三个参数:内存值V、旧的预期值A、要修改的新值B;只有当预期值A与内存值V相等时,才会将内存值V修改为新值B,否则什么都不做

    public class CASTest {
        public static void main(String[] args) {
            AtomicInteger a1 = new AtomicInteger(1);
            //V=1, A=1, B=2
            //V=A,所以修改成功,此时V=2
            System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
            //V=2, A=1, B=2
            //V!=A,修改失败,返回false
            System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
        }
    }
    

    源码解析:以 AtomicInteger 中的 getAndIncrement() 方法为例

    //获取并增加,相当于i++操作
    public final int getAndIncrement() {
    	return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    //调用UnSafe类中的getAndAddInt()方法
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获取当前内存值
            var5 = this.getIntVolatile(var1, var2);
            //循环比较内存值和预期值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    

    CAS 也存在一些问题:

    • 如果一直交换不成功,会一直循环,开销大
    • 只能保证一个共享变量的原子操作
    • ABA 问题:即 A 被修改为 B,又被改为 A,虽然值没发生变化,但这种操作还是存在一定风险的

    可以通过加时间戳或版本号的方式解决 ABA 问题:

    public class ABATest {
        public static void main(String[] args) {
            showABA();
        }
    
        /**
         * 重现ABA问题
         */
        private static void showABA() {
            AtomicReference<String> atomicReference = new AtomicReference<>("A");
            //线程X,模拟ABA问题
            new Thread(() -> {
                atomicReference.compareAndSet("A", "B");
                atomicReference.compareAndSet("B", "A");
            }, "线程X").start();
    
            //线程Y睡眠一会儿,等待X执行完
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicReference.compareAndSet("A", "C");
                System.out.println("最终结果:" + atomicReference.get());
            }, "线程Y").start();
        }
        
        /**
         * 解决ABA问题
         */
        private static void solveABA() {
            //初始版本号为1
            AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);
    
            new Thread(() -> {
                asr.compareAndSet("A", "B", 1, 2);
                asr.compareAndSet("B", "A", 2, 3);
            }, "线程X").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                asr.compareAndSet("A", "C", 1, 2);
                System.out.println(asr.getReference() + ":" + asr.getStamp());
            }, "线程Y").start();
        }
    }
    

    动手实现一个自旋锁

    public class SpinLockDemo {
        /**
         * 初始值为 null
         */
        AtomicReference<Thread> atomicReference = new AtomicReference<>(null);
    
        public static void main(String[] args) {
            SpinLockDemo spinLockDemo = new SpinLockDemo();
    
            new Thread(() -> {
                spinLockDemo.lock();
                spinLockDemo.unLock();
            }, "线程A").start();
    
            new Thread(() -> {
                spinLockDemo.lock();
                spinLockDemo.unLock();
            }, "线程B").start();
        }
    
        public void lock() {
            //获取当前线程对象
            Thread thread = Thread.currentThread();
            do {
                System.out.println(thread.getName() + "尝试获取锁...");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //当赋值成功才会跳出循环
            } while (!atomicReference.compareAndSet(null, thread));
        }
    
        public void unLock() {
            //获取当前线程对象
            Thread thread = Thread.currentThread();
            //置为null,相当于释放锁
            atomicReference.compareAndSet(thread, null);
            System.out.println(thread.getName() + "释放锁...");
        }
    }
    

    四、共享锁&独占锁

    是什么

    • 共享锁:也可称为读锁,可被多个线程持有
    • 独占锁:也可称为写锁,只能被一个线程持有,synchronized和ReentrantLock都是独占锁
    • 互斥:读读共享、读写互斥、写写互斥

    优缺点

    读写分离,适用于大量读、少量写的场景,效率高

    java中的共享锁&独占锁

    ReentrantReadWriteLock 中的读锁是共享锁、写锁是独占锁

    class MyCache {
        private volatile Map<String, Object> map = new HashMap<>();
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
        /**
         * 写锁控制写入
         */
        public void put(String key, Object value) {
            lock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "开始写入...");
                //睡一会儿
                TimeUnit.SECONDS.sleep(1);
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "写入完成...");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
            }
        }
        
        /**
         * 读锁控制读取
         */
        public Object get(String key) {
            lock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "开始读取...");
                //睡一会儿
                TimeUnit.SECONDS.sleep(1);
                Object value = map.get(key);
                System.out.println(Thread.currentThread().getName() + "读取结束...value=" + value);
                return value;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
            }
            return null;
        }
    
        public void clear() {
            map.clear();
        }
    }
    
    public class ReentrantReadWriteLockDemo {
        public static void main(String[] args) {
            MyCache cache = new MyCache();
            for (int i = 1; i <= 5; i++) {
                int finalI = i;
                new Thread(() -> {
                    cache.put(String.valueOf(finalI), String.valueOf(finalI));
                    cache.get(String.valueOf(finalI));
                }, "线程" + i).start();
            }
            cache.clear();
        }
    }
    
  • 相关阅读:
    IE6兼容问题
    清除浮动的几种方法
    CSS的继承性和层叠性
    CSS基础选择器复习
    梦与醒,进与退
    CSS的入门概念
    HTML的入门概念
    弹性布局整理
    点击评论加入输入框(笔记)
    css命名(笔记)
  • 原文地址:https://www.cnblogs.com/songjilong/p/13710455.html
Copyright © 2011-2022 走看看