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

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

1、多线程概述

1.1、线程的由来

概念


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


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


原理:

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

注意:

只有正在执行的程序才能叫进程。

1.2、多线程特点

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


线程是进程的基本组成部分

宏观并行,微观串行

原理: 一个"时间片"只能被一个进程拥有,一个进程一次只能执行一个线程

线程的组成:

时间片

由OS进行调度分配,是线程执行的因素之一

数据

栈:每个线程都有自己独立的栈空间(栈独立)

堆:堆空间被所有线程共享(堆共享)

代码

特指书写逻辑的代码

2、线程安全问题

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


临界资源:被多个线程同时访问的对象

原子操作:线程在访问临界资源的过程中,固定不可变的操作步骤

2.1、互斥锁

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


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


同步代码块

同步方法

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、两种同步思路的区别

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

同步方法:线程之间需要争抢时间片以及锁标记(效率慢)

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

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


2.2.2、sleep和wait的区别?

sleep属于Thread类,wait属于Object类

sleep进入的是有限期等待,wait进入的是无限期等待

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一致,在此基础上,如果进行的是增改操作,会进行去重

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


目录
相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
73 2
|
23天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
62 14
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
28天前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
32 6
|
1月前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
59 4
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
121 4
|
2月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
68 5