00、故事的起源

“二哥,上一篇《集合》的反响效果怎么样啊?”三妹对她提议的《教妹学 Java》专栏很关心。

“这篇文章的浏览量要比第一篇《泛型》好得多。”

“这是个好消息啊,说明更多人接受了二哥的创作。”三妹心花怒放了起来。

“也许没什么对比性。”

“没有对比性?我翻看了一下二哥 7 个月前写的文章,是真的水啊,嘻嘻。”三妹卖了一个萌,继续说道,“说实话,竟然还有读者愿意看,真的是不可思议。”

“你是想挨揍吗?”

“别啊。我是说,二哥现在的读者真的很幸运,因为他们看到了更高质量的文章。”三妹继续肆无忌惮地说着她的真心话。

“是啊,比以前好多了,但我还要更加地努力,这次的主题是《多线程》,三妹你准备好了吗?”

“早准备好了。让我继续来提问吧,二哥你继续回答。”三妹已经跃跃欲试了。

01、二哥,什么是线程啊?

三妹,听哥给你慢慢讲啊。

要想了解线程,得先了解进程,因为线程是进程的一个单元。你看,我这台电脑同时开了很多个进程,比如说打字用的这个输入法、写作用的这个浏览器,听歌用的这个音乐播放器。

这些进程同时可能干几件事,比如说这个音乐播放器,一边滚动着歌词,一边播放着音频。也就是说,在一个进程内部,可能同时运行着多个线程(Thread),每个线程负责着不同的任务。

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。在 Java 的程序当中,至少会有一个 main 方法,也就是所谓的主线程。

可以同时执行多个线程,执行方式和多个进程是一样的,都是由操作系统决定的。操作系统可以在多个线程之间进行快速地切换,让每个线程交替地运行。切换的时间越短,程序的效率就越高。

进程和线程之间的关系可以用一句通俗的话讲,就是“进程是爹妈,管着众多的线程儿女。”

02、二哥,为什么要用多线程啊?

三妹,先去给哥泡杯咖啡,再来听哥给你慢慢地讲。

多线程作为一种多任务、并发的工作方式,好处多多。

第一,减少应用程序的响应时间。

对于计算机来说,IO 读写和网络通信相对是比较耗时的任务,如果不使用多线程的话,其他耗时少的任务也必须要等待这些任务结束后才能执行。

第二,充分利用多核 CPU 的优势。

操作系统可以保证当线程数不大于 CPU 数目时,不同的线程运行于不同的 CPU 上。不过,即便线程数超过了 CPU 数目,操作系统和线程池也会尽最大可能地减少线程切换花费的时间,最大可能地发挥并发的优势,提升程序的性能。

第三,相比于多进程,多线程是一种更“高效”的多任务执行方式。

对于不同的进程来说,它们具有独立的数据空间,数据之间的共享必须通过“通信”的方式进行。而线程则不需要,同一进程下的线程之间共享数据空间。

当然了,如果两个线程存取相同的对象,并且每个线程都调用了一个修改该对象状态的方法,将会带来新的问题。

什么问题呢?我们来通过下面的示例进行说明。

public class Cmower {

    public static int count = 0;

    public static int getCount() {
        return count;
    }     public static void addCount() {
        count++;
    }     public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(10, 1000, 60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(10));         for (int i = 0; i < 1000; i++) {
            Runnable r = new Runnable() {                 @Override
                public void run() {
                    Cmower.addCount();
                }
            };
            executorService.execute(r);
        }
        executorService.shutdown();
        System.out.println(Cmower.count);
    } }

我们创建了一个线程池,通过 for 循环让线程池执行 1000 个线程,每个线程调用了一次 Cmower.addCount() 方法,对 count 值进行加 1 操作,当 1000 个线程执行完毕后,在控制台打印 count 的值。

其结果会是什么呢?

998、997、998、996、996

但几乎不会是我们想要的答案 1000。

03、二哥,为什么答案不是 1000 呢?

三妹啊,咖啡泡得太浓了。不过,浓一点的好处是更提神了。

程序在运行过程中,会将运算需要的数据从物理内存中复制一份到 CPU 的高速缓存当中,计算结束之后,再将高速缓存中的数据刷新到物理内存当中。

count++ 来说。当线程执行这个语句时,会先从物理内存中读取 count 的值,然后复制一份到高速缓存当中,CPU 执行指令对 count 进行加 1 操作,再将高速缓存中 count 的最新值刷新到物理内存当中。

在多核 CPU 中,每个线程可能运行于不同的 CPU 中,因此每个线程在运行时会有专属的高速缓存。假设线程 A 正在对 count 进行加 1 操作,此时线程 B 的高速缓存中 count 的值仍然是 0 ,进行加 1 操作后 count 的值为 1。最后两个线程把最新值 1 刷新到物理内存中,而不是理想中的 2。

这种被多个线程访问的变量被称为共享变量,他们通常需要被保护起来。

04、二哥,那该怎么保护共享变量呢?

三妹啊,等我喝口咖啡提提神。

针对上例中出现的 count,可以按照下面的方式进行改造。

public static AtomicInteger count = new AtomicInteger();

public static int getCount() {
    return count.get();
} public static void addCount() {
    count.incrementAndGet();
}

使用支持原子操作(即一个操作或者多个操作要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行)的 AtomicInteger 代替基本类型 int。

简单分析一下 AtomicInteger 类,该类源码中可以看到一个有趣的变量 unsafe

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe 是一个可以执行不安全、容易犯错操作的特殊类。AtomicInteger 使用了 Unsafe 的原子操作方法 compareAndSwapInt() 对数据进行更新,也就是所谓的 CAS。

public final native boolean compareAndSwapInt(Object o, long offset,
                                                int expected,
                                                int x);

参数 o 是要进行 CAS 操作的对象(比如说 count),参数 offset 是内存位置,参数 expected 是期望的值,参数 x 是需要更新到的值。

一般的同步方法会从地址 offset 读取值 A,执行一些计算后获得新值 B,然后使用 CAS 将 offset 的值从 A 改为 B。如果 offset 处的值尚未同时更改,则 CAS 操作成功。

CAS 允许执行“读-修改-写”的操作,而无需担心其他线程同时修改了变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。

AtomicInteger 类的源码中还有一个值得注意的变量 value

private volatile int value;

value 使用了关键字 volatile 来保证可见性——当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

当一个共享变量被 volatile 修饰后,它被修改后的值会立即更新到物理内存中,当有其他线程需要读取时,会去物理内存中读取新值。

而没有被 volatile 修饰的共享变量不能保证可见性,因为不确定这些变量会在什么时候被写入物理内存中,当其他线程去读取时,读到的可能还是原来的旧值。

特别需要注意的是,volatile 关键字只保证变量的可见性,不能保证原子性。

05、故事的未完待续

“二哥,《多线程》就先讲到这吧,再多我就吸收不了了!”三妹的态度很诚恳。

“可以。”

“二哥,我记得上次你说要给大号投稿,结果怎么样了?”三妹关切地问。

“唉,都不好意思说,只收获了两个点赞的表情符号,可能还是基于同情心。吓得我不敢再投稿了,先坚持写吧!”

“结局这么惨淡吗,真的没有一个号要转载吗?我看那个投稿群有三百多个公号呢。”三妹很伤心。

“《教妹学 Java》系列可能有点标题党吧?”

“二哥,既然决定要写,请不要怀疑自己。至少三妹很喜欢这种风格啊。”听完三妹语重心长的话,我心底的那种自我怀疑又烟消云散了。

