1.为什么要使用并发编程
- 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
- 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
2.进程与线程
2.1 进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备。进程就是用来加载指令,管理内存,管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这是就开启了一个进程。
- 进程就可以是为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本,画图,浏览器等),也由的程序只能启动一个实例进程(例如网易云音乐,360安全卫士等)
线程
- 一个进程之内可以分为一到多个线程
- 一个线程就是一个指令流,将指令流的一条条指令以一定的顺序交给CPU执行
- java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
- 进程拥有共享的资源,如内存空间等,但其内部的线程共享
- 进程通信较为复杂:同一台计算机的进程通信称为IPC;不同计算机间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
- 线程通信相对简单,一位内它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般要比进程上下文切换底
2.2 并行与并发
单核cpu下,线程实际还是串行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结一句话就是:微观串行,宏观并行。
引用Rob Pike的一段描述
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
- 并行(parallel)是同一时间动手做(doing)多件事情的能力。
例子
- 家庭主妇做饭,打扫卫生,给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另外一个就得等待)
- 雇了3个保姆,一个专门做饭,一个专门打扫卫生,一个专门喂奶,互不干扰,这时是并行。
2.3应用
3.java线程
3.1 创建和运行线程
方法一:直接使用Thread
//创建线程对象
Thread t = new Thread(){
@Override
public void run() {
//要执行的任务
System.out.println("do some thing");
}
};
//启动线程
t.start();
方法二:使用Runnable配合Thread
把【线程】和任务(要执行的代码)分开
- Thread代表线程
- runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
@Override
public void run() {
//要执行的任务
System.out.println("do some thing");
}
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();
java8精简
//java8方式
Runnable runnable = ()-> System.out.println("do some thing");
//创建线程对象:参数1是任务对象,参数2是线程名称
Thread t = new Thread(runnable,"t2");
//启动线程
t.start();
原理之Thread与Runnable的关系
- 方法1是把线程和任务合并在了一起,方法2是把线程和任务分开了
- 用runnable更容易与线程池等高级API配合
- 用runnable让任务类脱离了Thread继承体系,更灵活。
方法三:FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理返回结果的情况
//创建任务对象
FutureTask<Integer> task = new FutureTask<>(()->{
System.out.println("hello");
return 100;
});
//创建线程对象:参数1是任务对象,参数2是线程名称
Thread t = new Thread(task,"t3");
//启动线程
t.start();
//主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
System.out.println("结果是:"+result);
3.2 观察多个线程同时运行
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
pom.xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
@Slf4j(topic = "c.TestMultiThread")
public class TestMultiThread {
public static void main(String[] args) {
new Thread(() -> {
while (true){
log.debug("running");
}
},"t1").start();
new Thread(()->{
while (true){
log.debug("running");
}
},"t2").start();
}
}
3.3 查看线程和进程
3.4 原理之线程运行
栈与栈帧
jvm中由堆,栈,方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(thread context switch)
- 因为以下原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 更高优先级的线程需要运行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法程序
当context switch发生时需要由操系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念就是程序计数器,她的作用是记住下一条jvm指令的执行地址,是线程私有
- 状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等
- context switch频繁发生会影响性能
3.5 常见方法
3.6 run与start
3.7 sleep与yield
sleep
- 调用sleep会让当前线程从running进入timed warting状态
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出interruptedException
- 睡眠结束的线程未必会立刻得到执行
- 建议用timeUnit的sheep代替Thread的sleep来获得更好得可读性
yield
- 调用yield会让当前线程从running进入runnable状态,然后调度执行其它同优先级得线程。如果这时没有同优先得线程,那么不能保证让当前线程暂停的效果。
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用
sleep实现
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以用yield或sleep来让出cpu的使用权给其它程序
while (true){
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用wait或条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
3.8 join方法详解
为什么需要join
下面的代码执行,打印r是什么?
public class JoinDemo {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
System.out.println("开始");
Thread t1 = new Thread(()->{
System.out.println("开始");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束");
r=10;
});
t1.start();
// t1.join();
System.out.println("结果为:"+r);
System.out.println("结束");
}
}
分析:
- 因为主线程和线程t1是并行执行的,t1线程需要1秒后才能算出r=10
- 而主线程一开始就要打印r的结果,所以只能打印出r=0
解决方法
- 用sleep行不行?为什么?
- 用join,加在t1.start()之后即可
3.9 interrupt方法详解
3.10 不推荐的方法
3.11 主线程与守护线程
3.12 线程五种状态
3.13 线程的六种状态
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() { //NEW 没有start()
log.debug("running...");
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() { //RUNNABLE 一直在运行
while (true){
}
}
};
t2.start();
Thread t3 = new Thread("t3"){
@Override
public void run() { //TERMINATED 运行完结束了
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4"){
@Override
public void run() { //TIMED_WAITING 有时间的等待中
synchronized (TestState.class){
try {
Thread.sleep(1000000);
}catch (InterruptedException E){
E.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5"){
@Override
public void run() { //WAITING 一直等待着t2运行完
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6"){
@Override
public void run() { //BLOCKED 获取锁,但是没获取到
synchronized (TestState.class){
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
log.debug("t1 state {}",t1.getState());
log.debug("t2 state {}",t2.getState());
log.debug("t3 state {}",t3.getState());
log.debug("t4 state {}",t4.getState());
log.debug("t5 state {}",t5.getState());
log.debug("t6 state {}",t6.getState());
}
}
3.14 习题
引用之统筹
阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案。
小结
参考:https://www.bilibili.com/video/BV1jE411j7uX