• JUC(三):JUC包下锁概念


    线程不安全集合类

    ArrayList

    List是线程不安全的集合类,底层是Object数组实现,初始化容量是10(其实是一个空数组,第一次扩容时,将数组扩容为10),其后每次扩容大小为当前容量的一半(oldCapacity >> 1)。

    初始化

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    

    扩容

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    

    初次扩容,将底层数组容量设为10。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    动态扩容,是将底层数组容量扩容当前容量的一半(oldCapacity >> 1)。

    线程不安全示例

    package com.chinda.juc.coll;
    
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.util.IdUtil;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * ArrayList线程不安全
     * @author Wang Chinda
     * @date 2020/5/10
     * @see
     * @since 1.0
     */
    public class ListUnsafe {
    
        public static void main(String[] args) {
            List<String> list = CollUtil.newArrayList();
            for (int i = 1; i <= 3; i++) {
                new Thread(() -> {
                    list.add(IdUtil.simpleUUID());
                    System.out.println(list);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    本示例依赖包

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-core</artifactId>
        <version>5.2.3</version>
    </dependency>
    

    控制台输出

    [0b867a48ef73409885294f6e1e643ce3]
    [0b867a48ef73409885294f6e1e643ce3]
    [0b867a48ef73409885294f6e1e643ce3]
    

    循环30次控制台输出异常

    java.util.ConcurrentModificationException
    

    导致原因

    因是线程并发写入数据,当线程A正在写数据时,线程执行一半时,线程B抢到资源,开始执行。这就会导致线程A写入数据不正确。

    比如现实中的花名册

    单线程执行解释:班长将会依次的将全班所有的同学都写入花名册。

    多线程执行解释:全班同学自己签自己名字,可能会出现李四刚写一个李字时,花名册被张三抢去接着写的情况。假如班长看着,必须一个人写完名字,才允许第二个人写名字,依次往复。这就是锁的概念。

    解决不安全

    第一种方案

    添加元素方法中添加synchronized。Java已实现,Vector类。

    第二种方案

    集合操作工具类。

    List<String> list = Collections.synchronizedList(CollUtil.newArrayList());
    

    第三种方案

    推荐使用此种方案。

    List<String> list = new CopyOnWriteArrayList<>();
    
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    

    附带,HashMap初始化容量是16, 每次扩容是当前容量*2。HashSet底层数据结构就是HashMap,为什么HashMap是put存放数据而HashSet是add,是因为HashSet存放将add的元素存放为HashMap的key中,value一直是一个Object对象。

    锁概念

    公平锁和非公平锁

    java.util.concurrent.locks包中ReentrantLock的创建可以指定构造函数的boolean类型指定是公平锁还是非公平锁,默认是非公平锁。非公平锁的有点在于吞吐量比公平锁大。synchronized也是一种非公平锁。

    公平锁

    多个线程按照申请锁的顺序来获取锁,遵循先申请到先得原则。

    非公平锁

    多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请锁的线程比先申请锁的线程优先获取锁,在高并发的情况下,有可能造成优先级反转(后申请锁的线程总是先得到锁)或者饥饿现象(先申请锁的线程一直没有获取到锁)。

    可重入锁(递归锁)

    同一线程外出函数获得锁之后,内存递归函数仍然能获取该锁得代码,同一线程在外层方法获取锁的时候,在进入内层方法时会自动获取锁,即线程可以进入任何一个它已经拥有的锁同步的代码块。可重入锁最大的作用时避免死锁。

    可重入锁示例

    package com.chinda.juc.coll;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/10
     * @see
     * @since 1.0
     */
    public class ReenterLock {
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(() -> {
                phone.sendSMS();
            }, "t1").start();
            new Thread(() -> {
                phone.sendSMS();
            }, "t2").start();
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println();
            System.out.println();
            System.out.println();
            System.out.println();
    
            Thread t3 = new Thread(phone, "t3");
            Thread t4 = new Thread(phone, "t4");
            t3.start();
            t4.start();
        }
    }
    
    class Phone implements Runnable {
        public synchronized void sendSMS() {
            System.out.println(Thread.currentThread().getName() + "	 invoked sendSMS()");
            sendEmail();
        }
    
        public synchronized void sendEmail() {
            System.out.println(Thread.currentThread().getName() + "	 *****invoked sendEmail()");
        }
    
        Lock lock = new ReentrantLock();
        @Override
        public void run() {
            get();
        }
    
        private void get() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "	 invoked get()");
                set();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        private void set() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "	 invoked set()");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    控制台打印

    t1	 invoked sendSMS()
    t1	 *****invoked sendEmail()
    t2	 invoked sendSMS()
    t2	 *****invoked sendEmail()
    
    
    
    
    t4	 invoked get()
    t4	 invoked set()
    t3	 invoked get()
    t3	 invoked set()
    

    线程执行时,进入到嵌套方法时,不需要获取锁,可直接进入。线程执行嵌套方法时,没有被其余线程加塞。

    注意:若加锁与解锁个数相匹配,编译不会失败,执行不会阻塞;若加锁比解锁多,线程会进入阻塞状态;若解锁比加锁多执行会抛出IllegalMonitorStateException异常

    自旋锁

    尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU资源。

    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }
    

    自旋锁示例

    package com.chinda.juc.coll;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * 优点: 循环比较获取直到成功为止, 没有类似wait的阻塞
     * <p>
     * 通过CAS操作完成自旋锁, A线程先进来掉哦那个myLock方法自己持有锁5秒, B随后进来后发现当前线程持有锁, 不是null, 自选等待, 直到A释放锁后B才会抢到。
     *
     * @author Wang Chinda
     * @date 2020/5/10
     * @see
     * @since 1.0
     */
    public class SpinDemo {
        AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
    
        public void myLock() {
            Thread thread = Thread.currentThread();
            System.out.println(Thread.currentThread().getName() + "	 com in (*^_^*)");
            while (!atomicReference.compareAndSet(null, thread)) {
    
            }
        }
    
        public void myUnlock() {
            Thread thread = Thread.currentThread();
            atomicReference.compareAndSet(thread, null);
            System.out.println(Thread.currentThread().getName() + "	 invoked myUnlock()");
        }
    
    
    
        public static void main(String[] args) {
            SpinDemo spinDemo = new SpinDemo();
            new Thread(() -> {
                spinDemo.myLock();
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                spinDemo.myUnlock();
            }, "A").start();
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(() -> {
                spinDemo.myLock();
                spinDemo.myUnlock();
            }, "B").start();
        }
    }
    

    控制台打印

    A	 com in (*^_^*)
    B	 com in (*^_^*)
    A	 invoked myUnlock()
    B	 invoked myUnlock()
    

    线程A执行锁时,因为没有任何人获取锁,所以锁为null。线程B获取锁时,锁已经被线程A占用,线程B循环循环获取锁,直到线程A释放锁为止。

    独占锁(写锁)/共享锁(读锁)/互斥锁

    独占锁

    一次只能被一个线程所持有。ReetrantLock和synchronized都是独占锁。

    共享锁

    可以被多个线程所持有。

    ReentrantReadWriteLock其读锁时共享锁,写时独占锁。读锁的共享锁可保证高并发读时非常高效的。读写、写读、写写的过程时互斥的。

    读写锁

    线程不安全示例

    package com.chinda.juc.coll;
    
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.thread.ThreadUtil;
    
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 多个线程同时读取一个资源类没有任何问题, 所以为了满足并发量, 读取共享资源应该可以同时进行。
     * 但是如果有一个线程去写共享资源,就不应该再有其他线程可以对该资源进行读或者写。
     * 即:
     *    读-读 能共存
     *    读-写 不能共存
     *    写-写 不能共存
     *    写操作: 原子+独占, 整个过程必须时完成的统一体,中间不允许被分割, 被打断。
     * @author Wang Chinda
     * @date 2020/5/11
     * @see
     * @since 1.0
     */
    public class ReadWriteLockDemo {
        public static void main(String[] args) {
            MyCache myCache = new MyCache();
            for (int i = 1; i <= 10; i++) {
                String finalI = i + "";
                new Thread(() -> {
                    myCache.put(finalI, finalI);
                }, String.valueOf(i)).start();
            }
    
            for (int i = 1; i <= 10; i++) {
                String finalI = i + "";
                new Thread(() -> {
                    myCache.get(finalI);
                }, String.valueOf(i)).start();
            }
        }
    }
    
    class MyCache {
        private volatile Map<String, Object> map = CollUtil.newHashMap();
    
        public void put(String key, Object value) {
            System.out.println(Thread.currentThread().getName() + "	 正在写入: " + key);
            ThreadUtil.sleep(300, TimeUnit.MILLISECONDS);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "	 写入完成");
        }
        public void get(String key) {
            System.out.println(Thread.currentThread().getName() + "	 正在读取: " + key);
            ThreadUtil.sleep(300, TimeUnit.MILLISECONDS);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "	 读取完成:" + result);
        }
    }
    

    线程安全示例

    package com.chinda.juc.coll;
    
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.thread.ThreadUtil;
    
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 多个线程同时读取一个资源类没有任何问题, 所以为了满足并发量, 读取共享资源应该可以同时进行。
     * 但是如果有一个线程去写共享资源,就不应该再有其他线程可以对该资源进行读或者写。
     * 即:
     *    读-读 能共存
     *    读-写 不能共存
     *    写-写 不能共存
     *    写操作: 原子+独占, 整个过程必须时完成的统一体,中间不允许被分割, 被打断。
     * @author Wang Chinda
     * @date 2020/5/11
     * @see
     * @since 1.0
     */
    public class ReadWriteLockDemo {
        public static void main(String[] args) {
            MyCache myCache = new MyCache();
            for (int i = 1; i <= 10; i++) {
                String finalI = i + "";
                new Thread(() -> {
                    myCache.put(finalI, finalI);
                }, String.valueOf(i)).start();
            }
    
            for (int i = 1; i <= 10; i++) {
                String finalI = i + "";
                new Thread(() -> {
                    myCache.get(finalI);
                }, String.valueOf(i)).start();
            }
        }
    }
    
    class MyCache {
        private volatile Map<String, Object> map = CollUtil.newHashMap();
        private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
        public void put(String key, Object value) {
            rwLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "	 正在写入: " + key);
                ThreadUtil.sleep(300, TimeUnit.MILLISECONDS);
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "	 写入完成");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rwLock.writeLock().unlock();
            }
        }
        public void get(String key) {
            rwLock.readLock();
            try {
                System.out.println(Thread.currentThread().getName() + "	 正在读取: " + key);
                ThreadUtil.sleep(300, TimeUnit.MILLISECONDS);
                Object result = map.get(key);
                System.out.println(Thread.currentThread().getName() + "	 读取完成:" + result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rwLock.readLock().unlock();
            }
        }
    }
    

    CountDownLatch

    CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复。

    示例

    实现效果:图书馆有6个人,等6个人离开图书馆,门卫大爷锁门。

    反面示例

    package com.chinda.juc.coll;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/11
     * @see
     * @since 1.0
     */
    public class CountDownLatchDemo {
    
        public static void main(String[] args) {
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "	 离开图书馆");
                }, String.valueOf(i)).start();
            }
            System.out.println(Thread.currentThread().getName() + "	 ************* 门卫大爷锁门!");
        }
    }
    

    可能会出现人没有全部离开,门就被锁。

    CountDownLatch示例

    package com.chinda.juc.coll;
    
    import lombok.SneakyThrows;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/11
     * @see
     * @since 1.0
     */
    public class CountDownLatchDemo {
    
        @SneakyThrows
        public static void main(String[] args) {
            CountDownLatch downLatch = new CountDownLatch(6);
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "	 离开图书馆");
                    downLatch.countDown();
                }, String.valueOf(i)).start();
            }
            downLatch.await();
            System.out.println(Thread.currentThread().getName() + "	 ************* 门卫大爷锁门!");
        }
    }
    

    天下一统

    大秦帝国统一天下,前提灭六国,六国被灭顺序:韩国,赵国,魏国,楚国,燕国,齐国。

    示例

    package com.chinda.juc.coll;
    
    import lombok.Getter;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/12
     * @see
     * @since 1.0
     */
    public enum SengokuEnum {
        ONE(1, "韩国"),
        TWO(2, "赵国"),
        THREE(3, "魏国"),
        FOUR(4, "楚国"),
        FIVE(5, "燕国"),
        SIX(6, "齐国");
        @Getter
        private Integer code;
        @Getter
        private String name;
    
        SengokuEnum(Integer code, String name) {
            this.code = code;
            this.name = name;
        }
    
        public static SengokuEnum eachSengKu(int index) {
            SengokuEnum[] sengokus = SengokuEnum.values();
            for (SengokuEnum sengoku : sengokus) {
                if (index == sengoku.getCode()) {
                    return sengoku;
                }
            }
            return null;
        }
    }
    
    package com.chinda.juc.coll;
    
    import lombok.SneakyThrows;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/12
     * @see
     * @since 1.0
     */
    public class ChinDemo {
    
        @SneakyThrows
        public static void main(String[] args) {
            CountDownLatch downLatch = new CountDownLatch(6);
    
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "被灭。");
                    downLatch.countDown();
                }, SengokuEnum.eachSengKu(i).getName()).start();
            }
            downLatch.await();
            System.out.println("天下一统, 秦国统一天下。");
        }
    }
    

    CyclicBarrier

    集齐7颗龙珠,召唤神龙。

    示例

    package com.chinda.juc.coll;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/12
     * @see
     * @since 1.0
     */
    public class CyclicBarrierDemo {
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
                System.out.println("***********召唤神龙***********");
            });
    
            for (int i = 1; i <= 7; i++) {
                int finalI = i;
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "	 收集到第: " + finalI + "龙珠。");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }, String.valueOf(i)).start();
            }
        }
    }
    

    Semaphore

    信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

    示例

    实现效果:6辆车争抢3个车位,只允许一辆车开走,第二辆才允许进入车位。

    package com.chinda.juc.coll;
    
    import cn.hutool.core.thread.ThreadUtil;
    
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author Wang Chinda
     * @date 2020/5/12
     * @see
     * @since 1.0
     */
    public class SemaphoreDemo {
    
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3);
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "	抢到车位");
                        ThreadUtil.sleep(3, TimeUnit.SECONDS);
                        System.out.println(Thread.currentThread().getName() + "	离开车位");
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                }, String.valueOf(i)).start();
            }
        }
    }
    
  • 相关阅读:
    C# 反射
    WinForm资源管理器开发(TreeView&ListView)
    枚举——完美立方
    关于考证
    人工智能时代,程序员要不要精通算法?
    程序员需要掌握哪些软技能?
    Android数据解析——JSON
    C# 数组转json
    jQuery跨域调用Web API
    oracle 创建表同时添加注释
  • 原文地址:https://www.cnblogs.com/chinda/p/12866826.html
Copyright © 2020-2023  润新知