2019尚硅谷互联网大厂高频重点面试题(第2季)3:安全的List Set HashMap,公平 非公平,可重入锁,自旋锁,统计递减,栅栏递增,信号量,阻塞队列,生产者消费者

4. ArrayList 线程不安全

初值 扩容 不安全

        //底层是数组
        //Constructs an empty list with an initial capacity of ten. 初始值为10
        //transient Object[] elementData;

        //首次添加的时候:
        /*if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //10 和 1 取一个最大的。肯定就就是10了。
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }*/

        //10+10>>1。10为 1010,右移1位为:101,4+1=5
        //int newCapacity = oldCapacity + (oldCapacity >> 1); 扩容每次扩容1.5倍

        //为什么不安全,写的时候,没有加 锁
        /*public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }*/

        ArrayList<Integer> list = new ArrayList<>();

List不安全 1,2种解决

        //ArrayList  implements List<E>,RandomAccess
        //源码为下面:所以返回:SynchronizedRandomAccessList
        //        return (list instanceof RandomAccess ?
        //                new SynchronizedRandomAccessList<>(list) :
        //                new SynchronizedList<>(list));

        List<String> list = Collections.synchronizedList(new ArrayList<>());
        //打印
        //list.forEach(System.out::println);

        //如果改为30报错:java.util.Concurrent Modification Exception 并发修改异常
        //1. 使用:Vector<>() 类。
        //2. 使用 Collections.synchronizedList(new ArrayList<>());

        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                String str = UUID.randomUUID().toString().substring(0, 8);
                list.add(str);
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

        /*[null, 896a7dc9, 0d9055ad]
        [null, 896a7dc9, 0d9055ad]
        [null, 896a7dc9, 0d9055ad]*/
问题的根源
  • Concurrent Modification Exception

    • 比如 张三写的时候(刚写了张),李四去抢,造成张三写混乱。
      • *并发争抢修改导致,参考我们的花名册签名情况。
        *一个人正在写入,另外一个同学过来抢夺,导致数据不一致异常。并发修改异常。|
    if (expectedModCount != ArrayList.this.modCount) //toString的时候,会调用 hashNext。
    //希望的修改长度 和 真实的长度不一致。
     //如果不立刻打印输出,果然没问题。
    
  • 写时复制 是 读写分离的思想。

    • 张三 写的时候,复制一份到自己工作空间,写到 最后面,然后更新到主内存,告诉大家。
    • 此时原来的那一份立刻作废。所有引用都用 张三写过名字的。

第3种解决

  • import java.util.concurrent.CopyOnWriteArrayList;
    
  • CopyOnWriteArrayList 写时复制

  • 源码

    private transient volatile Object[] array;

