多线程03 线程安全问题以及一些简单的解决策略

简介: 多线程03 线程安全问题以及一些简单的解决策略

前言

首先我们引入多线程是为了解决多次创建进程和销毁进程带来的巨大开销,线程可以共享内存和硬盘资源等等,这里我们就会想,他们共享这些东西会不会涉及到一些安全问题呢?他们没有独立分配自己的资源是一定会有安全问题的,但是就目前在这个快节奏的社会来说,效率的提升是必然需要的,我们只能去发现和解决这些安全问题,效率第一!!

举例

下面我们给出一段代码,我们从代码的效果和原因来解析这段代码产生的线程安全问题以及解决方案.

package Thread;
public class ThreadDemo17 {
    private static int count = 0;
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

我们这里创建两个线程,t1和t2让他们合作完成100000次自增操作,并且阻塞main线程,最后让main线程来收集结果.

下面我给出几次运行产生的结果

运行相同的代码,怎么就一直产生错误的结果,而且错误的还是不一样的呢???

.

下面我给出解析举例

我们观察一下count++这一个自增操作需要几步吧

这里为我们其实只有三步

第一步是取元素,可以看到是getstatic取到count这个变量

第二步是自增1,就是iadd这个操作

第三步是putstatic,气死就是一个保存的操作

iconst_1其实是将1放到栈区的操作数栈顶

由于这个count++的操作是不原子的,所以这里会产生线程安全的问题

下面我们模拟一下不安全的一种用例(以下操作将上述多个操作抽象成取值,自增,保存三个操作)

假设这里只有一次自增,t1中的count先加载为0,此时t2读到的count也是0,t1最后自增保存了一个1,t2进行更新的时候其实更新的也是1,这就会造成了线程不安全问题,最后导致的结果是1而不是2,与我们的理想情况相悖.

导致这个结果的原因有很多,最重要的就是cpu的这种抢占式调度系统,你无法控制cpu先调度哪个线程,执行到哪个命令之后执行其他的命令.

解决方案

这里我们采用锁来保证线程的安全性,你可以理解为此时张三正在上厕所,咔嚓以下把门锁起来了,这个时候,你再急也进不去,完成不了你的任务,即使此时线程调度到你了,你也只是一个阻塞的状态,无法完成任务

我们使用synchronized关键字来修饰,我们会发现需要一个参数,这个参数其实无论你造一个什么对象都是可以的,只要保证这两个线程的参数是同一个对象即可

举例如下

package Test;
public class ThreadDemo1125 {
    private static int count = 0;
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+ count);
    }
}

此时线程就是安全的了,但也意味着线程由完全并发式的执行变成了半并发半串行的执行

我们发现代码中,synchronized修饰的代码块是串行执行的,但是其他代码还是并发执行的,所以相较于完全串行使用一个线程来执行还是提升了不少的效率的,下面我们来看代码运行结果

相关文章
|
9天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
33 2
|
13天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3
|
13天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
12 2
|
13天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
26 2
|
13天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
25 1
|
13天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
26 1
|
13天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
21 1
|
22天前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
40 4
|
29天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
43 6
|
26天前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
38 1