Web全栈~31.并发

上一期

进程和线程的关系

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

并行与并发

并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。

并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。

通俗的来说,并行其实就是,多个任务用多个CPU同时运行。而并发则是多个任务由一个CPU通过切换时间片运行,并发并不是真的再同时运行他们,只是给人的感觉,效果上看起来像是。

创建线程

继承Thread

Java中java.lang.Thread这个类表示线程,一个类可以继承Thread并重写其run方法来实现一个线程

public class Test extends Thread{@Overridepublic void run() {System.out.println("run");}public static void main(String[] args) {Thread thread = new Test();thread.start();}
}

Test这个类继承了Thread,并重写了run方法。run方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出受检异常。run方法类似于单线程程序中的main方法,线程从run方法的第一条语句开始执行直到结束。定义了这个类不代表代码就会开始执行,线程需要被启动,启动需要先创建一个Test对象,然后调用Thread的start方法

start表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。

//每个Thread都有一个id和name:
public class Test extends Thread{@Overridepublic void run() {System.out.println("run");System.out.println(Thread.currentThread().getId());System.out.println(Thread.currentThread().getName());}public static void main(String[] args) {Thread thread = new Test();thread.start();}
}

实现Runnable接口

通过继承Thread来实现线程虽然比较简单,但Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。Runnable接口的定义很简单,只有一个run方法。当然,仅仅实现Runnable是不够的,要启动线程,还是要创建一个Thread对象。

public class Test implements Runnable{public static void main(String[] args) {Runnable runnable = new Test();Thread thread = new Thread(runnable);thread.start();}@Overridepublic void run() {System.out.println("run");}
}

线程的基本属性和方法

id 和 name

每个线程都有一个id和name。id是一个递增的整数,每创建一个线程就加一。name的默认值是Thread-后跟一个编号,name可以在Thread的构造方法中进行指定,也可以通过setName方法进行设置,给Thread设置一个友好的名字,可以方便调试。

优先级

线程有一个优先级的概念,在Java中优先级从1到10,默认为5

public final void setPriority(int newPriority)
public final int  getPriority()

这种优先级会被映射到操作系统中的线程优先级,不过并不是所有操作系统都是10个优先级。Java中不同的优先级可能会被映射到操作系统中相同的优先级。另外,优先级对操作系统而言主要是给了一点建议和提示,并不是强制如此。

状态

线程还有一个状态的概念,Thread有一个方法用于获取线程的状态。

public State getState()
public enum State {//NEW:没有调用start的线程状态为NEW。NEW,/*RUNNABLE:调用start后线程在执行run方法且没有阻塞时状态为RUNNABLE,不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件。*/RUNNABLE,/*BLOCKED、WAITING、TIMED_WAITING:都表示线程被阻塞了,在等待一些条件*/BLOCKED,WAITING,TIMED_WAITING,//TERMINATED:线程运行结束后状态为TERMINATED。TERMINATED;
}

Thread中还有一个方法可以检查出线程是否还或者,当线程启动后,run方法运行结束前,它的返回值都是true

public final native boolean isAlive()

daemon线程

启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。

Thread有一个是否daemon线程的属性

public final void setDaemon(boolean on)
public final boolean isDaemon()

daemon一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的"hello world"类型的程序时,实际上,Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main线程结束的时候,垃圾回收线程也会退出。

sleep

Thread有一个静态的sleep方法,调用该方法会让当前线程睡眠指定的时间,单位是毫秒

public static native void sleep(long millis) throws InterruptedException;

睡眠期间,该线程会让出CPU,但睡眠的时间不一定是确切的给定毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException

yield()方法

//可以让出CPU:
public static native void yield();

这也是一个静态方法,调用该方法,是告诉操作系统的调度器:我现在不着急占用CPU,你可以先让其他线程运行。不过,这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。

join()方法

可以让调用join的线程等待该线程结束

public final void join() throws InterruptedException

在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出Interrupted-Exception。join方法还有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无期限等待

public final synchronized void join(long millis) throws InterruptedException

如果希望main线程在子线程结束后再退出,main方法可以改为

public class Test implements Runnable{public static void main(String[] args) throws InterruptedException {Runnable runnable = new Test();Thread thread = new Thread(runnable);thread.start();thread.join();}@Overridepublic void run() {System.out.println("run");}
}

共享内存

每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。

public class Test{private static int shared = 0;private static void incrShared(){shared++;}static class ChildThread extends Thread {List<String> list;public ChildThread(List<String> list) {this.list = list;}@Overridepublic void run() {incrShared();list.add(Thread.currentThread().getName());}}public static void main(String[]args) throws InterruptedException {List<String> list = new ArrayList<String>();Thread t1 = new ChildThread(list);Thread t2 = new ChildThread(list);t1.start();t2.start();t1.join();t2.join();System.out.println(shared);System.out.println(list);}
}

在这里有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法。不同执行流可以访问和操作相同的变量,比如这里面的shared和list变量。不同执行流可以执行相同的程序代码,就像这里面的incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread的执行流执行。当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。

当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题

竞态条件

当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。

public class Test extends Thread{private static int counter = 0;@Overridepublic void run() {for(int i = 0; i < 1000; i++) {counter++;}}public static void main(String[]args) throws InterruptedException {int num = 1000;Thread[]threads = new Thread[num];for(int i = 0; i < num; i++) {threads[i]= new Test();threads[i].start();}for(int i = 0; i < num; i++) {threads[i].join();}System.out.println(counter);}
}

有一个共享静态变量counter,初始值为0,在main方法中创建了1000个线程,每个线程对counter循环加1000次,main线程等待所有线程结束后输出counter的值。 期望的结果是100万,但实际执行,发现每次输出的结果都不一样,一般都不是100万,经常是99万多。为什么会这样呢?因为counter++这个操作不是原子操作