//transient关键字的作用是需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient

    final transient ReentrantLock lock = new ReentrantLock();

    public boolean add(E e) {=
        final ReentrantLock lock = this.lock;
        //获取锁
        lock.lock();
        try {
            //获取原来的长度
            Object[] elements = getArray();
            int len = elements.length;
            //长度 +1,在生成一个 数组。
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //原来的长度,就变成最大索引了。写入e
            newElements[len] = e;
            
            //在设置给 数组,因为有 volatile,立马可见。
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

写时复制

CopyOnwrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器object[]添加,

而是先将当前容器object[]进行cop,复制出一个新的容器object[ ] newElements,

然后新的容器object[ ] newELements里添加元素,添加完元素之后,

再将原容器的引用指向新的容器setArray(newELements);

这样做的好处是可以对Copyonwrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以CopyoOnMrite容器也是一种读写分离的思想,读和写不同的容器

不安全的Set

CopyOnWriteArraySet 问题,最搞笑的源码
  • 底层是:CopyOnWriteArrayList,添加:addIfAbsen
        //1. Collections.synchronizedSet(new HashSet<>());
        //2. CopyOnWriteArraySet

        /*public CopyOnWriteArraySet() {
            al = new CopyOnWriteArrayList<E>();
        }*/
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
        //return al.addIfAbsent(e); 使用:CopyOnWriteArraySet的addIfAbsent,如果存在,就不添加了。就不能重复了。
        //但是有个问题啊,这是有序的,测试的话,果然是有序的。
        set.add("ddd");
        set.add("aaa");
        set.add("ccc");
        set.add("ffff");

        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            System.out.println(next);
        }
Hashset 和 HashMap
        /*HashSet底层是 HashMap
        public HashSet() {
            map = new HashMap<>();
        }*/
        HashSet h = new HashSet();

		// add只使用了 key
        public boolean add(E e) {
            //private static final Object PRESENT = new Object();
            return map.put(e, PRESENT)==null;
        }

//Constructs a new, empty set; the backing HashMap instance has default initial capacity (16) and load factor (0.75).
//初值为16,负载因子为 0.75

			//每次扩容2倍。16 扩容成32。同理:达到24后再次扩容。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold

		//第一次是:16*0.75=12,添加第13个元素后,立刻:进行扩容。

		putVal的添加逻辑
        ++modCount;//变为13
		//++size 变为13,大于 > 12,扩容
        if (++size > threshold)
            resize();

安全的Map

Map hashMap = Collections.synchronizedMap(new HashMap<String, String>());

        //分段锁
        ConcurrentHashMap<Object, Object> hashMap1 = new ConcurrentHashMap<>();

5. 公平 非公平 可重入锁

5.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解﹖请手写一个自旋锁

  • CAS思想,AtomicInteger,使用了 自旋锁

公平和非公平锁

可重入锁(又名递归锁)

独占锁/共享锁

自旋锁

公平 非公平

  • 公平:队列,先来后到,1号先问 问题。
  • 非公平,允许某个人,直接排到第一位。
    • 加塞,我能加上就加。加不上 我在变成公平。
    • 银行:只是取款的,到 二号窗口。

公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
在高并发的情况下,有可能会造成优先级反转或者饥饿现象

        Lock l = new ReentrantLock();
    public ReentrantLock() {
        //非公平锁
        sync = new NonfairSync();
    }
	
	//传递 true 为公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
fair
adj.
公平的;合理的,公正的;相当好的;(数量、大小)相当大的;浅色的,
adv.
公平地,公正地;非常;正面地,直接地;清楚地
n.
集市,展览会;露天游乐场;庙会;
v.
变得晴朗;使(车辆,船舶,飞机)具有流线型

公平锁/非公平锁
并发包中ReentrantLock的创建可以指定构造函教的boolean类型来得到公平锁或非公平锁,默认是非公平锁

关于两者区别:

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

Java ReentrantLock而言,
通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大

对于Synchronized而言,也是一种非公平锁

可重入锁(又名递归锁)

  • 比如 厕所,进入加锁,出来解锁。张三用过,李四用。

  • ReentrantLock/Synchronized就是一个典型的可重入锁

    • 默认是 非公平的 可重入锁
  • 可重入锁最大的作用是避免死锁

可重入锁(也叫做递归锁)

指的是同一线程外层函数获得锁之后﹐内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

也即是说,线程可以进入任何一个它 已经拥有的锁所同步着的代码块

  • 拿到大门的锁后,一马平川,如履平地
    • 就算厕所有锁,属于可信任任务,我可以直接获得。
    • 锁里套锁,拿到外层锁,内层锁 也默认拿到了。
  • 递归: 1 找 2, 2方法完了,才能返回给1
public sync void method01(){
	method02();
}

public sync void method02(){
}
进入内层自动获取锁
public class ReenterLockDemo {
    public static void main(String[] args) throws Exception {

        Phone p = new Phone();

        new Thread(() -> {
            p.sendSMS();
        }, "t1").start();

        new Thread(() -> {
            p.sendSMS();
        }, "t2").start();
    }
}

class Phone {
    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getId() + "\t" + "发短信");
        sendMail();
    }

    public synchronized void sendMail() {
        System.out.println(Thread.currentThread().getId() + "\t" + "发邮件");
    }
}

14	发短信
14	发邮件
15	发短信
15	发邮件
class Phone implements Runnable {
    
    @Override
    public void run() {
        sendSMS();
    }

    Lock lock = new ReentrantLock();

    public void sendSMS() {
        lock.lock();//只要 加锁 和 解锁匹配,几把都行。如果不解锁,会卡死。
        //如果不加锁,直接解锁报错:java.lang.Illegal Monitor State Exception
        try {
            System.out.println(Thread.currentThread().getId() + "\t" + "发短信");
            
            //线程可以进入任何一个它 已经拥有的锁
            
            //所同步着的代码块
            sendEmail();
        } finally {
            lock.unlock();
        }
    }

