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》系列可能有点标题党吧?”

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

最新文章

  1. python 的dict的update 方法的一点诡秘的行为
  2. google翻译,翻译当前的网页
  3. 模拟请求之 HTTP_Request2
  4. userInteractionEnabled
  5. [转]SQL2005后的ROW_NUMBER()函数的应用
  6. Git 入门 ---- Git 与 SVN 区别
  7. Java Garbage Collectors
  8. Spring第七篇【Spring的JDBC模块】
  9. 《HelloGitHub》第 24 期(两周年)
  10. xilinx的quick boot(1) ——flash的一些内容
  11. Canny 边缘检测及相关应用
  12. GeForce Experience关闭自动更新
  13. Latex数学公式中的空格
  14. 指定某个div随着指定大div滚动,而不是随着整个窗口固定不动
  15. python 多进程并发接口测试实例
  16. asp.net六种方法刷新页面
  17. idea 导入Mapper错误报错设置
  18. 小A的旅行(绿豆蛙的归宿)【期望DP】
  19. 框架 Hibernate
  20. Redis数据库的学习及与python的交互

热门文章

  1. [洛谷P1122][题解]最大子树和
  2. Eclipse alt+/语法不提示的解决方法
  3. Java之Arrays类
  4. Fiddler之模拟响应、修改请求或响应数据(断点)
  5. 图解SynchronousQueue原理-公平模式
  6. kaldi简介及安装
  7. .Netcore Swagger - 解决外部库导致的“Actions require an explicit HttpMethod binding for Swagger 2.0”
  8. 详解Condition的await和signal等待/通知机制
  9. Python自动发送邮件--smtplib模块
  10. MyBatis的结构和配置