漫道多线程(一):多线程与并行计算简述

文章目录

 

什么是并行计算

并行计算或称平行计算是相对于串行计算来说的。它是一种一次可执行多个指令的算法,目的是提高计算速度,及通过扩大问题求解规模,解决大型而复杂的计算问题。所谓并行计算可分为时间上的并行和空间上的并行。 时间上的并行就是指流水线技术,而空间上的并行则是指用多个处理器并发的执行计算。

什么是多线程

多线程的定义如下,该定义来自多线程的百度百科

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。

由此我们知道多线程本质是一种实现并行计算的技术手段。

并行计算的好处

我们使用多线程就是为了更好的进行并行计算,那么并行计算有那些优势以及前景,优点如下:

1.硬件上的迫切需要

在硬件界有一个非常著名的摩尔定律:

集成电路上可以容纳的晶体管数目在大约每经过24个月便会增加一倍。换言之,处理器的性能每隔两年翻一倍。

而正是得益于摩尔定律的威力,cpu的性能日益高涨,我们的程序性能也随之水涨船高。但是可怕的是受限于人类的科技水平的发展,摩尔定律正在逐渐失效。

为了继续保持程序性能的高速发展,在摩尔定律失效的同时,于是人们逐渐把提升硬件性能的目光聚集在提升CPU多核集成上面,为了更好的利用好多核cpu的性能,由此,并行计算自然而然的推广了开来。

2.更好的提升资源利用率

打个比喻,如果使用串行计算去执行多个任务(例如多个用户的访问),那么就会出现任何一个用户上传或者下载一个大文件,都会引起其他用户的卡顿,因为是串行的,必须满足完这个用户的需求,才能服务下一个用户,可想而知,这对于程序是多么可怕的一件事。

而如果使用并行计算的话,可以同时允许多个用户多个任务同时在后台程序执行,就不会出现这种情况,实际上大多数程序也的确是采用的并行计算处理多用户访问。

在执行多个任务的情况下,可能并行计算的运行效率甚至略低于串行计算(在单核cpu的情况下,并行计算往往还有创建和切换上下文的开销),但是在资源使用效率上提高上,运行效率的略微降低甚至不足一提。而且现在大多数计算机都是多核cpu,那么并行计算的无论是在运行效率还是资源利用率上的优势都足以碾压串行计算。

为什么要使用多线程实现并行计算

实际上,并行计算的实现多种多样,消息队列,微服务,多线程都算是并行计算的技术实现方案。都是不同应用场景下对于并行计算的实现,这些技术方案之间没有高下,只有应用场景之分。

多线程实现并行计算的优势:

  • 方案简单
  • 成本低
  • 因为多线程是可以共享同一个进程下的内存,所以在一定的用户体量下,多线程实现的并行计算性能极高,但是同时也容易带来许多难以发现的bug

CPU时间分片

我们知道,线程概念的引入使程序员不必关心到底底层服务器使用了多少个处理器,提高了程序的可移植性。
也就是我们部署程序时不需要关心程序是跑在单核cpu的服务器上还是多核cpu的服务器上,那么程序底层是使用上面手段帮助我们屏蔽了单核cpu以及多核的差异性?答案就是时间分片技术。

所谓时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

并行与并发

我们知道多线程跑在cpu上只有两种情况,一种是一个cpu跑多个线程,另外一种是一个cpu不跑或者只跑一个线程,它们的术语分别叫做并发与并行

并发

当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发

并行

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行

如何在java中使用多线程

这里就介绍最常用的Thread类以及runable接口

继承Thread类

class MyThread extends Thread{
       
    private String name;
 
    public MyThread(String name){
        this.name = name;
    }
 
    @Override
    public void run() {      
        Thread.currentThread().setName(name); 
         System.out.println("I am Thread :" +name);     
    }
 
}
//  调用
public class ThreadLearn {
       
    public static void main(String[] args)  {
        //实例化继承了Thread的类
        MyThread thread1 = new MyThread("Thread1");
        //通过从Thread类中所继承的start()方法启动线程;
        thread1.start();    
 
    }
 
}

实现Runable接口(java8的Lambda可以轻松实现)

//  调用
public class ThreadLearn2 {
       
    public static void main(String[] args)  {
        //通过Lambda实现接口
        Runable runnable=()->{
            System.out.println("这是一个通过 Lambda 实现的多线程调用");
        }
        //通过传入runnable道一个线程类中进行启动
        Thread thread1 = new Thread(runnable);
        thread1.start();    
    }
 
}

java线程模型以及编写多线程代码会面临的问题

因为Java里面的并发,大多数都与线程脱不开关系。
我们在这里对前面做一个总结:

  1. 线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位,依赖于时间分片技术,可以忽略各个服务器核心数的差异)。
  2. 并行计算不一定要依赖多线程(如PHP中很常见的多进程并发,消息队列等)

