JUC并发编程系列(二) 6.八锁问题
1.两个sync 锁的对象是方法的调用者 ,按顺序调用 发短信 打电话
2.发短信休眠4S 依旧一样 按顺序调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /\*\* \* 8 锁,关于锁的8 个问题 \* 1. 两个sync 锁的对象是方法的调用者,按顺序调用 发短信 打电话 \* 2. 发短信休眠4S 依旧一样 按顺序调用 \* 核心: 对象!!! \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class Test1 {public static void main (String[] args) {Phone phone = new Phone ();Phone phone1 = new Phone ();new Thread (() -> { phone.sendMes(); }, "A" ).start();new Thread (() -> { phone.call(); }, "B" ).start();new Thread (() -> { phone1.call(); }, "C" ).start(); } }class Phone {public synchronized void sendMes () {try { TimeUnit.SECONDS.sleep(4 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信" ); }public synchronized void call () { System.out.println("打电话" ); } }
3.增加一个普通方法,该方法不会受sync的限制,优先执行
4.两个对象,两个同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 /\*\* \* 3. 增加一个普通方法 该方法不受同步锁限制 会先执行 \* 4. 两个对象 两个同步方法 \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class Test2 {public static void main (String[] args) {Phone2 phone1 = new Phone2 ();Phone2 phone2 = new Phone2 ();new Thread (() -> { phone1.sendMes(); }, "A" ).start();try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); }new Thread (() -> { phone2.call(); }, "B" ).start(); } }class Phone2 {public synchronized void sendMes () {try { TimeUnit.SECONDS.sleep(4 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() +"发短信" ); }public synchronized void call () { System.out.println(Thread.currentThread().getName() +"打电话" ); }public void sayHello () { System.out.println(Thread.currentThread().getName() + "=====>hello" ); } }
注意:
5.两个静态sync方法,一个对象去调用
6.两个静态sync方法,两个对象去调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /\*\* \* 5. 增加两个静态方法 一个对象 发短信 打电话 \* 6. 两个对象 两个静态的同步方法 发短信 打电话 \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class Test3 {public static void main (String[] args) {Phone3 phone1 = new Phone3 ();Phone3 phone2 = new Phone3 ();new Thread (() -> { phone1.sendMes(); }, "A" ).start();try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); }new Thread (() -> { phone2.call(); }, "B" ).start(); } }class Phone3 {public static synchronized void sendMes () {try { TimeUnit.SECONDS.sleep(4 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信" ); }public static synchronized void call () { System.out.println("打电话" ); } }
注意:
7.一个静态的同步方法,一个普通的同步方法 一个对象 打电话 发短信
8.一个静态的同步方法,一个普通的同步方法 两个个对象 打电话 发短信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /\*\* \* 7. 一个静态的同步方法 一个普通的同步方法 一个对象 打电话 发短信 \* 8. 一个静态的同步方法 一个普通的同步方法 两个对象 打电话 发短信 \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class Test4 {public static void main (String[] args) {Phone4 phone1 = new Phone4 ();Phone4 phone2 = new Phone4 ();new Thread (() -> { phone1.sendMes(); }, "A" ).start();try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); }new Thread (() -> { phone2.call(); }, "B" ).start(); } }class Phone4 {public static synchronized void sendMes () {try { TimeUnit.SECONDS.sleep(4 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信" ); }public synchronized void call () { System.out.println("打电话" ); } }
注意:
类锁和对象锁互不干扰,由于sendMes() 是类锁,要等4秒,而call()和它不是一类锁,所以先执行。
总结:
1、当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
2、Java 中的每一个对象都可以作为锁;普通同步方法锁 this,静态同步方法锁 Class ,同步方法块锁括号;
3、只要锁的对象不是同一个 ,就直接按照线程执行的快慢来决定 ;锁的对象是同一个 ,就按照线程进入的先后顺序决定 。
7.集合类不安全
List不安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /\*\* \* @author 路飞 \* @create 2021 /1 /18 \*/public class ListTest {public static void main (String[] args) { /\*\* \* 解决方案: \* 1. new Vector () 线程安全 \* 2. Collections.synchronizedList(new ArrayList <>()); \* 3. new CopyOnWriteArrayList <>(); \*/ List<String> list = new Vector <>();for (int i = 0 ; i <= 10 ; i++) {new Thread (() -> { list.add(UUID.randomUUID().toString().substring(0 , 5 )); list.forEach(System.out::println); }, String.valueOf(i)).start(); } } }
Map不安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /\*\* \* 测试HashMap \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class MapTest {public static void main (String[] args) { /\*\* \* 解决方案 \* 1. Collections.synchronizedMap(new HashMap <>()); \* 2. new ConcurrentHashMap <>(); \*/ Map<Object, Object> map = new ConcurrentHashMap <>();for (int i = 0 ; i < 10 ; i++) {new Thread (() -> { map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0 , 5 )); System.out.println(map); }).start(); } } }
HashMap面试绝对会问底层实现和如何插入重复key,后面会单独写一篇关于HashMap的文章!
Set测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /\*\* \* 测试HashSet \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class SetTest {public static void main (String[] args) { /\*\* \* 解决方案: \* 1. Collections.synchronizedSet(new HashSet <>()); \* 2. new CopyOnWriteArraySet <>(); \*/ Set<Object> set = new CopyOnWriteArraySet <>();for (int i = 0 ; i < 10 ; i++) {new Thread (() -> { set.add(UUID.randomUUID().toString().substring(0 , 5 )); System.out.println(set); }).start(); } } }
hashSet底层是什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 private transient HashMap<E,Object> map;private static final Object PRESENT = new Object (); /\*\* \* Constructs a new , empty set; the backing <tt>HashMap</tt> instance has \* default initial capacity (16 ) and load factor (0.75 ) . \*/public HashSet () { map = new HashMap <>(); } /\*\* \* Constructs a new set containing the elements in the specified \* collection. The <tt>HashMap</tt> is created with default load factor \* (0.75 ) and an initial capacity sufficient to contain the elements in \* the specified collection. \* \* @param c the collection whose elements are to be placed into this set \* @throws NullPointerException if the specified collection is null \*/public HashSet (Collection<? extends E> c) { map = new HashMap <>(Math.max((int ) (c.size()/.75f ) + 1 , 16 )); addAll(c); } /\*\* \* Constructs a new , empty set; the backing <tt>HashMap</tt> instance has \* the specified initial capacity and the specified load factor. \* \* @param initialCapacity the initial capacity of the hash map \* @param loadFactor the load factor of the hash map \* @throws IllegalArgumentException if the initial capacity is less \* than zero, or if the load factor is nonpositive \*/public HashSet (int initialCapacity, float loadFactor) { map = new HashMap <>(initialCapacity, loadFactor); } /\*\* \* Constructs a new , empty set; the backing <tt>HashMap</tt> instance has \* the specified initial capacity and default load factor (0.75 ) . \* \* @param initialCapacity the initial capacity of the hash table \* @throws IllegalArgumentException if the initial capacity is less \* than zero \*/public HashSet (int initialCapacity) { map = new HashMap <>(initialCapacity); } /\*\* \* Constructs a new , empty linked hash set. (This package private \* constructor is only used by LinkedHashSet.) The backing \* HashMap instance is a LinkedHashMap with the specified initial \* capacity and the specified load factor. \* \* @param initialCapacity the initial capacity of the hash map \* @param loadFactor the load factor of the hash map \* @param dummy ignored (distinguishes this \* constructor from other int , float constructor.) \* @throws IllegalArgumentException if the initial capacity is less \* than zero, or if the load factor is nonpositive \*/ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap <>(initialCapacity, loadFactor); } /\*\* \* Returns an iterator over the elements in this set. The elements \* are returned in no particular order. \* \* @return an Iterator over the elements in this set \* @see ConcurrentModificationException \*/public Iterator<E> iterator () {return map.keySet().iterator(); } /\*\* \* Returns the number of elements in this set (its cardinality) . \* \* @return the number of elements in this set (its cardinality) \*/public int size () {return map.size(); } /\*\* \* Returns <tt>true </tt> if this set contains no elements. \* \* @return <tt>true </tt> if this set contains no elements \*/public boolean isEmpty () {return map.isEmpty(); } /\*\* \* Returns <tt>true </tt> if this set contains the specified element. \* More formally, returns <tt>true </tt> if and only if this set \* contains an element <tt>e</tt> such that \* <tt>(o==null ? e==null : o.equals(e))</tt>. \* \* @param o element whose presence in this set is to be tested \* @return <tt>true </tt> if this set contains the specified element \*/public boolean contains (Object o) {return map.containsKey(o); } /\*\* \* Adds the specified element to this set if it is not already present. \* More formally, adds the specified element <tt>e</tt> to this set if \* this set contains no element <tt>e2</tt> such that \* <tt>(e==null ? e2==null : e.equals(e2))</tt>. \* If this set already contains the element, the call leaves the set \* unchanged and returns <tt>false </tt>. \* \* @param e element to be added to this set \* @return <tt>true </tt> if this set did not already contain the specified \* element \*/public boolean add (E e) {return map.put(e, PRESENT)==null ; }
8.常用辅助类 8.1.CountDownLatch
JDK1.8官方文档已经说的很清楚了,CountDownLatch起一个线程同步辅助的作用,类似一个计数器,已经给出示例代码,我们可以写测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /\*\* \* CountDownLatch 计数器 \* \* @author 路飞 \* @create 2021 /1 /18 \*/public class CountDownLatchDemo {public static void main (String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch (6 );for (int i = 0 ; i < 6 ; i++) {new Thread (() -> { countDownLatch.countDown(); System.out.println(Thread.currentThread().getName() + " Go out" +"当前计数器==>" +countDownLatch.getCount()); }, String.valueOf(i)).start(); } countDownLatch.await(); System.out.println("Close Door" ); } }
注意:
countDownLatch.countDown(); //数量-1
countDownLatch.await(); //等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续
执行!
8.2.CyclicBarrier
加法计算器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /\*\* \* CyclicBarrier 加法计数器 \* \* @author 路飞 \* @create 2021 /1 /19 \*/public class CyclicbarrierDemo {public static void main (String[] args) { /\*\* \* 集齐7 颗龙珠,召唤神龙 \*/CyclicBarrier cyclicBarrier = new CyclicBarrier (7 , () -> { System.out.println("召唤神龙成功!" ); });for (int i = 1 ; i <= 7 ; i++) {final int temp = i;new Thread (() -> { System.out.println(Thread.currentThread().getName() + "拿到" + temp + "龙珠" );try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
测试发现,当线程预期没有达到计数器的初始值时,都会阻塞 ,等待下一个线程,直到满足期望才会执行最终的方法。
8.3.Semaphore 信号量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /\*\* \* @author 路飞 \* @create 2021 /1 /19 \*/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(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "抢到了车位" );try { TimeUnit.SECONDS.sleep(3 ); System.out.println(Thread.currentThread().getName() + "离开了车位" ); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }, String.valueOf(i)).start(); } } }
结果:
2抢到了车位 3抢到了车位 1抢到了车位 1离开了车位 3离开了车位 4抢到了车位 2离开了车位 6抢到了车位 5抢到了车位 6离开了车位 4离开了车位 5离开了车位
可以看到在同一时间,只有三辆车停着的 ,与Semaphore初始值相同,符合预期!
semaphore.acquire() 获得,假设如果已经满了,等待直到被释放为止
semaphore.release() 释放,会将当前的信号量释放+1.然后唤醒等待的线程!
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数!
9.读写锁
可以看到读写锁是把lock更细粒度的分化 ,读可以被多个线程同时读,但写的时候只能一个线程去写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 /\*\* \* 独占锁(写锁) 一次只能被一个线程占用 \* 共享锁(读锁) 多个线程可以同时占用 \* ReadWriteLock \* \* @author 路飞 \* @create 2021 /1 /19 \*/public class ReadWriteLockDemo {public static void main (String[] args) {MyCacheLock myCache = new MyCacheLock ();for (int i = 1 ; i <= 10 ; i++) {final int temp = i;new Thread (() -> { myCache.write(temp + "" , temp + "" ); }, String.valueOf(i)).start(); }for (int i = 1 ; i < 10 ; i++) {final int temp = i;new Thread (() -> { myCache.read(temp + "" ); }, String.valueOf(i)).start(); } } } /\*\* \* 自定义缓存---不加锁 \*/class MyCache {private volatile Map<String, Object> map = new HashMap <>();public void write (String key, Object o) { System.out.println(Thread.currentThread().getName() + "写入Key" + key); map.put(key, o); System.out.println(Thread.currentThread().getName() + "写入完成" + key); }public void read (String key) { System.out.println(Thread.currentThread().getName() + "读取key" + key); map.get(key); System.out.println(Thread.currentThread().getName() + "读取成功" + key); } } /\*\* \* 自定义缓存-->加入读写锁 ReadWriteLock 比lock 粒度更细 \*/class MyCacheLock {private volatile Map<String, Object> map = new HashMap <>();private ReadWriteLock readWriteLock = new ReentrantReadWriteLock ();public void write (String key, Object o) { readWriteLock.writeLock().lock();try { System.out.println(Thread.currentThread().getName() + "写入Key" + key); map.put(key, o); System.out.println(Thread.currentThread().getName() + "写入完成" + key); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } }public void read (String key) { readWriteLock.readLock().lock();try { System.out.println(Thread.currentThread().getName() + "读取key" + key); map.get(key); System.out.println(Thread.currentThread().getName() + "读取成功" + key); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } }
这里在写测试类的时候,写了两个自定义缓存类,一个不加锁,一个加锁
不加锁的结果如下:
以发现在线程1写入key1的时候,线程5也在写入key5,然后线程4直接就把key4写入完成了,这很明显不符合多线程的要求,很容易出现线程不安全(key1还没写入完成,后面有插队线程执行写入key1,直接就把key1的值覆盖了)。
加如读写锁的结果:
可以看到,写入是时候只有一个线程,必须等该线程写入完毕后,其它线程才能继续写入,保证了线程安全!
10.阻塞队列
阻塞队列的使用场景:并发处理,线程池
学会使用队列的四组API
方法
抛出异常
有返回值,不抛出异常
阻塞等待
等待超时
添加
add
offer
put
offer
移除
remove
poll
take
poll
检查队首元素
element
peek
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 /\*\* \* ArrayBlockingQueue四组API的使用 \* \* @author 路飞 \* @create 2021 /1 /19 \*/public class BlockingQueueDemo {public static void main (String[] args) throws InterruptedException { test4(); } /\*\* \* 抛出异常 \*/public static void test1 () {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue <>(3 ); System.out.println(blockingQueue.add("a" )); System.out.println(blockingQueue.add("b" )); System.out.println(blockingQueue.add("c" )); System.out.println("----------------------" ); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); } /\*\* \* 有返回值,不抛出异常 \*/public static void test2 () {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue <>(3 ); System.out.println(blockingQueue.offer("a" )); System.out.println(blockingQueue.offer("b" )); System.out.println(blockingQueue.offer("c" )); System.out.println("--------------------------" ); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); } /\*\* \* 阻塞等待 \*/public static void test3 () throws InterruptedException {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue <>(3 ); blockingQueue.put("a" ); blockingQueue.put("b" ); blockingQueue.put("c" ); System.out.println("---------------" ); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); } /\*\* \* 等待,阻塞(超时等待) \*/public static void test4 () throws InterruptedException {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue <>(3 ); System.out.println(blockingQueue.offer("a" )); System.out.println(blockingQueue.offer("b" )); System.out.println(blockingQueue.offer("c" )); System.out.println("----------------------" ); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); } }
使用这个很简单,和数据结构的队列大同小异。
SynchronousQueue 同步队列
每个插入操作必须等待另一个线程相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /\*\* \* 同步队列 \* 和其他的BlockingQueue不一样,SynchronousQueue不存储元素 \* put一个元素,必须从里面取出来,否则不能put \* \* @author 路飞 \* @create 2021 /1 /19 \*/public class SynchronousQueueDemo {public static void main (String[] args) { BlockingQueue<String> blockingQueue = new SynchronousQueue <>();new Thread (() -> {try { System.out.println(Thread.currentThread().getName() + "put 1" ); blockingQueue.put("1" ); System.out.println(Thread.currentThread().getName() + "put 2" ); blockingQueue.put("2" ); System.out.println(Thread.currentThread().getName() + "put 3" ); blockingQueue.put("3" ); } catch (InterruptedException e) { e.printStackTrace(); } }, "T1" ).start();new Thread (() -> {try { TimeUnit.SECONDS.sleep(3 ); System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take()); TimeUnit.SECONDS.sleep(3 ); System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take()); TimeUnit.SECONDS.sleep(3 ); System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }, "T2" ).start(); } }