为何会有多线程?什么是线程安全?如何保证线程安全?

微信公众号:超悦编程
点击上方关注,了解更多算法、数据结构和计算机基础知识的内容
问题或建议,请公众号留言java

本文将会回答这几个问题:web

  1. 为何会有多线程?算法

  2. 什么是线程安全?编程

  3. 怎么样保证线程安全?安全

为何会有多线程

显然,线程安全的问题只会出如今多线程环境中,那么为何会有多线程呢?
最先期的计算机十分原始,尚未操做系统。想要使用计算机时,人们先把计算机能够执行的指令刻在纸带上,而后让计算机从纸带上读取每一条指令,依次执行。这时候的计算机每次只能执行一个任务,是地地道道的单线程。
这种状况下就产生了三个问题:
1. 计算资源的严重浪费
计算机在执行任务时,总少不了一些输入输出操做,好比计算结果的打印等。这时候CPU只能等待输入输出的完成。因此每每一个任务执行下来,可能CPU大部分人时间都是空闲的。而在当时CPU但是一种很是昂贵的资源,因而人们就想怎么可以提升CPU的利用率呢?
2. 任务分配的不公平
如今假如咱们有十个任务须要执行,这但是很常见的。而计算机每次只能执行一个任务,直到执行结束,中间不能中断。那么问题来了,是先执行张三给的任务呢?仍是先干李四的活呢?张三和李四可能拥有一样的优先级,所以不管怎么分配任务总会有人不满意,以为不公平。
3. 程序编写十分困难
计算机一次只能执行一个任务,因此编写程序的时候每每要把不少工做集成到一个程序中,这给程序的编写人员带来了极大的挑战。能不能把程序分模块编写,而后让模块之间只进行必要的通讯呢?
为了解决这些问题,计算机操做系统应运而生。操做系统就是管理计算机硬件与软件资源的计算机程序。那么操做系统如何同时执行多个任务呢?操做系统给每一个任务分配一个进程,而后给进程分配相应的计算资源、IO资源等,这样进程就能执行起来了。操做系统会控制多个进程之间的切换,给每一个进程分配必定的执行时间,而后再切换另外一个进程,这样多个进程即可以轮流着交替执行。由于轮流的时间很短,用户会以为仿佛在独占计算机资源来执行本身的任务。
进程虽然必定程度上缓解了咱们提到的那三个问题,可是仍是会存在问题。给你们举两个例子。一个例子是进程只能干一件事,或者说进程中的代码是串行执行的。这有什么问题吗?固然有。好比咱们用软件安装包安装一个程序,安装过程当中忽然不想安装了,而后点击了取消按钮,结果你发现程序并无取消安装。为何呢?由于进程正在执行安装程序的代码,用户的输入只有等待安装程序的代码完成以后才能执行。因此你发现等进程响应了你取消安装的输入时,其实安装程序早已执行完成。用专业术语来讲,就是用户接口的响应性太差了,用户的输入不能第一时间响应,甚至出现界面假死现象。另外一个例子是如今大部分的处理器是多处理器,好比如今有一个双处理器,而只有一个任务。那么这个任务只能由一个进程来执行,而一个进程只能由一个处理器来执行,那么就有50%的计算资源被浪费了。
这时候,就要说到线程了。线程是进程中实施调度和分派的基本单位。一个进程能够有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动。内存资源分配给进程,同一个进程的全部线程共享该进程全部资源。而CPU分配给线程,即真正在处理器运行的是线程。多线程的出现便解决了咱们以前提到的三个问题,可是多线程每每会带来许多意想不到的问题,这就是接下来咱们要说的线程安全了。微信

什么是线程安全

在谈什么是线程安全的问题以前,先给你们举一个线程不安全的例子,直接上代码数据结构