教妹学 Java:难以驾驭的多线程的更多相关文章

  1. 教妹学 Java:大有可为的集合

    00.故事的起源 “二哥,上一篇<泛型>的反响效果怎么样啊?”三妹对她提议的<教妹学 Java>专栏很是关心. “有人评论说,‘二哥你敲代码都敲出幻想了啊.’” “呵呵,这句话 ...

  2. 教妹学 Java:动态伴侣 Groovy

    ​ 00.故事的起源 “二哥,听说上一篇<多线程>被 CSDN 创始人蒋涛点赞了?”三妹对她提议的<教妹学 Java>专栏一直很关心. “嗯,有点激动.刚开始还以为是个马甲,没 ...

  3. 教妹学 Java:晦涩难懂的泛型

    00.故事的起源 “二哥,要不我上大学的时候也学习编程吧?”有一天,三妹突发奇想地问我. “你确定要做一名程序媛吗?” “我觉得女生做程序员,有着天大的优势,尤其是我这种长相甜美的.”三妹开始认真了起 ...

  4. 教妹学Java:Spring 入门篇

    你好呀,我是沉默王二,一个和黄家驹一样身高,刘德华一样颜值的程序员(管你信不信呢).从两位偶像的年纪上,你就可以断定我的码龄至少在 10 年以上,但实话实说,我一直坚信自己只有 18 岁,因为我有一颗 ...

  5. 学Java的前景与就业,资深程序员教你怎么开始学Java!

    IT行业一直是就业的热门岗位,程序员这个职业稳定性和收入比都有着不错的前景,那么学Java的前景和就业是什么样的呢?随着入行Java的准程序员越来越多,各种学习Java的流派也层出不穷!其实在编程的世 ...

  6. 不是广告--如何学Java,我说点不太一样的学习方式

    首先声明,这篇文章不是卖课程.介绍培训班的广告. 最近有不少读者通过微信问我:小白应该怎么学好 Java? 提问的人里有在校大学生.有刚参加工作的.有想转行做程序员的,还有一部分是最近找工作不顺的. ...

  7. 重学 Java 设计模式:实战抽象工厂模式

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获!

  8. 重学 Java 设计模式:实战享元模式「基于Redis秒杀,提供活动与库存信息查询场景」

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 程序员‍‍的上下文是什么? 很多时候一大部分编程开发的人员都只是关注于功能的实现,只 ...

  9. 宝贝,来,满足你,二哥告诉你学 Java 应该买什么书?

    (这次的标题是不是有点皮,对模仿好朋友 guide 哥的,我也要皮一皮) 高尔基说过,对吧?宝贝们,"书籍是人类进步的阶梯",不管学什么,买几本心仪的书读一读,帮助还是非常大的.尽 ...

随机推荐

  1. 如何写一个Python万能装饰器,既可以装饰有参数的方法,也可以装饰无参数方法,或者有无返回值都可以装饰

    Python中的装饰器,可以有参数,可以有返回值,那么如何能让这个装饰器既可以装饰没有参数没有返回值的方法,又可以装饰有返回值或者有参数的方法呢?有一种万能装饰器,代码如下: def decorate ...

  2. linux的装配与虚拟机的快照

    一.科普 1969年,“c语言之父”,“b语言之父”,ken Thompson,开发了一个叫unics系统,是unix系统的雏形,只不过此时的UNICS是用汇编语言写的.移植到其它计算机上需要改很多源 ...

  3. 【Java】导入项目时,出现The project cannot be built until build path errors are resolved错误解决方法

    先检查jar包,jar包的地址如果不一样需要remove后重新导入的,右键项目→Build Path. 看额外的jar包有没有×,地址正不正确,要是不正确,remove错误jar包,再点击Add Ex ...

  4. [译]Vulkan教程(23)暂存buffer

    [译]Vulkan教程(23)暂存buffer Staging buffer 暂存buffer Introduction 入门 The vertex buffer we have right now ...

  5. IT兄弟连 HTML5教程 HTML5表单 HTML5新增表单元素

    HTML5有一些新的表单元素:<datalist>.<keygen>.<output>.不是所有的浏览器都支持HTML5新的表单元素,但即使浏览器不支持该表单属性, ...

  6. Spring Boot可执行Jar包运行原理

    目录 1. 打可执行Jar包 2. 可执行Jar包内部结构 3. JarLauncher 4. 简单总结 5. 远程调试 Spring Boot有一个很方便的功能就是可以将应用打成可执行的Jar.那么 ...

  7. java获取当前年份、月份和日期字符串等

    Java获取当前年份.月份和日期是通过Calendar类的实例对象来获取的. 首先创建一个Calendar类的实例对象,Calendar类属于java.util包. Calendar calendar ...

  8. SpringCloud的入门学习之概念理解、Eureka服务注册与发现入门

    1.微服务与微服务架构.微服务概念如下所示: 答:微服务强调的是服务的大小,它关注的是某一个点,是具体解决某一个问题.提供落地对应服务的一个服务应用,狭意的看,可以看作Eclipse里面的一个个微服务 ...

  9. [Spring cloud 一步步实现广告系统] 1. 业务架构分析

    什么是广告系统? 主要包含: 广告主投放广告的<广告投放系统> 媒体方(广告展示媒介-)检索广告用的<广告检索系统> 广告计费系统(按次,曝光量等等) 报表系统 Etc. 使用 ...

  10. (转)阿里 RocketMQ 安装与简介

    原文:阿里 RocketMQ 安装与简介 一.简介 官方简介: l  RocketMQ是一款分布式.队列模型的消息中间件,具有以下特点: l  能够保证严格的消息顺序 l  提供丰富的消息拉取模式 l ...