什么是并发?QPS?TPS?
- 并发数即当前系统能够同时承载的并发数(即请求处理数 )
- QPS(Queries Per Second)是每秒查询率(一台服务器每秒能够响应的查询次数),即最大吞吐能力。
- TPS(TPS Transactions Per Second)是事务数/秒,这里的事务是指客户端向服务器发送请求然后服务器做出反应的过程。(从客户端发送请求开始计时,服务器响应后结束计时)
TPS和QPS的区别
- QPS不够全面,不能描述增删改,不建议用qps来作为系统性能指标。
- 如果是对一个查询接口(单场景)压测,且这个接口内部不会再去请求其它接口,那么tps=qps,否则,tps≠qps
怎样才能实现高并发?
- 硬件资源:CPU(核心数决定当前能够同时处理的任务数量),内存(在内存中保存数据,提升I/O性能),磁盘(固态硬盘相对来说比较快),网卡(千兆网卡,万兆网卡之类的)
- 软件层面
- 线程:CPU执行和调度的最小单元。
- I/O:磁盘I/O,通过内存,缓存,分库分表减少磁盘I/O。消息队列可以实现异步刷盘。
- 分布式系统
上述中多线程技术是最基础的利用CPU提高性能的技术。
CPU浪费现象和上下文切换
java程序即.java源文件(保存在磁盘上)通过Java c(编译)成.class文件再通过main方法运行这个程序(加载到内存中),程序运行起来就产生一个进程了,由CPU来执行这个进程中的指令(通过线程执行)。
假设进程中,有一个从磁盘加载一个文件保存到数据库的操作。这里会出现IO操作,CPU会阻塞,如果一直让CPU去等待I/O完成,那么就造成了CPU资源的严重浪费,CPU在IO这段时间里应该继续被利用,继续去执行其它线程。
核心总结为:CPU处理速度和I/O速度差距太大,所以就需要在I/O过程中继续利用CPU资源
但应该注意:CPU这样去执行其它线程,肯定就涉及到线程的切换,那么就会涉及到硬件资源如(寄存器)更换另一个线程的信息。这就是上下文切换,所以多线程不一定快,如果上下文切换的代价大于了等待IO的代价,反而多线程会变慢
线程和进程
进程
进程即正在进行的程序,操作系统在运行一个程序的同时会创建一个进程。系统进行资源分配和调度的一个独立单位
例子:启动一个java程序,操作系统就会创建一个进程。
线程
线程是操作系统调度的最小单元,也叫轻量级进程。
一个进程可以创建多个线程,每个线程都有各自的计数器,堆栈和局部变量等属性,并且它们还能访问共享共同属的进程的资源。(这里就产生出了并发编程的毛病)。
为什么要引入线程
如果使用进程为最小的执行单元,那么上下文切换的代价就很大,因为进程相比线程的数据会多很多嘛,而线程是轻量级进程,只持有少部分运行必须的资源(使用ThreadLocal存储),上下文切换就相对来说比较小。
可以总结如下:
进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置
线程切换时,仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作
Java实现线程的方式
- 实现Runnable接口
public class ThreadDemo implements Runnable{
@Override
public void run() { //回调方法
System.out.println("当前线程会被执行的代码");
}
public static void main(String[] args) {
new Thread(new ThreadDemo()).start();
}
}
- 继承Thread类
class ThreadRunA extends Thread {
@Override
public void run() {
System.out.println("=====A=====");
}
}
}
- 实现Callable/Future接口
public class ThreadName implements Callable<Integer>
{
public Integer call()
{
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
}
// call()方法的返回值
return i;
}
public static void main(String[] args)
{
// 创建Callable对象
ThreadName rt = new ThreadName();
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(rt);
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
说明:Callable接口是Java5中新增的接口,不是Runnable接口的子接口,所以Callable对象不能直接作为Thread对象的Target
于是Java5中提供了Future接口来代表Callable接口里call方法的返回值,并为Future接口提供了一个FutureTask实现类,它实现了Future接口和Runnable接口,可以作为Thread类的Target。
所以在创建Callable接口实现类之后,要用FutureTask来包装Callable对象(实现手动装箱)。然后用FutureTask对象作为Target。
Thread类本身也实现了Runnable接口
public
class Thread implements Runnable {
}
为什么是调用start方法而不是run方法?
run方法只是一个实例方法,如果调用其实并没有真正的创建线程,而start方法会用到本地方法即调用JVM的C++代码,从而去操作系统层面真正的使用线程(java本身没有线程),然后再回调run方法
源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
}
private native void start0();
多线程
现在的电脑一般都是多核的了,而一个线程在一个时刻只能运行在一个CPU上。如果不使用多线程,那么其实再多的CPU都没有意义了,因为始终只有一个线程在运行,那么也只需要一个CPU,其它的CPU都是空闲状态,没事干。
如果是单核的话,多线程还是有意义的,因为还是可以在IO期间,CPU转而去执行其它的线程,而不是一直等着那唯一的一个线程。