这个问题可能就需要使用synchronized关键字,或者使用显式锁、原子变量等方法来解决了

内存可见性

多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到。

public class Test extends Thread{private static boolean shutdown = false;static class My{public void run() {while(!shutdown){}System.out.println("exit hello");}}public static void main(String[]args) throws InterruptedException {new Test().start();Thread.sleep(1000);shutdown = true;System.out.println("exit main");}
}

在这个程序中,有一个共享的boolean变量shutdown,初始为false,My在shutdown不为true的情况下一直死循环,当shutdown为true时退出并输出"exit hello",main线程启动My后休息了一会儿,然后设置shutdown为true,最后输出"exit main"。 期望的结果是两个线程都退出,但实际执行时,很可能会发现HelloThread永远都不会退出,也就是说,在My执行流看来,shutdown永远为false,即使main线程已经更改为了true。

这就是内存可见性问题。在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。

解决这个问题可以使用volatile关键字或者synchronized

线程的成本

创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。此外,线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效。

如果线程中实际执行的事情比较多,这些成本是可以接受的;另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。

Web全栈~31.并发相关推荐

  1. 《Web全栈工程师的自我修养》读书笔记

    <Web全栈工程师的自我修养>读书笔记 [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://w ...

  2. php web教程视频教程下载,Web全栈 PHP+React系列视频教程下载

    Web全栈 PHP+React系列视频教程下载 课程介绍: 此套Web全栈 PHP+React系列视频教程覆盖PHP.前端和区块链应用开发三大热门职位,教程对网络基础.前端基础(HTML CSSJav ...

  3. Web全栈工程师技能树梳理

    FSE-SKILL-TREE Web全栈工程师技能树梳理 各个分支正在细化中,欢迎Star.PR. 点击链接加入群[Web全栈QQ群]:https://jq.qq.com/?_wv=1027& ...

  4. Web全栈工程师修养

    全栈工程师现在是个很热的话题,如何定义全栈工程师?在著名的问答网站Quora上有人提出了这个问题,其中一个获得了高票的回答是: 全栈工程师是指,一个能处理数据库.服务器.系统工程和客户端的所有工作的工 ...

  5. Web全栈~34.CAS

    Web全栈~34.CAS 上一期 原子变量 Java并发包中的原子变量有以下几种 AtomicBoolean:原子Boolean类型,常用来在程序中表示一个标志位. AtomicInteger:原子I ...

  6. Web全栈~25.文件

    Web全栈~25.文件 上一期 前言 Web全栈系列博客到了文件这里已经逐步的走向重点.前面的内容之所以很多都不全面,其实根本原因还是自己太懒...当然对于一些细节日后会在其他系列的博客去说 事实上我 ...

  7. 开课吧mysql课件百度云_开课吧web全栈十二期|百度云|天翼云下载

    目录:/Web全栈架构师第12期 [93.1G] ┣━━阶段10:webpack [6.2G] ┃ ┣━━10-1 01课 webpack实战 (2019.11.21).mp4 [1.7G] ┃ ┣━ ...

  8. Bootstrap实战练习---Web全栈工程师简历模版

    Web全栈工程师简历模版 head <!DOCTYPE html> <html> <head><meta charset="UTF-8"& ...

  9. 《web全栈工程师的自我修养》阅读笔记

    在买之前以为这本书是教你怎么去做一个web全栈工程师,以及介绍需要掌握的哪些技术的书,然而看的过程中才发现,是一本方法论的书.读起来的感觉有点像红衣教主的<我的互联网方法论>,以一些自己的 ...

最新文章

  1. SQUAD的rnet复现踩坑记
  2. 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术
  3. centos7下使用yum安装pip
  4. shell如何检测linux发行版本,shell判断软件版本
  5. mysql通用mapper_通用Mapper(Mybatis)
  6. python forward(10)什么意思-Python turtle.forward方法代码示例
  7. matlab计数重叠细胞,医学图像处理案例(三)——用分水岭算法分割重叠细胞
  8. 最新 IOS应用开发Icon规格自动裁剪器(C#)
  9. nginx 二 配置conf
  10. Android Path之Direction.CW、Direction.CCW
  11. 小程序转uni-app——onLoad语法转换
  12. 微信小程序iconfont不显示解决
  13. 药明海德在苏州打造疫苗CDMO服务中国基地;现代汽车将在印尼新首都启用“空中出租车” | 美通企业日报...
  14. 推荐一个好用的 所见即所得的 markdown 编辑器 Mark Text
  15. javaweb之Html/Hss/JavaScript/BootStrap小结
  16. Java引用界的四大天王
  17. 善用 GOOGLE -- 从入门到精通
  18. IDEA更换主题背景为黑色和设置背景图片并设置字体大小
  19. 【高效办公】批量生成指定文件名文件夹
  20. wince7 屏幕控制_WinCE下如何调用触摸屏校准程序

热门文章

  1. 《Microduino实战》——2.5 Microduino传感器系列
  2. 能够1年涨薪2次的软件测试工程师,他到底强在哪里?
  3. Linux系统怎么复制文件夹下的全部文件到另外文件夹?
  4. Nginx配置详情-配置说明-参数优化(一)
  5. win10彻底关闭休眠状态(1909以上版本)
  6. 单片机设计:基于stm32智能语音识别蓝牙音响(ld3320语音识别模块+mp3模块+喇叭+点阵屏+OLED+蓝牙+手机app)
  7. Linux--原子操作(介绍及其操作函数集)
  8. ArcGIS DEM数字高程模型数据的生成
  9. cout 和cerr的区别
  10. 数据库锁的概念与介绍