提到“高并发”,足以算是这几年火遍编程界的网络名词了。毕竟,随着现在互联网的高速发展特别是电商平台类的应用快速发展,互联网服务内容越来越丰富,平台用户量越来越大,淘宝、天猫、京东、“拼夕夕”、抖音等几乎是广大群众每日必备的国民级应用。
在这些应用中,诸如“天猫双11”、“京东618”、“商品秒杀”、“火车票抢票”等大型活动往往都是短时间内集中爆发大量的并发访问量,面对这些瞬间涌入的用户流量和访问需求,如果不能得到妥善解决,那么就会像之前天猫双11崩溃一般,不仅影响了用户体验,而且还会令正常活动受挫,营业额大打折扣...
而如何解决这种高并发问题,首要的基础就是能够玩好线程;所以,今天的内容我们就来学习下关于线程方面的干货知识:
1. 为什么要有线程?
举个栗子:我们打开百度网盘这个应用,想要使用上传和下载功能。
如果没有线程的话,我们的操作是这样的:在上传文件的时候就不能干别的事,需要我们上传成功之后才能下载别的东西,并且上传文件也只能一个个的上传,那将是很糟糕的一个体验;
而如果我们想要实现既能上传又能同时下载这个功能,并且可以多个上传且多个下载的多任务操作怎么办呢?
没错!就是使用线程了!
现在的操作系统不管是windows也好、linux系列也好,基本上都是多用户多任务的操作系统,而多任务就是靠多线程来实现的;多任务执行也就是所谓的“并发”。
2. 进程和线程的区别
提到线程,我们就不得不先提下进程,往往很多人认为一个进程就是一个程序,那么是不是这么一回事呢?
别急,我们来看看进程的定义:
●进程的概述
系统中能够独立运行的程序被称为一个进程,进程也是CPU分配资源的最小单位;
例如我们日常比较熟悉windows进程:
每个进程都有自己独立的一块内存空间,一个单核CPU是单进程处理,即同一时间只能处理一个进程, 但是系统可以分配给每个进程一段有限的执行 CPU 的时间(被称为 CPU 时间片),CPU 在这段时间中执行某个进程,然后下一个时间段可能又跳到另一个进程中去执行,因为CPU切换的速度太快了,远远超出了我们肉眼的识别能力,所以我们看到很多的进程似乎都是同时在运行一样。
而多核CPU则可以实现同时多个进程的执行,只不过因为调度的问题可能导致一个核心在一个时间片内调用多个进程。
●线程的概述
线程是进程中完成一个流程的执行任务,是进程中的一个执行路径,跟进程共享一个内存空间。线程是程序中运行的最小单位。
线程本身不能单独存在,需要运行在进程中。一个进程可以有多个线程(例如:一个Java程序基本上都有main主线程和GC线程[垃圾回收器]等两个以上线程),同一进程的所有线程共享本进程的资源。
3. Java中的线程实现
通过前面的介绍我们认识了线程,那么在Java中线程是如何实现的呢?
其实,在Java中线程的主要实现方式有以下三种:
● 继承Thread类
●实现Runnable接口
●实现Callable接口
前两种实现方式是比较常见的方式并且类和接口也都在java.long包下,第三种Callable在java.util.concurrent包中我们在后面部分会介绍其实现以及不同;
先来看前两种的实现方式:
(1)继承Thread类
首先我们看到其实Thread类也是实现了Runnable接口
这样我们直接创建Thread对象就可以使用了。
使用步骤:
A. 继承Thread类或者直接创建Thread对象
B. 在run方法中实现线程任务代码
C. 调用Thread的start方法启动线程
代码如下:
同样也可以直接new Thread使用,代码如下:
分析:下面的方式使用起来不需要定义类使用简单一些,适合调用次数较少的情况。另外虽然Thread类线程启动之后执行的是run方法,但是线程的启动方法是start方法!
start方法和run方法的区别:
run方法只是单纯的Thread类的一个普通方法,只不过线程启动的时候会调用而已,如果只是使用 thread.run()那么也仅仅代表我们调用了thread的run方法但是并没有开启一个新的线程,代码还是在主线程main中执行。
start方法调用表示创建了一个新的线程,线程进入准备就绪状态,当线程得到cpu时间片处理时,Java 虚拟机就会调用该线程的 run 方法并执行里面的任务代码,任务代码执行完毕则线程结束。另外start开启的新的线程是一个独立的执行任务,下面的代码无须等待线程执行完毕就可以继续执行。
使用Thread类的局限性:
Thread类实现方式虽然简单,但是因为Thread是一个类,在Java中类只能进行单继承,所以对于线程的扩展能力就差。
(2)实现Runable接口
Runnable是一个函数式接口(有@FunctionalInterface修饰并只有一个抽象方法),定义非常简单只有一个run方法,源代码如下:
之前我们也介绍了Thread类其实也是实现了Runable接口,其中启动线程执行任务代码的run方法其实就是Runnable接口的方法。所以我们可以通过实现Runable接口配合Thread类实现线程的扩展使用。
使用步骤:
A. 定义一个类实现Runnable接口
B. 在run方法中实现线程任务代码
C. 通过Thread(Runnable r)构造方法传入Runnable接口实例对象,并调用start方法启动线程
代码如下:
分析:
使用Runnable接口的实现,因为接口可以多实现的特点,所以Runnable接口可以被更多的类实现,扩展性比Thread要强,另外又因Runnable是作为Thread构造方法传入才创建的线程所以Runnable需要依赖与Thread类使用,并且多个Thread对象可以使用同一个Runnable实例。
Runnable的Lambda表达式的使用:
Java8提供了Lambda表达式,我们使用Lambda表达式创建Runable实现类实例的时候不需要我们定义类,变得更加方便。
代码实现如下:
4. 多线程经典案例-卖票案例
虽然开发中通过继承Thread类和实现Runnable接口都可以实现线程,但是因为继承Thread类具有局限性。而实现Runnable接口更容易扩展,并且实现Rnnnable接口的实例可以被多个Thread对象共享,这样解决一些多线程处理资源就更方便一些。我们以卖票的案例来说明:
我们有三个窗口在卖票,总共有100张票,那我们怎么实现3个窗口同时都在卖,并且卖掉100张票呢?
(1)Thread类的实现
运行的结果:
结论:
最后发现每一个窗口都是卖100张票,总共卖了300张票,但是一共只有100张票,显然继承Thread类实现不方便,因为t1、t2、t3三个窗口线程不能共享100张票这个资源,所以导致都各自卖了100张。
(2)Runnable接口的实现
运行的结果:
结论:
我们发现使用Runnable接口的方式可以让t1、t2、t3三个窗口线程同时在卖100张票,而不是每个线程都卖100张,这是因为t1、t2、t3都使用了同一个Runnable实例。
显然Runnable接口的实现可以实现100张票资源的共享,但是通过运行结果我们会发现一个令人困惑的问题,就是不管程序运行多少次,总是有两个或者三个线程卖了同一张票!
这是什么原因导致的呢?
其实这就是所谓的著名的“多线程并发访问不安全的问题!”
Ok,本期干货分享就到这里,后面的篇章中我们接着介绍为什么多线程访问对象会不安全,感兴趣的小伙伴可以关注一波,我们下期见吧~