【超详细】深入探究Java中的线程安全,让你的程序更加可靠~

这篇具有很好参考价值的文章主要介绍了【超详细】深入探究Java中的线程安全,让你的程序更加可靠~。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

深入探究Java中的线程安全,让你的程序更加可靠!

我们将从以下四个问题入手,对Java的多线程问题抽丝剥茧。

  1. 什么是线程安全?
  2. 如何实现线程安全?
  3. 不同的线程安全实现方法有什么区别?
  4. 如何实现HashMap线程安全?

1. 什么是线程安全?


线程安全指的是多个线程并发访问共享资源时,不会出现数据不一致或其他意外情况的情况。在多线程编程中,线程安全非常重要,因为多个线程可能会同时访问和修改同一数据,如果不进行适当的同步处理,就可能导致数据不一致、竞态条件和死锁等问题。

为了实现线程安全,需要使用一些技术和方法来保证数据的一致性和同步性,例如锁机制、原子操作、线程局部变量等。常用的线程安全类包括Vector、CopyOnWriteArrayList、Hashtable、ConcurrentHashMap、原子类等。

2. 如何实现线程安全?


在Java中,线程安全可以通过以下几种方式实现:

  1. synchronized关键字:读作“森科奈日得”。Java中最基本的锁机制。使用synchronized关键字可以保证多个线程访问共享资源时的互斥性,确保同一时刻只有一个线程能够访问共享资源。可以用于方法或代码块。当方法或代码块被synchronized关键字修饰时,只有一个线程能够进入该方法或代码块,其他线程则会被阻塞,直到当前线程执行完毕。

synchronized的实现原理:被synchronized修饰的代码块称为同步块,当线程进入同步块时,会尝试获取对象的锁,如果对象没有被加锁或者已经获取了该对象的锁,则锁计数器+1;如果该对象已经被其他线程加锁,则该线程会进入阻塞状态,等待其他线程释放锁。当其他线程释放锁后,等待的线程会被唤醒,并重新尝试获取锁并执行同步块中的代码。同一时刻,只有一个线程可以获取对象的锁并执行同步块中的代码。

对象头:在Java中,每个对象都有一个对象头(Object Header),它用于存储对象的元数据,包括对象的哈希码(hashCode)、锁状态、GC标记状态等信息。对象头的大小是固定的,通常占用8个字节(64位系统)或4个字节(32位系统)。
对象头中最重要的信息是锁状态,用于实现Java中的synchronized关键字的同步机制。锁状态的值可以是无锁状态、偏向锁状态、轻量级锁状态或重量级锁状态,锁状态取决于线程之间的竞争情况和锁的使用方式。

以下方法为synchronized关键字的使用:

public class Counter {
    private int count;

    // synchronized 修饰方法
    public synchronized void increment() {
        count++;
    }

    // synchronized 修饰代码块
    public void add(int n) {
        synchronized (this) {
            count += n;
        }
    }
}

使用synchronized关键字的注意点:

  • 方法是实例方法(非静态方法)

优点:简单易用、支持可重用锁。
缺点:性能问题、只能保护代码块或方法。

  1. volatile关键字:读作“我你太欧”。只能用于修饰变量。使用volatile关键字可以保证变量的可见性,即使多个线程同时访问同一个变量时,保证变量的值是一致的。此外,volatile还具有禁止指令重排的作用。当一个变量被volatile修饰时,所有线程访问该变量都是从主内存中读取最新的值。

可见性:如果两个线程同时对一个volatile变量进行修改,由于volatile变量能够保证可见性,那么它们的修改结果都会被立即刷新到主内存中,从而使得另外一个线程可以读取到最新的值。

读取操作顺序与写入操作顺序一致:如果一个线程读取了volatile变量,而在它进行写入之前,另一个线程也读取了同一个volatile变量,那么在第一个线程写入变量之后,另一个线程读取到的变量值是第一个线程写入的最新值,而不是读取时的值。

指令重排:是指处理器或编译器为了优化程序执行效率,在不改变原有程序执行结果的前提下,改变指令的执行顺序,以达到减少指令执行的等待时间、利用处理器的多级流水线、减少分支预测错误等目的。在单线程环境下,指令重排不会带来任何问题,因为最终执行结果不会发生变化。但在多线程环境下,指令重排可能会导致一些意料之外的结果,例如数据不一致、死锁、无限循环等问题。

