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

好了,我们下期见

相关文章
|
11天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
42 6
|
20小时前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
29天前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
39 6
|
1月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
1月前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
42 1
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
47 4
|
1月前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
118 11
|
1月前
|
Java
为什么一般采用实现Runnable接口创建线程?
因为使用实现Runnable接口的同时我们也能够继承其他类,并且可以拥有多个实现类,那么我们在拥有了Runable方法的同时也可以使用父类的方法;而在Java中,一个类只能继承一个父类,那么在继承了Thread类后我们就不能再继承其他类了。
28 0
|
3天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
12 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1