• 【Java面试宝典】线程安全问题|线程死锁的出现|线程安全的集合类


    CSDN话题挑战赛第2期
    参赛话题:面试宝典

      前言

      线程安全在面试中是考官比较青睐的考点,那我就从多线程的组成特点上开始,分析线程安全问题、死锁出现与解决的方法以及线程安全的集合类总结。希望可以帮助大家理清有关知识点,直面考官,收割offer!


    1、多线程概述

    1.1、线程的由来

    概念

    线程是进程中并发执行的多个任务,进程是操作系统中并发执行的多个程序任务。

    进程具有宏观并行,微观串行的特点:

    • 原理:
      在同一时间段内,CPU会将该时间段划分为很多个时间片,时间片之间交替执行,一个时间片只能被一个进程拥有,只有拿到时间片的程序才能执行自身内容,当时间片的划分足够细小,交替频率足够快,就会形成宏观并行的假象,本质仍然是串行。
    • 注意:
      只有正在执行的程序才能叫进程。

    1.2、多线程特点

    只存在多线程,不存在多进程

    • 线程是进程的基本组成部分
    • 宏观并行,微观串行
      • 原理: 一个"时间片"只能被一个进程拥有,一个进程一次只能执行一个线程
    • 线程的组成:
      1. 时间片
        • OS进行调度分配,是线程执行的因素之一
      2. 数据
        • 栈:每个线程都有自己独立的栈空间(栈独立
        • 堆:堆空间被所有线程共享(堆共享
      3. 代码
        • 特指书写逻辑的代码

    2、线程安全问题

    当多个线程同时访问同一临界资源时,有可能破坏其原子操作,从而导致数据缺失。

    • 临界资源:被多个线程同时访问的对象
    • 原子操作:线程在访问临界资源的过程中,固定不可变的操作步骤

    2.1、互斥锁

    每个对象都默认拥有互斥锁,开启互斥锁之后,线程必须同时拥有时间片和锁标记才能执行,其他线程只能等待拥有资源的线程执行结束释放时间片和锁标记之后,才有资格继续争夺时间片和锁标记。

    利用synchronized开启互斥锁,使线程同步,可以采取两种方法:

    1. 同步代码块
    2. 同步方法

    2.1.1、同步代码块

    思路:谁访问临界资源,谁对其加锁

    synchronized(临界资源对象){
        //对临界资源对象的访问操作
    }
    

    示例:

    public class Test {
        public static void main(String[] args) throws Exception {
            //myList 是自定义的集合类,封装了添加与遍历集合的方法
            MyList m = new MyList();
            //线程1:往集合中添加1-5
            Thread t1=new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=1;i<=5;i++){
                        synchronized (m){
                            m.insert(i);
                        }
                    }
                }
            });
    
            //线程2:往集合中添加6-10
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=6;i<=10;i++){
                        synchronized (m){
                            m.insert(i);
                        }
                    }
                }
            });
    
            //开启线程
            t1.start();
            t2.start();
    
            //让t1和t2优先执行
            t1.join();
            t2.join();
    
            //查看集合元素
            m.query();
        }
    }
    

    此例中临界资源是m,为了防止t1进程中for循环执行后没来得及为其添加元素就被其他进程抢走时间片,因此在刚执行for循环时就将m锁住。

    2.1.2、同步方法

    思路:对多个线程同时访问的方法进行加锁

    访问修饰符 synchronized 返回值类型 方法名(){}
    

    示例:

    public class MyList {
        List<Integer> list = new ArrayList<>();
        //往集合属性中添加一个元素
        public synchronized void insert(int n){
            list.add(n);
        }
        //查看集合元素
        public void query(){
            System.out.println("集合长度:"+list.size());
            for (int n : list){
                System.out.print(n+"  ");
            }
            System.out.println();
        }
    
    }
    
    

    这里是我定义MyList类的源码,如果这时候在insert方法加锁标记,那么这时线程再想被调度执行就需要同时拥有时间片和锁标记。

    2.2.3、两种同步思路的区别

    1. 同步代码块:线程之间只需要争抢时间片,拥有时间片的线程默认拥有锁标记(效率更高)
    2. 同步方法:线程之间需要争抢时间片以及锁标记(效率慢)

    2.2、死锁

    通常是由其中一个线程突然休眠导致

    当多个线程同时访问多个临界资源对象时:假设线程1拥有锁标记1但是没有时间片和锁标记2,线程2拥有时间片和锁标记2但是没有锁标记1,则双方线程都无法正常执行,程序会被锁死。

    结合线程通信来解决死锁问题

    2.2.1、线程通信

    临界资源对象.方法名()

    1. wait():使写入该方法的当前线程释放自身所有资源,进入无限期等待状态,直到其他线程执行结束将其强制唤醒之后,才能回到就绪状态继续时间片和锁标记的争夺
    2. notify():在当前临界资源的等待队列中随机唤醒一个处于无限期等待状态的线程
      • 该方法的调用者应该与对应wait的调用者保持一致
    3. notifyAll():强制唤醒当前临界资源等待队列中的所有线程

    示例:

    public class Test2 {
        public static void main(String[] args) {
            //创建临界资源对象
            Object o1=new Object();
            Object o2=new Object();
    
            //创建线程1:先访问o1,再访问o2
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (o1){
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            System.out.println("休眠异常!!");
                        }
                        synchronized (o2){
                            System.out.println(1);
                            System.out.println(2);
                            System.out.println(3);
                            System.out.println(4);
                            //唤醒t2或t3
                            //o2.notify();
                            //唤醒t2和t3
                            o2.notifyAll();
                        }
                    }
                }
            });
    
            //先访问o2,再访问o1
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (o2){
                        try {
                            o2.wait();//让当前线程释放自身所有资源,在o2的队列中进入无限期等待
                        } catch (InterruptedException e) {
                            System.out.println("操作失败!");
                        }
                        synchronized (o1){
                            System.out.println("A");
                            System.out.println("B");
                            System.out.println("C");
                            System.out.println("D");
                        }
                    }
                }
            });
    
            //先访问o2,再访问o1
            Thread t3 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (o2){
                        try {
                            o2.wait();//让当前线程释放自身所有资源,在o2的队列中进入无限期等待
                        } catch (InterruptedException e) {
                            System.out.println("操作失败!");
                        }
                        synchronized (o1){
                            System.out.println("+");
                            System.out.println("-");
                            System.out.println("*");
                            System.out.println("/");
                        }
                    }
                }
            });
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    在线程t2t3中增加wait会释放时间片与锁标记陷入无限期等待,而t1进程可以在使用完成o2资源后唤醒其他线程从而操作o1资源,这样就不会出现死锁的情况。

    2.2.2、sleep和wait的区别?

    1. sleep属于Thread类,wait属于Object
    2. sleep进入的是有限期等待,wait进入的是无限期等待
    3. sleep只会释放时间片,wait会释放时间片和锁标记

    3、线程安全的集合类

    • 悲观锁:悲观的认为集合一定会出现线程安全问题,所有直接加锁
    • 乐观锁:乐观的认为集合不会出现线程安全问题,所以不加锁,当真正出现问题时,
      再利用算法+少量的synchronized解决问题
    1. ConcurrentHashMap:JDK5.0 java.concurrent

      • JDK8.0之前:悲观锁
        • 在16个数组位上桶加锁
      • JDK8.0之后:CAS算法+少量的synchronized
    2. CopyOnWriteArrayList:JDK5.0 java.concurrent

      • 原理:
        • 当集合进行增删改操作时,会先复制出来一个副本,在副本中进行写操作,如果未出现异常,再将集合地址指向副本地址,若出现异常,则舍弃当前副本,再次尝试。
        • 目的为确保当前集合无异常发生的可能,舍弃写的效率,提高读的效率
      • 适用于读操作远多于写操作时
    3. CopyOnWriteArraySet:JDK5.0 java.concurrent

      • 原理:与CopyOnWriteArrayList一致,在此基础上,如果进行的是增改操作,会进行去重

    本文多为总结性内容,建议大家收藏哦~

  • 相关阅读:
    Java volatile功能简介说明
    UE4 AI行为树实现随机和跟随移动
    【小沐学前端】Node.js实现基于Protobuf协议的UDP通信(UDP/TCP)
    架构问题:技术选型
    OceanBase 4.2.1 LTS 发版 | 一体化数据库首个长期支持版本
    vue+Vant,关闭Popup弹框,遮罩层并没有消失
    线上又出问题了!又是特殊场景,哎呀,当时怎么没有想到!
    【java学习】数组中涉及的常见算法-含冒泡排序(11)
    复现MySQL的索引选择失误以及通过OPTIMIZER_TRACE分析过程
    实战!如何从零搭建10万级 QPS 大流量、高并发优惠券系统--图文解析
  • 原文地址:https://blog.csdn.net/m0_58618795/article/details/127095639