当前位置: 首页 > news >正文

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

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

文章目录

  •   前言
  • 1、多线程概述
    • 1.1、线程的由来
    • 1.2、多线程特点
  • 2、线程安全问题
    • 2.1、互斥锁
      • 2.1.1、同步代码块
      • 2.1.2、同步方法
      • 2.2.3、两种同步思路的区别
    • 2.2、死锁
      • 2.2.1、线程通信
      • 2.2.2、sleep和wait的区别?
  • 3、线程安全的集合类

  前言

  线程安全在面试中是考官比较青睐的考点,那我就从多线程的组成特点上开始,分析线程安全问题、死锁出现与解决的方法以及线程安全的集合类总结。希望可以帮助大家理清有关知识点,直面考官,收割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程序设计】基于SSH(非maven)便利店管理系统-有文档
  • mybatis-plus通用业务分页查询封装
  • 【毕业设计】 心血管疾病分析系统(医学大数据分析)
  • MyBatis学习(三)
  • 【数据挖掘】2022年2023届秋招知能科技公司机器学习算法工程师 笔试题
  • 基于Python的视频中的人脸识别系统设计与实现
  • MySQL主从复制详解
  • 【深度学习入门】- 用电路思想解释感知机
  • 文字生成图片
  • HTTPS的原理浅析与本地开发实践(下)
  • java-多态
  • 一、CSS文本样式[文本基础、文本样式、段落控制]
  • Nginx网络服务的配置
  • m基于随机接入代价的异构网络速率分配算法matlab仿真(包括matlab仿真录像)
  • 【VUE的Form表单】使用v-if切换控件时,表单校验不生效
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 《Java编程思想》读书笔记-对象导论
  • 【附node操作实例】redis简明入门系列—字符串类型
  • Akka系列(七):Actor持久化之Akka persistence
  • CentOS从零开始部署Nodejs项目
  • ECMAScript6(0):ES6简明参考手册
  • ERLANG 网工修炼笔记 ---- UDP
  • JS 面试题总结
  • linux安装openssl、swoole等扩展的具体步骤
  • MobX
  • Sublime Text 2/3 绑定Eclipse快捷键
  • Twitter赢在开放,三年创造奇迹
  • webpack项目中使用grunt监听文件变动自动打包编译
  • -- 查询加强-- 使用如何where子句进行筛选,% _ like的使用
  • 从输入URL到页面加载发生了什么
  • 回流、重绘及其优化
  • 通信类
  • 一份游戏开发学习路线
  • 用简单代码看卷积组块发展
  • 与 ConTeXt MkIV 官方文档的接驳
  • 做一名精致的JavaScripter 01:JavaScript简介
  • Android开发者必备:推荐一款助力开发的开源APP
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #1014 : Trie树
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (全注解开发)学习Spring-MVC的第三天
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • (转)eclipse内存溢出设置 -Xms212m -Xmx804m -XX:PermSize=250M -XX:MaxPermSize=356m
  • (转)http-server应用
  • .mysql secret在哪_MYSQL基本操作(上)
  • .NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇)
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET gRPC 和RESTful简单对比
  • .net Signalr 使用笔记
  • .NET 动态调用WebService + WSE + UsernameToken
  • .net生成的类,跨工程调用显示注释
  • .skip() 和 .only() 的使用
  • [ C++ ] STL---string类的模拟实现