    public void sendEmail() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t" + "发邮件");

        } finally {
            lock.unlock();
        }
    }
}


        Phone p = new Phone();

        Thread t3 = new Thread(p);
        Thread t4 = new Thread(p);

        t3.start();
        t4.start();

14	发短信
14	发邮件
15	发短信
15	发邮件
命令查找死锁
  • jsstack + pid吧

自旋锁

CAS思想(自旋)

自旋锁(spinlock)

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

  • 学生问 问题看到老师在 打电话,
    • 回去 抽根烟再看,还在打电话
    • 又回去 买瓶水,在看一次。
    • 循环探查的情况。
//Unsafe
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;
    }
*** 手写

题目:实现一个自旋锁
自旋锁好处:循环比较获取直到成功为止,没有类似wait的阻塞。
通过CAs操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁郈B随后抢到。

public class SpinLockDemo {

    AtomicReference<Thread> ar = new AtomicReference<>();

    public void myLock() {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "进入");
        while (!ar.compareAndSet(null, t)) {
        }
    }

    public void myUnlock() {
        Thread t = Thread.currentThread();
        ar.compareAndSet(t, null);
        System.out.println(t.getName() + "出去");
    }

    public static void main(String[] args) {
        SpinLockDemo s = new SpinLockDemo();
        new Thread(() -> {
            s.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            s.myUnlock();
        }, "AA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            s.myLock();
            s.myUnlock();
        }, "BB").start();

    }
}
AA进入
BB进入 #B进入自旋

AA出去 #5秒后,A一定是 先出去。
BB出去 #B 才能出去。
英
/spɪn/
v.
(使)快速旋转;(使)急转身;(头感到)眩晕;纺纱,纺织;飞驰,疾驰;编故事;有倾向性地陈述;
n.
高速旋转;兜风;带倾向性的解释(或说法)

读写锁

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

  • 直播,所有人都能看到 老师的屏幕。共享。
  • 写操作,都是独占。去厕所。一个老师占用一个教室讲课。

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。

  • 读随便读,写的时候独占。名册贴到黑板上,签名的时候,其他人能看。
  • 签名的笔 只有一支。
  • 即保证了数据的一致性,又保证了 并发性。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

多个线程同时读一个资源类没有任何问题。所以为了满足并发量,读取共享资源应该可以同时进行。但是
如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写小

总结:
读-读能共存
读-写不能共存
写-写不能共存

写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断

lnterface ReadWriteLock

  • ReentrantReadwriteLock
trace
英
/treɪs/

v.
查出,发现,追踪;追溯,追究;描绘(事物的过程或发展),
n.
痕迹,遗迹,踪迹;微量,少许;描记图,扫描线;挽绳

heap
英
/hiːp
n.
(凌乱的)一堆;许多,大量;<非正式>破旧的汽车,老爷车
v.
堆积,堆放;对……大加赞扬(或指责等)

stack
英
/stæk
n.
(整齐的)一堆;<英> 垛,堆;大量,许多;(尤指工厂的)大烟囱;
v.
使成整齐的一堆;使成叠(或成摞、成堆)地放在…...;
代码
  • ReentrantReadWriteLock
1正在写入
1写入成功
2正在写入
2写入成功
3正在写入
3写入成功

1正在读取
3正在读取
2正在读取
3读取成功3
1读取成功1
2读取成功2
public class ReadWriteLockDemo {
    
    public static void main(String[] args) {

        MyCache m = new MyCache();

        for (int i = 1; i <= 3; i++) {
            final int t = i;
            new Thread(() -> {
                m.put(t + "", t + "");
            }, i + "").start();
        }

        for (int i = 1; i <= 3; i++) {
            final int t = i;
            new Thread(() -> {
                m.get(t + "");
            }, i + "").start();
        }
    }
}

class MyCache {
    //保证可见性
    private volatile Map<String, Object> map = new HashMap<>();

    private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwlock.writeLock().lock();

