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

好了,我们下期见

相关文章
|
7月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
332 0
|
4月前
|
缓存 安全 Java
JUC系列之《CountDownLatch:同步多线程的精准发令枪 》
CountDownLatch是Java并发编程中用于线程协调的同步工具,通过计数器实现等待机制。主线程等待多个工作线程完成任务后再继续执行,适用于资源初始化、高并发模拟等场景,具有高效、灵活、线程安全的特点,是JUC包中实用的核心组件之一。
|
4月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
268 2
|
4月前
|
Java 测试技术 API
【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!
710 2
|
4月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
381 1
|
7月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
8月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
570 5
|
4月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
217 6
|
7月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
372 83
|
9月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
344 0

热门文章

最新文章