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

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


目录
相关文章
|
3天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
3天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
3天前
|
存储 Java 编译器
java wrapper是什么类
【10月更文挑战第16天】
11 3
|
5天前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
17 5
|
6天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
20 5
|
5天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
14 1
|
6天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
18 2
|
4天前
|
算法 Java
JAVA 二叉树面试题
JAVA 二叉树面试题
10 0
|
21天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
36 1
C++ 多线程之初识多线程
|
6天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
11 3