以下方法为volatile关键字的使用:

public class VolatileExample {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

优点:变量对于所有线程的可见性、顺序读写
缺点:不能保证原子性、频繁读写volatile变量的开销大

  1. Lock锁:使用Lock锁机制可以实现更加灵活的锁操作,比synchronized关键字更加高效。Lock锁最常用的是可重入锁ReentrantLock,Reentrant读作“瑞恩穿特”。

可重入锁是指可以对同一个锁进行重复的加锁和解锁操作,每次加锁操作都必须对应一个解锁操作,否则锁将一直被占用。synchronized关键字也是可重入锁。

以下方法为ReentrantLock可重入锁的使用:

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    private ReentrantLock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println("method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println("method2");
        } finally {
            lock.unlock();
        }
    }
}


可重入锁的特点:

  • 支持重复加锁。在同一个线程中,可重入锁可以对同一个锁进行多次加锁,而不会出现死锁的情况。

重复加锁:内部有一个类似于计数器的变量(锁计数器),每当加锁时,计数器+1,解锁时,计数器-1,直到计数器为0时释放锁。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private ReentrantLock lock = new ReentrantLock();

    public void foo() {
        lock.lock(); // 第一次加锁
        System.out.println(Thread.currentThread().getName() + " get lock.");
        lock.lock(); // 第二次加锁
        System.out.println(Thread.currentThread().getName() + " get lock again.");
        lock.unlock(); // 第一次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock.");
        lock.unlock(); // 第二次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock again.");
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();

        Thread t1 = new Thread(() -> {
            demo.foo();
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            demo.foo();
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

//输出结果
//Thread-1 get lock.
//Thread-1 get lock again.
//Thread-1 release lock.
//Thread-1 release lock again.
//Thread-2 get lock.
//Thread-2 get lock again.
//Thread-2 release lock.
//Thread-2 release lock again.

  • 支持公平锁和非公平锁。可重入锁可以指定是公平锁还是非公平锁,默认是非公平锁。

公平锁和非公平锁:公平锁是指多个线程在等待锁时,按照等待的时间先后依次获得锁,即先到先得的策略。非公平锁则是不考虑等待的时间先后,直接去抢占锁,这可能会导致某些线程一直无法获得锁。

  • 支持中断响应。可重入锁允许在等待锁的过程中响应中断。

  • 支持多条件变量。可重入锁可以为每个条件变量创建一个等待队列,并且可以在条件变量上等待或者唤醒指定数量的线程。

优点:可以重复获取锁避免死锁,性能好,可扩展(公平锁非公平锁、可重入读写锁等)
缺点:代码复杂

  1. 原子操作:原子操作是一种无需加锁的线程安全操作方式,可以保证多个线程同时访问同一个变量时,仍能保证数据的一致性。Java中通过使用原子操作类中的原子操作方法,实现线程安全。

优点:保证操作的完整性,不需要加锁,保证数据的一致性和可见性
缺点:不能保证并发访问的顺序,不能保证数据的原子性(如果操作的数据比较大,仍然需要加锁来保证原子性),实现比较复杂

  1. 线程安全的集合类:Java提供了一些线程安全的集合类,例如Vector、CopyOnWriteArrayList、Hashtable、ConcurrentHashMap等。

Vector:可以理解为线程安全的ArrayList,提供的方法与ArrayList相似,使用synchronized关键字实现。比较古老,可以类比HashTable和HashMap的关系。
CopyOnWriteArrayList:线程安全的ArrayList,将原有的数组复制一份,然后在新数组上进行修改操作,最后再赋给原的数组引用。相对Vector更加高效。
原子操作类:包括AtomicInteger、AtomicLong、AtomicBoolean在内7种。

需要针对具体情况选择合适的线程安全技术和方法,保证多个线程能够正确、高效地访问共享资源。

3. 不同的线程安全实现方法有什么区别?


不同的线程安全实现方法有不同的适用场景和性能表现。比如,synchronized关键字适用于对临界区进行加锁,可以保证线程安全,但是性能可能会受到影响;而使用ConcurrentHashMap等线程安全的集合类,则可以在高并发情况下提高性能和并发性能。

粒度 性能 使用难易
synchronized 修饰方法或代码块,作用对象是整个类或者整个方法、类中的某个成员变量或者代码块 性能较差,不适合高并发场景。 只需在需要同步的方法或代码块前加上synchronized关键字
volatile 修饰变量,作用对象是变量 频繁读写时开销大 只需要在变量前加上volatile关键字
ReentrantLock可重入锁 作用对象是某个变量或者某个代码块 性能较好,适合高并发场景。 需要自己手动加锁和释放锁
原子操作 作用对象是某个变量或者某个代码块 相对较低 实现比较复杂,需要对硬件平台和操作系统进行深入了解,对开发人员的要求比较高

4. 如何实现HashMap线程安全?


  1. HashMap和Hashtable
区别 HashMap Hashtable
线程安全性 不安全,需要进行额外的同步操作 安全
null值 允许key和value都为null 不允许
初始容量和扩容机制 默认初始容量为16,扩容机制是元素数量大于负载因子和数组长度的乘积时,将数组长度翻倍 默认初始容量为11,扩容机制是元素数量大于数组长度时,将数组长度翻倍再加1
遍历方式 通过Iterator实现 通过Enumeration实现
  1. HashTable的常见问题:
    HashTable线程安全而HashMap线程不安全:Hashtable采用了同步机制来保证线程安全,即在每个公共方法上使用了synchronized关键字来确保同一时间只能有一个线程操作Hashtable。而HashMap则没有采用这种同步机制。

HashTable公共方法源码:

// 判断Hashtable中是否存在某个value
public synchronized boolean contains(Object value) {
    if (value == null) {
        throw new NullPointerException();
    }

    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}

HashTable的key和value不允许为null:对于 Hashtable,插入元素时,如果该桶中已经存在元素,则通过 equals方法比较新插入的 key 和桶中已经存在的 key 是否相等,在比较 key 是否相等时需要调用 key 的 equals 方法,如果key为null则没有equals方法。Hashtable的值不允许为null,是因为在Hashtable内部,值被存储在一个Object类型的数组中,数组中的每个元素是一个单独的值对象,而不是一个值的引用。因此,如果允许值为null,将导致无法区分数组中的空槽和实际存储了null值的槽。这可能会导致在对Hashtable进行操作时出现意外的结果。为了避免这种情况,Hashtable不允许null值。

HashMap的key和value允许为null:HashMap的设计目标是尽可能提供高效的查找、插入和删除操作,因此对值的类型没有限制。这样可以让使用者在需要时自由地将null作为值来使用,增加了灵活性。在HashMap中,如果key为null,则它的哈希值为0,因此会将其放在哈希表的第0个位置。

  1. 实现HashMap的线程安全
  • 使用 ConcurrentHashMap
    这是Java提供的线程安全的HashMap实现。它通过分段锁(Segment)的方式来实现线程安全,多个线程可以同时访问不同的Segment,从而提高并发度。

ConcurrentHashMap与HashTable的区别:Hashtable 是基于 synchronized 实现线程安全, ConcurrentHashMap 使用了分段锁的方式来实现线程安全。如果需要在多线程环境下使用哈希表,推荐使用 ConcurrentHashMap。

  • 使用 Collections.synchronizedMap
    这是Java提供的一个工具类,用于将一个非线程安全的Map包装成一个线程安全的Map。它通过对Map的操作加上同步锁(使用synchronized关键字)的方式来实现线程安全。

使用Collections.synchronizedMap实现线程安全代码示例:

Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

synchronizedMap.put("key1", "value1");
synchronizedMap.put("key2", "value2");

String value = synchronizedMap.get("key1");
System.out.println(value);

  • 使用锁机制
    可以自己实现锁机制来保证HashMap的线程安全。比如可以使用synchronized关键字或者ReentrantLock来实现锁机制,保证同一时刻只有一个线程访问HashMap。

在多线程环境下,推荐使用ConcurrentHashMap,因为它的并发度更高,性能更优,但需要注意一些细节问题,如在遍历时需要使用迭代器等。而如果是简单的线程安全需求,可以考虑使用Collections.synchronizedMap。文章来源地址https://www.toymoban.com/news/detail-423432.html

到了这里,关于【超详细】深入探究Java中的线程安全,让你的程序更加可靠~的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践

    线程使程序能够通过同时执行多个任务而更有效地运行。 线程可用于在不中断主程序的情况下在后台执行复杂的任务。 创建线程 有两种创建线程的方式。 扩展Thread类 可以通过扩展Thread类并覆盖其run()方法来创建线程: 实现Runnable接口 另一种创建线程的方式是实现Runnable接口

    2024年03月15日
    浏览(45)
  • 信息系统安全审计,这种系统让你的电脑彻底安全!!

    信息系统安全性审计的主要目标是审查企业信息系统和电子数据的安全性、可靠性、可用性、保密性等。 一是预防来自互联网对信息系统的威胁,二是预防来自企业内部对信息系统的危害。 ——百度百科 信息系统安全涉及多个层面和维度,确保数据的机密性、完整性和可用

    2024年04月10日
    浏览(34)
  • Java中的多线程——线程安全问题

    作者:~小明学编程   文章专栏:JavaEE 格言:热爱编程的,终将被编程所厚爱。 目录 多线程所带来的不安全问题 什么是线程安全 线程不安全的原因 修改共享数据 修改操作不是原子的 内存可见性对线程的影响 指令重排序 解决线程不安全的问题 synchronized 互斥 刷新内

    2024年02月03日
    浏览(58)
  • Java多线程(2)---线程控制和线程安全的详细讲解

    目录 前言 一.线程控制方法 1.1启动线程--start() 1.2线程睡眠---sleep()方法 1.3中断线程--interrupt() 方法 1.4等待线程---join() 二.线程安全  2.1数据不安全---数据共享 ⭐不安全的演示和原因  ⭐不安全的处理方法 ⭐synchronized的使用 2.2数据不安全---内存可见性 ⭐不安全的演示和原因

    2024年02月13日
    浏览(22)
  • 【Spring Security】让你的项目更加安全的框架

    🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Spring Security》。🎯🎯 👉点击这里,就可以查看我的主页啦!👇👇 Java方文山的个人主页 🎁如果感觉还不错的话请给我点赞吧!🎁🎁 💖期待你的加入,一

    2024年02月04日
    浏览(45)
  • EntityFramework多线程安全和连接池问题探究

    因为EntityFramework的DataContext不是线程安全的,所有多线程使用EntityFramework的DataContext遇到了下面错误 “A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threadin

    2024年02月04日
    浏览(25)
  • 支持信创的低代码平台,让你的数据更安全

    编者按:低代码平台火爆,信创很重要,二者相遇会碰撞出怎样的火花呢?本文介绍了低代码平台在信创国产化这块是如何实践的。 概要: (1)信创的意义 (2)支持信创的低代码平台 信创,即信息技术应用创新产业,包含了从IT底层的基础软硬件到上层的应用软件全产业

    2024年02月11日
    浏览(32)
  • 如何让你的会话更安全,浅析Session与Cookie

            在我们面试的时候,面试官问及 XSS 漏洞的时候,我们常常会说比如劫持 Cookie,问及防御方法的时候,又常常会说设置 httponly ,本篇文章将从代码层面简单的普及 Session 和 Cookie 的生成过程,及防御的方法,希望看到这篇文章后,下一次遇到面试官的时候,你能够

    2024年02月22日
    浏览(42)
  • 让你的Demo更安全--Spring Boot实现短信验证码

    随着移动互联网的普及,短信验证码已经成为了很多应用的常用登录和注册方式之一。在传统的应用中,我们一般使用第三方集成商提供的短信验证码服务来实现短信验证码的发送和验证,但是这些服务有可能存在时间限制、价格过高等限制。 为了解决这些问题,我们可以使

    2024年02月07日
    浏览(29)
  • 深入探究 C++ 编程中的资源泄漏问题

    目录 1、GDI对象泄漏 1.1、何为GDI资源泄漏? 1.2、使用GDIView工具排查GDI对象泄漏

    2024年02月08日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包