Java大联盟
帮助万千Java学习者持续成长
前言
从今天开始,楠哥给大家讲讲 JUC 并发编程,之前在 B 站直播讲过这个系列的课程,反响不错,还没关注楠哥 B 站的小伙伴赶快上车吧,以免错过更多干货。
B 站搜索:楠哥教你学Java
正文
为什么并发编程这么重要,所有公司都很看重?因为并发编程的目的就是充分利用计算机的资源,把计算机的性能发挥到最大,公司当然看重,因为可以提升效率,效率提升就意味着节约成本。
1、什么是高并发
首先我们要搞清楚并发和并行的区别,并发(concurrency) VS 并行(parallelism)
并发是指多线程操作同一个资源,但不是同时操作,是交替操作,如单核 CPU 的情况下,资源按时间段分配给多个线程。
并行才是真正的多个线程同时执行,多核 CPU,每个线程使用一个 CPU 的资源来运行。
我们所说的并发编程描述的是一种使系统允许多个任务可以在重叠的时间段内执行的设计结构,所谓并发不是指多个任务在同一时间段内同时执行,而是指系统具备处理多个任务在同一时间段内同时执行的能力。
高并发顾名思义是指我们设计的程序,可以支持海量任务的执行在时间段上重叠的情况,高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发是一个比较抽象的概念,以下几个概念可以作为高并发的标准:
(1)QPS:每秒响应的 HTTP 请求数,QPS 并不等于并发数,并发数是指某时刻有多少请求同时访问,QPS 指每秒响应的请求数。
(2)吞吐量:单位时间内处理的请求数,由 QPS 和并发数决定。
(3)平均响应时间:系统对一个请求做出响应的平均时间。
QPS = 并发数 / 平均响应时间。
(4)并发用户数:同时承载正常使用系统功能的用户数量。
互联网分布式架构设计,提高系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。
垂直扩展
垂直扩展:提升单机处理能力,垂直扩展的方式又有两种:
(1)增强单机硬件性能,例如:增加 CPU 核数如32核,升级网卡如万兆,硬盘扩容,升级内存。
(2)提升单机架构性能,例如:使用 Cache 来提高效率,使用异步请求来增加单服务吞吐量,使用 NoSQL 数据库提升数据访问性能:10亿级别的数据量,oracle查询数据需要2-3分钟,mongodb只需要几秒钟的时间。
这种方式是有它的上限的,因为单机性能总是有极限的,比如你们公司普通程序员一天可以完成 5 个需求,高级程序员一天可以完成 10 个需求,为了赶进度,让高级程序员替代普通程序员来写代码,就是垂直扩展。
现在难度升级,有 100 个需求,需要一天搞定,你把高级程序员累死也完成不了,因为已经超过他的能力上限了,怎么办?同时招 10 个一样水平的高级程序员不就搞定了吗,这就是水平扩展。
所以互联网分布式架构设计高并发终极解决方案还是水平扩展。
水平扩展
所谓的集群和分布式都是水平扩展的方案,水平扩展又可分为:
(1)站点层扩展:nginx 反向代理,一个 tomcat 跑不动,让10个tomcat去跑,10个tomcat去分担所有请求。
(2)服务层的水平扩展:通过 RPC 框架实现远程调用,常用的技术栈有我们所熟知的 Dubbo、Spring Boot/Spring Cloud,分布式架构,将业务逻辑拆分到不同的 RPC-Client,各自完成对应的业务,如果某项业务并发量很大,就增加新的 RPC-Client,就能扩展服务层性能,做到理论上的无限高并发。
(3)数据层的水平扩展:在数据量很大的情况下,将原来的一台数据库服务器,拆分成多台,以达到扩充系统性能的目的,主从复制,读写分离,分表分库。
2、进程和线程
什么是进程?简单来理解,进程就是计算机正在运行的一个独立的应用程序,例如打开 IDEA 编写 Java 程序就是一个进程,打开浏览器查找学习资料就是一个进程等。一个应用程序至少有一个进程,也可以有多个进程。那什么是线程呢?进程和线程之间的关系是什么?线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。
进程和线程是应用程序在执行过程中的概念,如果应用程序没有执行,比如 IDEA 工具没有运行起来,那么就不存在进程和线程的概念。应用程序是静态的概念,进程和线程是动态概念,有创建就有销毁,存在也是暂时的,不是永久性的。进程与线程的区别在于进程在运行时拥有独立的内存空间,即每个进程所占用的内存都是独立的,互不干扰。而多个线程是共享内存空间的,但是每个线程的执行是相互独立的,同时线程必须依赖于进程才能执行,单独的线程是无法执行的,由进程来控制多个线程的执行。
Java 有两个线程,main 和 GC,Java 本身是无法开启线程的,因为 Java 无法操作硬件,只能通过调用本地方法,即用 C++ 编写的动态函数库,最终由 C++ 去操作底层开启线程,所以 start 方法本身就是 native 的,如下所示。
private native void start0();
Callable 实现多线程
Callable 同样是个接口,与 Runnable 不同的是 Callable 的 call 方法有返回值,具体使用如下所示。
public class Test {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
//获取返回值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("callablle");
return "hello";
}
}
Runnable 有一个实现类 FutureTask,FutureTask 有一个构造函数参数就是 Callable。
Thread 只能接收 Runnable 类型的参数,但是 Callable 跟 Runnable 没有任何关系?怎么办呢?不能直接搭上关系就间接找关系。
Runnable 有一个实现类 FutureTask,FutureTask 有一个构造函数参数就是 Callable。
搞定,我们需要通过 FutureTask 间接将 Callable 与 Runnable 搭上关系,进而将 Callable 传入 Thread,获取返回值通过调用 FutureTask 的 get 方法。
Callable 与 Runnable 的区别
Callable 可以在任务结束后提供一个返回值,Runnable 没有这个功能。
Callable 中的 call() 方法可以抛出异常,而 Runnable 的 run() 方法不能抛出异常
使用 Callable 可以拿到一个 FutureTask 对象,由于线程属于异步计算模型,因此我们无法从正在运行的线程中得到函数的返回值,在这种情况下,就可以使用 FutureTask 来监视目标线程调用 call 方法的情况,当调用 FutureTask 的 get 方法以获取结果时,当前线程就会阻塞,直到 call() 方法结束返回结果。这样就可以在外部通过 FutureTask 的 get 方法异步获取执行结果,FutureTask 是一个可以控制的异步任务的存在,是对 Runable 实现一种继承和扩展。
Callable 来自于 JUC,Runnable 来自于 java.lang。
注意
get 方法可能会产生阻塞,一般放在代码的最后
Callable 有缓存,两个线程开启,只执行一次
public class Test {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
Thread thread2 = new Thread(futureTask);
thread2.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("callablle");
return "hello";
}
}
3、实际开发中线程的实现方式
多线程的使用方式有两种,一种是将资源和 Runnable 接口绑定在一起,另外一种是将资源和 Runnable 进行解耦合,实际开发中我们更推荐解耦合的方式,两者的区别如下图所示。
没有解耦合
实现解耦合
具体实现代码如下所示。
没有解耦合
public class Test2 {
public static void main(String[] args) {
Account2 account = new Account2();
new Thread(account,"A").start();
new Thread(account,"B").start();
}
}
class Account2 implements Runnable{
private static int num;
@Override
public void run() {
// TODO Auto-generated method stub
num++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}
}
实现解耦合
public class Test {
public static void main(String[] args) {
Account account = new Account();
new Thread(()->{
account.count();
},"A") .start();
new Thread(()->{
account.count();
},"B") .start();
}
}
/**
* 将资源和 Runnable 进行解耦合
* @author southwind
*
*/
class Account{
private static int num;
public void count() {
num++;
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}
}
这里需要注意的是,实际开发中不会直接使用 sleep,而是使用 JUC 的方式,如下所示。
System.out.println("1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2");
JUC 底层还是调用 Thread.sleep,如下所示。
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}
sleep(long millis, int nanos) 方法的使用规则如下所示。
当 nanos 大于等于 500 时,millis 就加 1,当nanos小于500时,不改变millis的值。
当 millis 的值为 0 时,只要 nanos 不为 0,就将millis设置为1。
推荐阅读
楠哥简介
资深 Java 工程师,微信号 nnsouthwind
《Java零基础实战》一书作者,今日头条认证大V
GitChat认证作者,B站认证UP主(楠哥教你学Java)
致力于帮助万千 Java 学习者持续成长。
有收获,就点个在看