知识屋:更实用的电脑技术知识网站
所在位置:首页 > 科技

java并发编程(一)基础知识

发表时间:2022-03-25来源:网络

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 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

%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n

pom.xml

org.projectlombok lombok 1.18.12 ch.qos.logback logback-classic 1.2.3 @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://blog.csdn.net/java_shm/article/details/108765263

阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案。

 

 

小结

 

申明:内容来自网络,仅供学习使用
参考:https://www.bilibili.com/video/BV1jE411j7uX

收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