public class Test {
    private static int count;
    private static class Thread1 extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread1  t1 = new Thread1();
        Thread1  t2 = new Thread1();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这段代码实现的逻辑很简单,首先定义了一个int型的count变量,而后开启了两个线程,每一个线程执行1000次循环,循环中对count进行加1操做。等待两个线程都执行完成后,打印count的值。那么这段代码的输出结果是多少呢?可能不少人会说是2000。可是程序运行后却发现结果大几率不是2000,而是一个比2000略小的数,好比1998这样,并且每次运行的结果可能都不相同。
那么这是为何呢?这就是线程不安全。线程安全是指在多线程环境下,程序能够始终执行正确的行为,符合预期的逻辑。好比咱们刚刚的程序,共两个线程,每一个线程对count变量累加1000次,预期的逻辑是count被累加了2000次,而代码执行的结果却不是2000,因此它是线程不安全的。
为何是不安全的呢?由于count++的指令在实际执行的过程当中不是原子性的,而是要分为读、改、写三步来进行;即先从内存中读出count的值,而后执行+1操做,再将结果写回内存中,以下图所示。
多线程

线程交替执行

这就是线程在计算机中真实的执行过程,看起来好像没问题啊,别急,再看一张图
不安全的线程交替执行

看出来问题了么?上图中线程1执行了两次自加操做,而线程2执行了一次自加操做,可是count却从6变成了8,只加了2.咱们看一下为何会出现这种状况。当线程1读取count的值为6完成后,此时切换到了线程2执行,线程2一样读取到了count的值为6,然后进行改和写操做,count的值变为了7;此时线程又切回了线程1,可是线程1中count的值依然是线程2修改前的6,这就是问题所在!!!即线程2修改了count的值,可是这种修改对线程1不可见,致使了程序出现了线程不安全的问题,没有符合咱们预期的逻辑。
相信你们如今已经对线程不安全已经有了必定的认识了。如今咱们总结一下致使线程不安全的缘由,主要有三点:
  • 原子性:一个或者多个操做在 CPU 执行的过程当中被中断jvm

  • 可见性:一个线程对共享变量的修改,另一个线程不能马上看到性能

  • 有序性:程序执行的顺序没有按照代码的前后顺序执行

前两点前面已经举例了,如今在解释一下第三点。为何程序执行的顺序会和代码的执行顺序不一致呢?java平台包括两种编译器:静态编译器(javac)和动态编译器(jit:just in time)。静态编译器是将.java文件编译成.class文件(二进制文件),以后即可以解释执行。动态编译器是将.class文件编译成机器码,以后再由jvm运行。问题通常会出如今动态编译器上,由于动态编译器为了程序的总体性能会对指令进行重排序,虽然重排序能够提高程序的性能,可是重排序以后会致使源代码中指定的内存访问顺序与实际的执行顺序不同,就会出现线程不安全的问题。

如何保证线程安全

下面简单谈谈针对以上的三个问题,java程序如何保证线程安全呢?
针对问题1:JDK里面提供了不少atomic类,好比AtomicInteger, AtomicLong, AtomicBoolean等等,这些类自己能够经过CAS来保证操做的原子性;另外Java也提供了各类锁机制,来保证锁内的代码块在同一时刻只能有一个线程执行,好比刚刚的例子咱们就能够加锁,以下:

synchronized (Test.class){
    count ++;
}

这样,就可以保证一个线程在多count值进行读、改、写操做时,其余线程不可对count进行操做,从而保证了线程的安全性。
针对问题2:一样能够经过synchronized关键字加锁来解决。与此同时,java还提供了一种轻量级的锁,即volatile关键字,要优于synchronized的性能,一样能够保证修改对其余线程的可见性。volatile通常用于对变量的写操做不依赖于当前值的场景中,好比状态标记量等。
 针对问题3:能够经过synchronized关键字定义同步代码块或者同步方法保障有序性,另外也能够经过Lock接口保障有序性。
怎么样?如今是否是对线程安全有了更加深刻的理解了呢?

以为文章有用的话,点个在看呗,好让更多的人看到这篇文章。
更多关于算法、数据结构和计算机基础知识的内容,欢迎扫码关注个人原创公众号「超悦编程」。


本文分享自微信公众号 - 超悦编程(gh_ca6d8e9bfd68)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索