5、多线程锁
经典的八锁问题
- 标准访问,先打印短信还是邮件
停4秒在短信方法内,先打印短信还是邮件
普通的hello方法,是先打短信还是hello
现在有两部手机,先打印短信还是邮件
两个静态同步方法,1部手机,先打印短信还是邮件
两个静态同步方法,2部手机,先打印短信还是邮件
1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
参考代码
package com.rg.sync; import java.util.concurrent.TimeUnit; /** * @author lxy * @version 1.0 * @Description * @date 2022/4/27 18:15 */ class Phone { public static synchronized void sendEmail() throws Exception{ try { TimeUnit.SECONDS.sleep(4); }catch (InterruptedException e){ e.printStackTrace(); } System.out.println("------sendEmail"); } public synchronized void sendSMS()throws Exception{ System.out.println("------sendSMS"); } public void sayHello(){ System.out.println("------sayHello"); } } /** * * @Description: 8锁 * 1 标准访问,先打印短信还是邮件 邮件 2 停4秒在短信方法内,先打印短信还是邮件 邮件 3 新增普通的hello方法,是先打短信还是hello hello 4 现在有两部手机,先打印短信还是邮件 短信 5 两个静态同步方法,1部手机,先打印短信还是邮件 邮件 6 两个静态同步方法,2部手机,先打印短信还是邮件 邮件 7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件 短信 8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件 短信 * * ==============================解析================================================= * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了, * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法 * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法 (1,2锁) * * 加个普通方法后发现和同步锁无关(3锁) 例:同一个手机,A和B同时枪,但是A要的是手机,B要的是手机壳. * * 换成两个对象后,不是同一把锁了。(4锁) 例:A和B每个有都有一个手机,无需抢了,各自用各自的就行. * * 都换成静态同步方法后,情况又发生变化, 直接把当前的类模板锁了(5,6锁) 例子:我把学校的大门锁了,里面的所有设施都不能用了. * * 一个静态同步,一个普通同步,前者锁的是模板,后者锁的是对象实例. 各自锁的内容不同,彼此无关.(7,8锁) 例:华为手机厂停电与你的手机是否会用无关.. * ===============================总结=============================================== * synchronized实现同步的基础:Java中的每一个对象都可以作为锁。 * 具体表现为以下3种形式。 * 对于普通同步方法,锁是当前实例对象。 * 对于静态同步方法,锁是当前类的Class对象。(不加static锁的是this,加static锁的是X.class) * 对于同步方法块,锁是Synchonized括号里配置的对象. * * 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。 * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁. * 可是别的实例对象的普通同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。 * * 所有的静态同步方法用的也是同一把锁——类对象本身, * 这两把锁(this/Class)是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的。 * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁, * 而不管是同一个实例对象的静态同步方法之间, * 还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象! * */ public class Lock_8{ public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(()->{ try { phone.sendEmail(); } catch (Exception e) { e.printStackTrace(); } },"AA").start(); Thread.sleep(100); new Thread(()->{ try { // phone.sendSMS(); // phone.sayHello(); phone2.sendSMS(); } catch (Exception e) { e.printStackTrace(); } },"BB").start(); } }
6、集合的线程安全
6.1 ArrayList集合线程不安全演示
/** * @author lxy * @version 1.0 * @Description * 题目:请举例说明集合类是不安全的 * * 2 导致原因 * * * 3 解决方案 * 3.1 Vector * 3.2 Collections.synchronizedList() * 3.3 CopyOnWriteArrayList * * 4 优化建议(同样的错误,不出现第2次) * @date 2022/4/28 11:43 */ public class NotSafeDemo { public static void main(String[] args) { List <String> list = new ArrayList <>(); // List <String> list = new Vector <>(); // List <String> list = Collections.synchronizedList(new ArrayList <>()) ; // List <String> list = new CopyOnWriteArrayList <>(); for (int i = 1; i <= 30; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); } } }
运行结果:
ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常
并发修改异常
6.1.1 原理
查看arrayList的底层源码
public boolean add(E e) {//没有synchronized,线程不安全 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
6.2 解决方案
6.2.1 Vector
改用List <String> list = new Vector <>();
原理
public synchronized boolean add(E e) {//vector中为每个方法加上了synchronized修饰,线程安全 modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
6.2.2 Collections
改用 List <String> list = Collections.synchronizedList(new ArrayList <>()) ;
运行结果一切正常!
原理
// 注意:synchronizedList是用同步代码块给传入的集合对象加锁! // 可以看到所有的操作都是上了锁的,synchronized (mutex),锁对象是mutex是来自SynchronizedCollection父类 public void add(int index, E element) { synchronized (mutex) { list.add(index, element); }//mutex:锁对象 如果手动传入,则是传入的对象,如果没有传入则是当前对象this }
扩展
HashMap,HashSet是线程安全的吗? 也不是 , 所以有同样的线程安全方法
注: 转换包装后的list可以实现add,remove,get等操作的线程安全性,但是对于迭代操作,Collections.synchronizedList并没有提供相关机制,所以迭代时需要对包装后的list(必须对包装后的list进行加锁,锁其他的不行)进行手动加锁。
List list = Collections.synchronizedList(new ArrayList()); //必须对list进行加锁 synchronized (list) { Iterator i = list.iterator(); while (i.hasNext()) ...... }
关于 Collections.synchronizedList 更为详尽的说明,请参考 https://blog.csdn.net/weixin_45480785/article/details/118934849
6.2.3 CopyOnWriteArrayList
CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。
原理
/** * CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加, * 而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。 * 添加元素后,再将原容器的引用指向新的容器setArray(newElements)。 * 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。 * 所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 */ 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();//解锁 } }
6.3 扩展类比HashSet和HashMap
6.3.1 HashSet
Set<String> set = new HashSet<>();//线程不安全 Set<String> set = Collections.synchronizedSet(new HashSet<>());//线程安全 Set<String> set = new CopyOnWriteArraySet<>();//线程安全 更推荐使用
补:HashSet底层数据结构是什么?
HashMap
但HashSet的add是放一个值,而HashMap是放K、V键值对
private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT)==null;//元素放在hashMap的key上,value位置上放一个Object常量 }
6.3.2 HashMap
Map <Integer, String> map = new ConcurrentHashMap <>();//new HashMap <>();
关于HashMap底层更为详尽的介绍,请参考 [HashMap深度解析 , 一文让你彻底搞懂HashMap](
总结
OK,今天关于 JUC的知识分享 就到这里,希望本篇文章能够帮助到大家,同时也希望大家看后能学有所获!!!
好了,我们下期见