【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月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
29天前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
45 8
|
1月前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
29天前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
64 1
|
1月前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
63 17
|
1月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
121 4
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
68 2
|
1月前
|
SQL 算法 关系型数据库
面试:什么是死锁,如何避免或解决死锁;MySQL中的死锁现象,MySQL死锁如何解决
面试:什么是死锁,死锁产生的四个必要条件,如何避免或解决死锁;数据库锁,锁分类,控制事务;MySQL中的死锁现象,MySQL死锁如何解决
|
1月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
53 4