        try {
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在写入");

            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);
            System.out.println(name + "写入成功");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwlock.writeLock().unlock();
        }
    }

    public void get(String key) {
        rwlock.readLock().lock();

        try {
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在读取");

            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object r = map.get(key);
            System.out.println(name + "读取成功" + r);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwlock.readLock().unlock();
        }
    }

    public void clearMap() {
        map.clear();
    }
}

6. 递减 栅栏 信号量

6.CountDownLatrh/CyclicBarrier/Semaphore使用过吗?

java.util.concurrent

  • Class CountDownLatch 火箭发射倒计时(全部都正常以后)
    • 只有 减到 0,主线程才会放开。

CountDownLath

让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒

  • 秦灭六国,一统华夏

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。

2离开教室
3离开教室
1离开教室
4离开教室
班长最后一个走

        CountDownLatch c = new CountDownLatch(4);

        for (int i = 1; i <= 4; i++) {
            new Thread(() -> {
                String name = Thread.currentThread().getName();
                System.out.println(name + "离开教室");

                c.countDown();
            }, String.valueOf(i)).start();
        }

        try {
            //CountDownLatch 必须要达到0,才会放开 await
            c.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("班长最后一个走");

枚举的values()

public enum CountryEnum {

    //齐楚 燕韩 赵魏 秦

    ONE(1, "齐国"),
    TWO(2, "楚国"),
    THREE(3, "燕国"),
    FOUR(4, "赵国");

    @Getter
    private Integer code;

    @Getter
    private String name;

    CountryEnum(int code, String name) {
        this.code = code;
        this.name = name;
    }

    public static CountryEnum forEach(Integer index) {
        for (CountryEnum e : CountryEnum.values()) {
            if (index == e.getCode()) {
                return e;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        System.out.println(CountryEnum.forEach(1).getName());
    }
}

单词

latch
英
/lætʃ/
n.
门闩,窗闩;<英>碰锁,碰簧锁;(电子)门闩线路,锁存器
v.
闩上(门),用弹簧锁锁住(门等);(电子)(装置)状态恒定;占有,抓住

cyclic
英
/ˈsaɪklɪk/
adj.
环的;循环的;周期的

barrier
英
/ˈbæriə(r)
n.
障碍,壁垒;障碍物,关卡;分界线,屏障;大关,界限




cycl
美
/ˈsɪkl/
pref.
圆形的;与自行车有关的;环状的

cycling
英
/ˈsaɪklɪŋ
n.
骑自行车

bicycle
n.
单车,自行车
v.
骑自行车

bike
n.
自行车,脚踏车,单车;摩托车

CyclicBarrier

  • 和 CountDownLatch相反的

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

  • 人到齐了,才能开会

CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,
屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过cyclicBarrier的await()方法。

        CyclicBarrier c = new CyclicBarrier(7,
                () -> {
                    System.out.println("召唤神龙");
                }
        );

        for (int i = 1; i <= 7; i++) {
            final int t = i;
            new Thread(() -> {
                System.out.println("收集到了" + t + "颗龙珠");
                try {
                    c.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
收集到了2颗龙珠
收集到了3颗龙珠
收集到了4颗龙珠
收集到了1颗龙珠
收集到了7颗龙珠
收集到了5颗龙珠
收集到了6颗龙珠
召唤神龙

Semaphore

  • 多个线程,抢多个资源。
    • 20个停车位,来了30辆车。争车位。
    • 走一辆,进一辆。
      • 可代替 :锁。比如设置为1,就代表只有一个停车位。
      • 相当于 退化成 synchronized
  • 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
acquire
英
/əˈkwaɪə(r)/
v.
获得,得到;学到,习得;患上(疾病);逐渐具有,开始学会
2抢到车位
3抢到车位
1抢到车位

3离开车位
1离开车位
2离开车位

5抢到车位
4抢到车位
5离开车位
4离开车位
        //第二个参数,默认非公平锁
        Semaphore s = new Semaphore(3, false);

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {

                try {
                    s.acquire();

                    String name = Thread.currentThread().getName();

                    System.out.println(name + "抢到车位");

                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    s.release();
                }

            }, String.valueOf(i)).start();
        }

7. 阻塞队列

7.阻塞队列知道吗?

* Array BlockingOueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

* Linked BLockingQueue:一个基于链表结构的阻塞队列,此队列按FTFO(先进先出)排序元素,吞吐量通常要高于ArrayBLockingueue。

* Synchronous Queue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高

2阻塞队列

  • 2.1阻塞队列有没有好的一面

    • 海底捞,人越多越好。
  • 2.2不得不阻塞,你如何管理

    • 去医院,不得不被堵塞。
    • 去银行取钱。
  • 实现生产者 和 消费者

wait() 睡眠
notify() 唤醒

队列+阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:

线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素

  • 当阻塞队列是时,从队列中获取元素的操作将会被阻塞。
    • 柜台里的蛋糕是空的,获取蛋糕的顾客 会被阻塞
    • 空了 消费者阻塞
  • 当阻塞队列是时,往队列里添加元素的操作将会被阻塞。
    • 柜台的蛋糕是满的,做蛋糕的师傅 被阻塞。
    • 满了 生产者阻塞

BlockingQueue

  • put 一端放入
  • take 一端取出

·试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增

为什么用有什么好处

在多线程领域:所谓阻塞,在某些情况下会挂起线程〈即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

notifyAll() #唤醒所有等待的

为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

体系

  • Collection
    • Queue 队列
      • BlockingQueue 阻塞队列接口
        • LinkedTransferQueue 由链表结构组成的无界阻塞队列。
        • BlockingDeque
        • LinkedBlockingDeque 由链表结构组成的双向阻塞队列
        • Priority BlockingQueue 支持优先级排序的无界阻塞队列。
        • *** SynchronousQueue**
          • 不存储元素的阻塞队列,也即单个元素的队列。
            • 只有一个不取走,不生产第二个。
        • DelayQueue 使用优先级队列实现的延迟无界阻塞队列。
        • *** Array** BlockingQueue
          • 由数组结构组成的有界阻塞队列。
        • *** Linked** BlockingQueue
          • 由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
block
n.
大块,一块(木料、石头等);(四面临街的)街段,
v.
阻塞,堵塞(道路、管道等);遮住(视线)

priority
英
/praɪˈɒrəti/
n.
优先事项,最重要的事;优先,优先权,重点;<英>优先通行权
adj.
优先的

synchron ized
synchronous
英
/ˈsɪŋkrənəs/
adj.
同步的;同时的



offer
英
/ˈɒfə(r)/
v.
提供,给予;提议,表示愿意(做某事);出(价),开(价);提出,作出;表示(爱、友谊等);
n.
主动提议,提供; 出价,报价; (商品的)特价,特惠;求婚


poll
n.
民意调查,民意测验;选举投票,计票;投票数;
v.
对……进行民意测验(调查);获得(票数);(电信,计算机)轮询,探询;

peek
英
/piːk/
v.
偷看,窥视;微露出,探出
n.
一瞥,偷偷地一看;(计算机)读取数据

核心方法

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用

抛出异常

当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException: Queue full

当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

特殊值

插入方法,成功ture失败false

移除方法,成功返回出队列的元素,队列里面没有就返回null

一直阻塞

当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。

当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

add remove element抛出异常
        BlockingQueue<String> b = new ArrayBlockingQueue(2);

        System.out.println(b.add("1"));
        System.out.println(b.add("2"));
        //llegalStateException: Queue full
        //System.out.println(b.add("3"));

        //在开头的为:1
        System.out.println("在开头的为:" + b.element());

        //先进先出,先出来 1
        System.out.println(b.remove());
        System.out.println(b.remove());
        
        //java.util.NoSuchElementException
        System.out.println(b.remove());
offer poll peek 返回值
  • 特殊值
offer
英
/ˈɒfə(r)
v.
提供,给予;提议,表示愿意(做某事);出(价),开(价);
n.
主动提议,提供; 出价,报价; (商品的)特价,特惠;求婚

poll
英
/pəʊl/
n.
民意调查,民意测验;选举投票,计票;投票数;投票点(the polls);
v.
对……进行民意测验(调查);获得(票数);(电信,计算机)轮询,探询;
        BlockingQueue<String> b = new ArrayBlockingQueue(1);
        //true
        System.out.println(b.offer("1"));
        //false
        System.out.println(b.offer("2"));

        //队首 为1
        System.out.println(b.peek());

        //取出1
        System.out.println(b.poll());
        //取出失败为:null
        System.out.println(b.poll());
put take 阻塞
  • 只有 take 出位置,put 才停止阻塞。反之亦然。
		BlockingQueue<String> b = new ArrayBlockingQueue(1);
        try {
            b.put("1");
            //一直阻塞
            //b.put("2");

            b.take();
            //没值的时候,take,依然阻塞
            b.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
offer poll 过期不候
        BlockingQueue<String> b = new ArrayBlockingQueue(1);

        try {
            b.offer("1", 2L, TimeUnit.SECONDS);
            //阻塞2秒钟,返回false
            System.out.println(b.offer("1", 2L, TimeUnit.SECONDS));

            System.out.println(b.poll(2L, TimeUnit.SECONDS));
            //阻塞2秒钟,返回null
            System.out.println(b.poll(2L, TimeUnit.SECONDS));

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

SynchronousQueue

SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。

  • 生产出来,要立刻被别人拿走。定制版。
    • 取的那一刻,才能放入。放入的那一刻,必须有取。
  • 生产一个消费一个,一对一。

每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然

        //SynchronousQueue(boolean fair) 默认非公平锁
        BlockingQueue<String> b = new SynchronousQueue<>();

        new Thread(() -> {
            try {
                System.out.println("放入1");
                b.put("1");
                System.out.println("放入2时 等5秒钟");
                b.put("2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "1").start();

        new Thread(() -> {
            try {
                System.out.println("取出1");
                b.take();
                TimeUnit.SECONDS.sleep(5);
                System.out.println("取出2");
                b.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "2").start();

用在哪里

生产者消费者模式
线程池
消息中间件

传统版
阻塞队列版

*** 生产者消费者传统

synchronized

  • wait
  • notify

lock

  • await

  • Singal

tradition
英
/trəˈdɪʃ(ə)n/
n.
传统,惯例;传统故事,传说,传统信仰;和著名历史人物有共同品质的人;(神学)圣传
  • this.notifyAll()将唤醒所有当前正在this锁等待的线程,
  • 而this.notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)
public class ConsumerTraditionDemo {
    //题目:一个初始值为零的变量,两个线程对其交替操作,一个加1一个减1,来5轮

    //线程 操作(方法) 资源类
    //判断 千活 通知
    //防止虚假唤醒机制

    public static void main(String[] args) {

        ShareData s = new ShareData();
        new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    s.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    s.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

//资源类
class ShareData {
    private volatile int number = 0;
    /*private Lock lock = new ReentrantLock();
    private Condition c = lock.newCondition();*/

    //增加
    public synchronized void increment() throws Exception {
        //lock.lock();
        try {
            //if判断 存在 虚假唤醒。2个线程以上。
            //所以:判断要用 while。被唤醒后,要重新在进行一次判断。

            //判断。
            while (number != 0) {
                //老版写法:加synchronized,this.wait(); Object的方法
                //c.await();
                this.wait();
            }
            //干活
            number++;
            System.out.println(Thread.currentThread().getName() + "\t" + number);

            //通知
            //this.notifyAll();; Object的方法
            //c.signalAll();
            this.notifyAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //lock.unlock();
        }
    }

    public synchronized void decrement() throws Exception {
        //lock.lock();
        try {
            //判断
            while (number == 0) {
                //c.await();
                this.wait();
            }
            //干活
            number--;
            System.out.println(Thread.currentThread().getName() + "\t" + number);

            this.notifyAll();
            //c.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //lock.unlock();
        }
    }
}
虚假唤醒

img

https://blog.csdn.net/logtcm4/article/details/127851774

拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,

  • 那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒
  • 此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,
    • 其中A获取了锁并且加1,执行完毕之后B再执行。
    • 如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1
    • 如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。(我们使用while判断)

synchronized 和 Lock

题目: synchronized和Lock有什么区别?用新的Lock有什么好处?你举例说说

1原始构成
synchronized是关键字属于JVM层面,Java的关键字

  • 底层 汇编,是这两个。进入和离开。

    • javap -c 可以看到
  • monitorenter(,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能遇wait /notify等方法

  • monitorexit

    • 会有两次,正常退出 和 异常退出。
  • 调用wait/notify,如果不加sync报错:java.lang.IllegalMonitorStateException

Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

  • java1.5 后,新的类

2使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用ReentcantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。
需要Lock( )利lunLock()方法配合try/finally语句块来完成。

3等待是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成
ReentcantLock可中断,

  • 1.设置超时方法 tryLock(Long timeout,TimeUnit unit)

  • 2.lockIntercuptibly()放代码块中,调用interrupt()方法可中断

4加锁是否公平
synchronized非公平锁
ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5锁绑定多个条件condition
synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

  • A 唤醒B,B唤醒C , C唤醒D,D唤醒A
精确唤醒

题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:AA打印5次,BB打印0次,CC打印5次
紧接着
AA打印5次,BB打印0次,CC打15次…
来10轮

A	1
B	1
B	2

A	1
B	1
B	2
public class ConditionDemo {

    public static void main(String[] args) {
        ShareResource s = new ShareResource();

        new Thread(() -> {
            for (int i = 1; i <= 2; i++) {
                s.print1();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 2; i++) {
                s.print2();
            }
        }, "B").start();

    }

}

class ShareResource {

    private int number = 1;//A:1,B:2,C:3
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();

    void print1() {
        lock.lock();
        try {
            //1. 判断。循环 当前的值不为 1,就进入等待。
            while (number != 1) {
                //分组睡眠,睡C1
                c1.await();
            }
            //2. 干活
            for (int i = 1; i <= 1; i++) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "\t" + i);
            }
            //3. 唤醒
            //唤醒第二个。先改变,共享变量。
            number = 2;
            c2.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void print2() {
        lock.lock();
        try {
            //1. 判断
            while (number != 2) {
                c2.await();
            }
            //2. 干活
            for (int i = 1; i <= 2; i++) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "\t" + i);
            }
            //3. 唤醒
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

生产者消费者队列版

  • volatile/ CAS /atomicInteger/阻塞队列/线程交互
A插入成功:1
B消费成功:1
A插入成功:2
B消费成功:2
A插入成功:3
B消费成功:3
A插入成功:4
B消费成功:4
A插入成功:5
B消费成功:5
5秒后,叫停了
A生产 停止了 标志位 为假:false
B2秒内没有消费到,自动退出
class MyResource {
    private volatile boolean FLAG = true;//默认开启,进行生产+消费

    public void setFLAG(boolean FLAG) {
        this.FLAG = FLAG;
    }

    //原子整形
    private AtomicInteger ai = new AtomicInteger();

    //在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,
    //尤其还要兼顾效率和线程安全,
    //而这会给我们的程序带来不小的复杂度。
    // 使用阻塞队列,无需考虑
    private BlockingQueue<String> b = null;

    //构造传递,你的 阻塞队列
    public MyResource(BlockingQueue<String> b) {
        this.b = b;
    }

    public void myProd() throws InterruptedException {
        String data;
        boolean r;

        while (FLAG) {
            data = ai.incrementAndGet() + "";
            //2秒内,插入
            r = b.offer(data, 2L, TimeUnit.SECONDS);

            String name = Thread.currentThread().getName();

            if (r) {
                System.out.println(name + "插入成功:" + data);
            } else {
                System.out.println(name + "插入失败:" + data);
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() +
                "生产 停止了 标志位 为假:" + FLAG);
    }


    public void myConsumer() throws InterruptedException {
        String data;

        //FLAG
        while (true) {

            //2秒内,查询
            data = b.poll(2L, TimeUnit.SECONDS);

            String name = Thread.currentThread().getName();

            if (null == data || "".equalsIgnoreCase(data)) {
                FLAG = false;
                System.out.println(name + "2秒内没有消费到,自动退出");
                return;
            }
            System.out.println(name + "消费成功:" + data);

            /*if (null != data) {
                System.out.println(name + "消费成功:" + data);
            } else {
                System.out.println(name + "消费失败:" + data);
            }*/
            TimeUnit.SECONDS.sleep(1);
        }
        /*System.out.println(Thread.currentThread().getName() +
                "消费 停止了 标志位 为假:" + FLAG);*/
    }
}

public class ConsumerBlockDemo {

    public static void main(String[] args) {
        MyResource m = new MyResource(new LinkedBlockingDeque<>());

        new Thread(() -> {
            try {
                m.myProd();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(() -> {
            try {
                m.myConsumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

        try {
            TimeUnit.MILLISECONDS.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("5秒后,叫停了");
        m.setFLAG(false);

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值