Java多线程并发通俗易懂解读

00.线程或进程

现代操作系统都是以进程作为使用cpu的调度单位,有的以线程作为单位,比如java;也就说,无论是进程还是线程,都是一组数据结构,这个数据结构设计之初是为了方便使用cpu;因此,我们创建多个线程,或fork多个进程,会怎样呢?因为它本质是数据,所以会占用内存资源,但不会占用cpu资源,只有当它运行的时候才会占用cpu资源。理解这一点很重要。

01. 了解Java程序运行过程

  1. java程序,都是通过java命令来运行,实际上,java命令是启动一个jvm进程;jvm就是java虚拟机,jvm翻译java字节码,找到main入口,启动java线程,从main入口,执行java程序;通常执行main的java线程是main线程。
  2. mian线程执行java代码过程中,可能会遇到创建新的线程情况,新创建的线程启动后,和main线程并行执行。即使main线程退出后,其他创建的线程可能还在运行。

02. 程序运行的本质

  • 不管是什么语言的程序,最终都是转换成为机器010101...码执行,于是所有程序都可以抽象成执行者和任务清单,执行者最终体现在硬件上,最常见的是CPU,通用是处理器,在软件程序语言层面,执行者就是常说的进程/线程。另外一个是任务清单,执行者要工作,必须有一份任务清单,程序代码可以看作是任务清单,是人和机器交流的语言。人要驱动机器干活,必须说机器能听懂的语言,这就是程序语言。
  • 一份代码,为什么会出现多线程问题。打个比方,一份代码好比一份菜单做菜过程,假设要做10盘同样的菜,为了快,要10个人同时炒菜;想象一下,10个人同时用一个锅,炒出来的菜能吃吗。这个菜单做菜步骤就是一份代码,10个厨师同时炒菜就是10个线程并发,按理说并发也没问题,但是,因为共享一个锅,问题就出现了。这就是多线程问题。

03. 多线程问题例子-计算固定字符串的md5值


public class Test {
    private static MessageDigest digest;
    public static void main(String[] args) throws Exception {
        try {
            digest = MessageDigest.getInstance("md5");
        } catch (Exception e) {
            e.printStackTrace();
        }
        calcMd5();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                calcMd5();
            }).start();
        }
    }
    private static void calcMd5() {
        String str = "test";
        byte[] res = digest.digest(str.getBytes(Charset.forName("utf-8")));
        StringBuilder sb = new StringBuilder();
        for (byte b : res) {
            sb.append(String.format("%02x", b));
        }
        System.out.println(Thread.currentThread().getName() + ":" + sb.toString());
    }
}

test的md5值是098f6bcd4621d373cade4e832627b4f6,

但上述代码,多线程执行,有的线程计算结果有问题,一次执行结果如下

image.png

04. 多线程并发问题解决

解决10个厨师同时用一口锅的问题,有如下方法

  • 每个厨师自己准备锅,即用线程的局部变量。
public class Test {

    public static void main(String[] args) throws Exception {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("md5");
        } catch (Exception e) {
            e.printStackTrace();
        }
        calcMd5();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                calcMd5();
            }).start();
        }
    }
    private static void calcMd5() {
        String str = "test";
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("md5");
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        byte[] res = digest.digest(str.getBytes(Charset.forName("utf-8")));
        StringBuilder sb = new StringBuilder();
        for (byte b : res) {
            sb.append(String.format("%02x", b));
        }
        System.out.println(Thread.currentThread().getName() + ":" + sb.toString());
    }
}
  • 还是每个厨师一口锅,但这个锅不是厨师的,是饭馆提供的,即ThreadLocal

public class Test {

    private static ThreadLocal<MessageDigest> digestThreadLocal = new ThreadLocal<>();
    public static void main(String[] args) throws Exception {
        calcMd5();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                calcMd5();
            }).start();
        }
    }

    private static MessageDigest getMd5() {
        if(digestThreadLocal.get() == null) {
            try {
                MessageDigest digestTmp = MessageDigest.getInstance("md5");
                digestThreadLocal.set(digestTmp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return digestThreadLocal.get();
    }

    private static void calcMd5() {
        String str = "test";
        byte[] res = getMd5().digest(str.getBytes(Charset.forName("utf-8")));
        StringBuilder sb = new StringBuilder();
        for (byte b : res) {
            sb.append(String.format("%02x", b));
        }
        System.out.println(Thread.currentThread().getName() + ":" + sb.toString());
    }
}
  • 共用一口锅也可以,但确保每次只有一个厨师再用,他用完了再到下一个,即用锁lock
public class Test {
    private static MessageDigest digest;
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws Exception {
        try {
            digest = MessageDigest.getInstance("md5");
        } catch (Exception e) {
            e.printStackTrace();
        }
        calcMd5();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                calcMd5();
            }).start();
        }
    }
    private static void calcMd5() {
        String str = "test";
        byte[] res = null;
        lock.lock();
        try {
             res  = digest.digest(str.getBytes(Charset.forName("utf-8")));
        } finally {
            lock.unlock();
        }

        StringBuilder sb = new StringBuilder();
        for (byte b : res) {
            sb.append(String.format("%02x", b));
        }
        System.out.println(Thread.currentThread().getName() + ":" + sb.toString());
    }
}

总结,上述3个解决方案,一般最好是用局部变量或ThreadLocal,锁如果万不得已,不应该使用。

10 声望
1 粉丝
0 条评论
推荐阅读
Appache httpclient证书认证过程
tcp三次握手后,client开始和server进行SSL连接,默认client用jdk的证书库对服务器证书进行认证,如果证书非法抛异常。如下示例代码

ThinkFault阅读 500

PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 1.7k评论 2

封面图
万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide5阅读 725

封面图
与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.9k

封面图
计算机网络连环炮40问
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

程序员大彬7阅读 1k

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.2k评论 2

封面图
Redis 发布订阅模式:原理拆解并实现一个消息队列
“65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?““那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆宣传,暴击单身狗。”像这种 65 哥通过朋...

码哥字节6阅读 1.4k

封面图
10 声望
1 粉丝
宣传栏