多线程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修饰的代码块是串行执行的,但是其他代码还是并发执行的,所以相较于完全串行使用一个线程来执行还是提升了不少的效率的,下面我们来看代码运行结果

相关文章
|
3天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
19天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
30天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
1月前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
58 0
|
1月前
|
存储 前端开发 Java
【C++ 多线程 】C++并发编程:精细控制数据打印顺序的策略
【C++ 多线程 】C++并发编程:精细控制数据打印顺序的策略
45 1
|
1月前
|
数据采集 存储 Java
「多线程大杀器」Python并发编程利器:ThreadPoolExecutor,让你一次性轻松开启多个线程,秒杀大量任务!
「多线程大杀器」Python并发编程利器:ThreadPoolExecutor,让你一次性轻松开启多个线程,秒杀大量任务!
|
1月前
|
存储 算法 Java
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
74 0
|
3天前
|
安全 算法 Java
JavaSE&多线程&线程池
JavaSE&多线程&线程池
18 7
|
4天前
|
存储 缓存 NoSQL
为什么Redis使用单线程 性能会优于多线程?
在计算机领域,性能一直都是一个关键的话题。无论是应用开发还是系统优化,我们都需要关注如何在有限的资源下,实现最大程度的性能提升。Redis,作为一款高性能的开源内存数据库,因其出色的单线程性能而备受瞩目。那么,为什么Redis使用单线程性能会优于多线程呢?
17 1
|
11天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
16 1