Java线程模型建立在两个基本概念之上:

  1. 共享的、默认可见的可变状态
  2. 抢占式线程调度

由此可以引申出3个容易导致数据不一致的情况:

  • 因为可变状态是可见的,代表所有线程可以轻易共享同一个进程的对象,任何线程都可以轻易修改同一个对象,容易引起原子性问题
  • 抢占式线程调度则代表交由系统进行在cpu核心上调入或调出线程。避免了一个无限循环方法一直占用cpu,但是同时也导致了线程调度切换的不可预料,容易让线程执行中半途而废,出现状态不一致的问题,也容易引入原子性问题
  • 多个线程的修改中可能是基于多核cpu的,但是cpu的每个核的缓存都不是和内存实时同步的,容易出现a核线程修改一个变量完毕,b核线程的对应变量还未同步,显而易见容易引起可见性问题

由这两点,我们知道了java的线程模型是对于数据不友好的,让数据容易状态不一致,所以为了保护数据的脆弱,java让数据可以被锁住

总而言之:如何避免线程切换中以及多线程修改出现的情况下保障数据的一致性
这就是我们程序员进行多线程编程的所要考虑的首要问题。

如何解决多线程 并发中数据不一致的问题

设计上如何避免

第一,尽可能限制线程之间的直接通信,大多数的数据不一致都是多个线程修改同一个对象引起的,所以限制直接内存通信对安全性非常有帮助。

  • 隐藏数据,不需要暴露的数据尽量不对其他线程暴露。
  • 采用不可变对象通信
  • 采用字符串进行通信

第二,尽量少使用多线程执行,不使用多线程自然就不会出现数据不一致的问题。所以要尽可能保证子系统内部结构的确定性。

第三,尽量使用现代并发的一些结构以及语法,方便在开发阶段以及维护易于阅读。

使用锁保护脆弱的数据

这里就简单介绍三种:synchronized锁,volatile锁,原子类锁

synchronized锁

该锁既可以用在代码块上也可以用在方法上。它表明在执行整个代码
块或方法之前线程必须取得合适的锁。对于方法而言,这意味着要取得对象实例锁(对于静
态方法而言则是类锁)

这个锁的优点是使用简答,相对应的缺点则是比较笨重:锁定的范围大且会阻塞线程,并且性能低,在许多不需要考虑原子性,只需要保障可见性的情况,就可以用下面这个volatile锁。

volatile锁

它是一种简单的对象域同步处理办法,包括原始类型。可以简单的理解volatile就代表禁用了cpu的缓存,直接从的内存进行读写。因此不会发生可见性问题。

可以把围绕该域的操作看成是一个小小的同步块。程序员可以借此编写简化的代码,但付出
的代价是每次访问都要额外刷一次内存。还有一点要注意,volatile 变量不会引入线程
锁,所以使用volatile 变量不可能发生死锁。

总而言之:volatile只是一种可见性锁,不能解决并发所引起的原子性问题,但是相对于synchronized的优点是不会引起死锁。

注:volatile可以保障本身变量域读写的原子性,但是不能保障变量所涉及到的代码块区域的原子性,所以个人习惯称之为可见性锁

在《java程序员修炼之道》中对于volatile关键字有句关键描述如下:

volatile 变量是真正线程安全的,但只有写入时不依赖当前状态(读取的
状态)的变量才应该声明为volatile 变量。对于要关注当前状态的变量,只能借助线程锁
保证其绝对安全性。

该描述指出了volatile的一个缺点,volatile的原子性只能作用与一次读写,当出现

volatile int a=0;
a++;

并且处于多线程的操作中就会出现问题,++的操作对于volatile来说不是原子性的,因为本质上是先读取的当前状态,然后在基于当前的状态进行加了一次再回写到变量中去,这三步操作对于volatile来说不是原子性 。

原子类锁

许多应用场景下面我们有需要自加自减的原子性且可见性的操作,但是不需要引入synchronized这种重量级锁

因此JDK在1.5时提供了很多原子类,在java.util.concurrent.atomic包下
恰好满足轻量级原子锁定以及高可见性的情况
image

原子类如何自加

private static AtomicInteger atomicInteger = new AtomicInteger();
private static void increase() {
    //  意义等价与 atomicInteger++;
    atomicInteger.getAndIncrement();
}

因为原子类api较多,就不多说了,如果对于原子类有兴趣的道友,可以详读该博客
Java中的13个原子操作类

关于更多多线程的技术方面介绍,可以参考其他博主写出来的技术博客以及关注我的最新博客

资料引用

多线程系列第二篇文章 漫道多线程(二):临界区、锁与JMM

原文来自码农要飞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值