Java开发之高并发必备篇——线程基础
Posted weixin_43802541
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java开发之高并发必备篇——线程基础相关的知识,希望对你有一定的参考价值。
提到高并发,这几年几乎是火遍编程界的网络名词了。无它,随着现在互联网的高速发展特别是电商平台类的应用快速发展,互联网服务内容也越来越丰富,用户越来越多,淘宝、天猫、京东、“拼夕夕”、抖音等几乎成为了广大群众每日必用的应用了。而在这些应用中见到的“天猫双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方法启动线程
分析:下面的方式使用起来不需要定义类使用简单一些,适合调用次数较少的情况。另外虽然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张。
Runnable接口的实现
运行的结果:
结论:
我们发现使用Runnable接口的方式可以让t1、t2、t3三个窗口线程同时在卖100张票,而不是每个线程都卖100张,这是因为t1、t2、t3都使用了同一个Runnable实例。显然Runnable接口的实现可以实现100张票资源的共享,但是通过运行结果我们会发现一个令人困惑的问题,就是不管程序运行多少次,总是有两个或者三个线程卖了同一张票!这是什么原因导致的呢?其实这就是所谓的著名的“多线程并发访问不安全的问题”!后面的篇章中我们会介绍为什么多线程访问对象会不安全。
以上是关于Java开发之高并发必备篇——线程基础的主要内容,如果未能解决你的问题,请参考以下文章
Java开发之高并发必备篇——线程安全操作之synchronized