《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(三)

简介: 《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全

5、多线程锁

经典的八锁问题

  1. 标准访问,先打印短信还是邮件

停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异常

并发修改异常

0913e867827110187bd648acb7b06a21.png


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 <>();


07d378d4a363bdc5336678c3484dae30.png

原理

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是线程安全的吗? 也不是 , 所以有同样的线程安全方法


99011643cb5af1486c85244a5b61626b.png


注: 转换包装后的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等)都是通过生成底层数组的新副本来实现的。


b72051b4e99a921dae709ce9dc5941ad.png

原理

/**
 * 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的知识分享 就到这里,希望本篇文章能够帮助到大家,同时也希望大家看后能学有所获!!!

好了,我们下期见

相关文章
|
6天前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
1天前
|
Java 调度
|
1天前
|
存储 Java 调度
线程池的概述和创建
线程池的创建,构造器需要分别传入什么参数
线程池的概述和创建
|
5天前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
1天前
|
Java
多线程线程同步
多线程的锁有几种方式
|
9天前
|
调度 Python
|
11天前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
50 6
|
1天前
|
安全 C# 开发者
【C# 多线程编程陷阱揭秘】:小心!那些让你的程序瞬间崩溃的多线程数据同步异常问题,看完这篇你就能轻松应对!
【8月更文挑战第18天】多线程编程对现代软件开发至关重要,特别是在追求高性能和响应性方面。然而,它也带来了数据同步异常等挑战。本文通过一个简单的计数器示例展示了当多个线程无序地访问共享资源时可能出现的问题,并介绍了如何使用 `lock` 语句来确保线程安全。此外,还提到了其他同步工具如 `Monitor` 和 `Semaphore`,帮助开发者实现更高效的数据同步策略,以达到既保证数据一致性又维持良好性能的目标。
6 0
|
4天前
|
Java UED
基于SpringBoot自定义线程池实现多线程执行方法,以及多线程之间的协调和同步
这篇文章介绍了在SpringBoot项目中如何自定义线程池来实现多线程执行方法,并探讨了多线程之间的协调和同步问题,提供了相关的示例代码。
31 0
|
5天前
|
编译器 C语言 iOS开发
iOS 16 系统键盘修复问题之确定_lock是否用于保护对_deferredTasks的多线程读写如何解决
iOS 16 系统键盘修复问题之确定_lock是否用于保护对_deferredTasks的多线程读写如何解决