【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大厂面试高频:Collection 和 Collections 到底咋回答?
Java中的`Collection`和`Collections`是两个容易混淆的概念。`Collection`是集合框架的根接口,定义了集合的基本操作方法,如添加、删除等;而`Collections`是一个工具类,提供了操作集合的静态方法,如排序、查找、同步化等。简单来说,`Collection`关注数据结构,`Collections`则提供功能增强。通过小王的面试经历,我们可以更好地理解这两者的区别及其在实际开发中的应用。希望这篇文章能帮助你掌握这个经典面试题。
44 4
|
2月前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
52 3
|
26天前
|
Java 程序员
Java社招面试中的高频考点:Callable、Future与FutureTask详解
大家好,我是小米。本文主要讲解Java多线程编程中的三个重要概念:Callable、Future和FutureTask。它们在实际开发中帮助我们更灵活、高效地处理多线程任务,尤其适合社招面试场景。通过 Callable 可以定义有返回值且可能抛出异常的任务;Future 用于获取任务结果并提供取消和检查状态的功能;FutureTask 则结合了两者的优势,既可执行任务又可获取结果。掌握这些知识不仅能提升你的编程能力,还能让你在面试中脱颖而出。文中结合实例详细介绍了这三个概念的使用方法及其区别与联系。希望对大家有所帮助!
163 60
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
33 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
25天前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
65 16
|
22天前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
55 9
|
27天前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
59 12
|
1月前
|
监控 Dubbo Java
Java Dubbo 面试题
Java Dubbo相关基础面试题
|
1月前
|
SQL Java 数据库连接
Java MyBatis 面试题
Java MyBatis相关基础面试题

热门文章

最新文章