Java的并发编程1
Java的并发编程1
一、进程与线程
1、进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)
2、线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器
二者对比
- 进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为
IPC
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如
HTTP
- 同一台计算机的进程通信称为
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
3、并行与并发
单核cpu
下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将cpu
的时间片(windows
下时间片最小约为15毫秒)分给不同的线程使用,只是由于cpu
在线程间(时间片很短)的切换非常快,人类就感觉是同时运行的。总结就是一句话:微观串行,宏观并行
一般会将这种线程轮流使用CPU
的做法称为并发,concurrent
多核cpu
下,每个核都可以调度运行线程,这时候线程是可以并行的
并发和并行的描述:
- 并发是同一时间应对多件事情的能力
- 并行是同一时间动手做多件事情的能力
4、异步调用
从方法调用的角度来讲,如果:
- 需要等待结果返回,才能继续运行的就是同步
- 不需要等待结果返回,才能继续运行的就是异步
注意:同步在多线程中还有另外一层意思,是让多个线程步调一致
1)设计
多线程可以让方法执行变为异步的(即不要干巴巴等着)比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒调用者什么都做不了,其代码都得暂停…
2)结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
tomcat
的异步servlet
也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat
的工作线程ui
程序中,开线程进行其他操作,避免阻塞ui
线程
操作大文件一般都要开启新线程
多核
cpu
才能提升效率,单核仍然是轮流执行
- 单核
cpu
下,多线程不能实际提高程序运行效率,只能为了能够在不同的任务之间切换,不同线程轮流使用cpu
,不至于一个线程总占用cpu
,别的线程没法干活 - 多核
cpu
可以并行跑多个线程,但能否提高程序运行效率还是要分情况的- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分
- 也并不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
IO
操作不占用cpu
,只是我们一般拷贝文件使用的是【阻塞IO】
,这时相当于线程虽然不用cpu
,但是需要一直等待IO
结束,没有充分利用线程。所以才有后面的【非阻塞IO】
和【异步IO】
优化
二、Java
线程
1、创建和运行线程
方法一:直接使用Thread
就是继承Thread类
// 创建线程对象 可以直接给构造方法设置参数指定线程的名字
Thread t = new Thread("t1") {@Override// run 方法内实现了要执行的任务public void run() {// 要执行的任务}
};
// 启动线程
t.start();
方法二:使用Runnable
配合Thread
需要实现Runnable
接口
把【线程】和【任务】(要执行的代码)分开
Thread
代表线程Runnable
可运行的任务(线程要执行的代码)
// 创建任务对象
Runnable runnable = new Runnable() {@Overridepublic void run() {// 要执行的任务}
};
// 创建线程对象 参数1-是任务对象 参数2-线程名字
Thread t2 = new Thread(runnable, "t2");
// 启动线程
t2.start();
Java 8
以后可以使用lambda
精简代码:
// 创建任务对象
Runnable task2 = () -> log.debug("Hello");Thread t2 = new Thread(task2, "t2");
t2.start();
Lambda
表达式:
Lambda
本质上是一种 语法糖,它用来简化 函数式接口(functional interface
)的写法。说白了,它就是 把匿名内部类的写法变得更简洁。
例子(匿名内部类写法):
Runnable r = new Runnable() {@Overridepublic void run() {System.out.println("Hello Lambda");}
};
Lambda
写法:
Runnable r = () -> System.out.println("Hello Lambda");
Lambda
只能用在函数式接口:
- 函数式接口:只包含一个抽象方法的接口
- 一般这种接口会有注解
@FunctionalInterface
,但是注解不是必须的,但加上可以帮助编译器检查,保证接口只有一个抽象方法。
Runnable
接口就是函数式接口,故可以简化
Thread
与Runnable
的关系:
- 方法一是把线程和任务合并在一起,方法二是把线程和任务分开了
- 用
Runnable
更容易与线程池等高级API
配合 - 用
Runnable
让任务类脱离了Thread
继承体系,更灵活
方法三:FutureTask
配合Thread
FutureTask
能够接收Callable
类型的参数,用来处理有返回结果的情况(间接实现了Runnable
接口)
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("running");return 100;
});Thread thread = new Thread(task);
thread.start();// get会阻塞线程 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}", result);
2、线程的查看和杀死
Windows
:
- 任务管理器中可以查看进程和线程数,也可以用来杀死进程
tasklist
可以查看进程taskkill
杀死进程
PID
对应的就是进程ID
,故要杀死指定的进程时:taskkill /F /PID 55520
:/F
表示强制杀死,最后输入指定的进程ID
可以输入
tasklist | findstr java
查找含有java
的进程
linux
:
ps -fe
查看所有进程ps -fT -p <PID>
查看某个进程(PID
)的所有线程kill
杀死进程top
按大写H
切换是否显示线程top -H -p <PID>
查看某个进程(PID
)的所有线程
可以输入
ps -fe | grep java
查找含有java
的进程
Java
jps
命令查看所有的Java
进程jstack <PID>
查看某个Java
进程(PID
)的所有线程状态jconsole
来查看某个Java
进程中线程的运行情况(图形界面)
jconsole
远程监控配置
jconsole
是 Java 提供的一个监控和管理工具,属于 JDK 的一部分。它用于监控 Java 应用的性能、资源使用情况,以及 JVM(Java Virtual Machine)的状态。
启动使用 win
+ R
输入jconsole
即可启动:
-
如果需要远程连接(比如连接在虚拟机上启动
Java
进程),就按照以下的方法在虚拟机中运行自己的java
程序:java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连 接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类
这里取消安全连接和认证,都填成
false
,连接端口都可以,ip
地址就写这个虚拟机的端口即可,java
类改成自己想要运行的java
类(注意:如果连接不上可能是虚拟机中防火墙没关) -
修改
/etc/hosts
文件将127.0.0.1
映射至主机名
如果要认证访问,还需要做如下步骤:
- 复制
jmxremote.password
文件 - 修改
jmxremote.password
和jmxremote.access
文件的权限为600即文件所有者可 读写 - 连接时填入
controlRole
(用户名),R&D
(密码)
3、线程运行的原理
3.1 栈与栈帧
Java Virtual Machine Stacks
(Java
虚拟机栈)
JVM
是由堆、栈、方法区所组成,其中当每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧
Frame
组成,对应着每次方法调用时所占用的内存 - 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 每个线程的活动栈帧互不影响,因为线程都有独立的虚拟机栈
- 共享堆上的对象才会导致线程间相互影响,这就是需要加锁/并发控制的地方
3.2 线程上下文切换(Thread Context Switch
)
因为以下一些原因导致cpu
不再执行当前的线程,转而执行另一个线程的代码:
- 线程的
cpu
时间片用完 - 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
当Context Switch
发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程池的状态,Java
中对应的概念就是程序计数器(Program Counter Register
),它的作用是记住下一条jvm
指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
Context Switch
频繁发生会影响性能,(所以不是线程池中线程越多越好,合适的数量才行)
4、常用方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start 方法只能调用一次,如果调用了多次会出现IllegalThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造Thread 对象时传递了Runnable 参数,则线程启动后会调用Rumnable 中的run 方法,否则默认不执行任何操 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的id | id唯一 | |
getName() | 获取线程名 | ||
setName() | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | ||
getState() | 获取线程状态 | Java中线程状态是用6个enum表示,分别为:NEW ,RUNNABLE ,BLOCKED ,WAITING ,TIMED_WAITING ,TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在sleep ,wait ,join 会导致被打断的线程抛出InterruptedException ,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记 ;park的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |