多线程知识

一、多线程的基本概念

1、进程与线程的区别和联系

进程:进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程;
线程:是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。通俗理解:例如你打开微信就是打开一个进程,在微信里面和好友视频聊天就是开启了一条线程。
两者之间的关系:
一个进程里面可以有多条线程,至少有一条线程。
一条线程一定会在一个进程里面。

2、创建线程的2种方法

方式1:继承java.lang.Thread类,并覆盖run()方法。优势:编写简单;劣势:无法继承其他父类

方式2:实现java.lang.Runnable接口,并实现run()方法。优势:可以继承其他类,多线程可以共享同一个Thread对象;劣势:编程方式稍微复杂,如需访问当前线程,需调用Thread.currentThread()方法

public class MyThread implements Runnable{
    private String name;
    public MyThread(String name){
    	this.name = name;
    }
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(name);
	}
	public static void main(String[] args) {
		new Thread(new MyThread("123")).start();
	}
}

3、Java创建线程后,调用start()方法和run()的区别

两种方法的区别

1) start:

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

2) run:

run()方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

两种方式的比较 :

实际中往往采用实现Runable接口,一方面因为java只支持单继承,继承了Thread类就无法再继续继承其它类,而且Runable接口只有一个run方法;另一方面通过结果可以看出实现Runable接口才是真正的多线程。
4、并发,并行的区别

并发:同一时间段内交替运行多个进程(线程);
并行:同一时刻运行多个进程(线程)。很明显,只有多处理器才能支持。

并发就像我们的大脑思考一样,同一个时刻只能想一件事,但是在很短的一个时间段内我们可以三心二意。当然如果你长了几个脑袋,那你就可以并行思考了。
5、同步与异步,阻塞与非阻塞方式

下面介绍同步,异步,阻塞,非阻塞这几个概念,加深对多线程编程的理解。
  有了之前的概念,我们可以想象,当几个线程或者进程在并发执行时,如果我们不加任何干预措施,那么他们的执行顺序是由系统当时的环境来决定的,所以不同时间段不同环境下运行的顺序都会不尽相同,这便是异步(有差异的步骤)。当然,同步肯定就是通过一定的措施,使得几个线程或者进程总是按照一定顺序来执行(总是按照相同的步骤)。
  当一个进程或者线程请求某一个资源而不得时,如I/O,便会进入阻塞状态,一直等待。scanf()便是一个很好的例子,当程序运行到scanf()时,如果输入缓存区为空,那么程序便会进入阻塞状态等待我们从键盘输入,这便是以阻塞的方式调用scanf()。通过一定方法,我们可以将scanf()变成非阻塞的方式来执行。如给scanf()设置一个超时时间,如果时间到了还是没有输入那么便跳过scanf(),这个时候我们就称为用非阻塞的方式来调用scanf()。
  对比可以发现,同步即阻塞。想要按照某特定顺序来执行一系列过程,在上一个过程完成之前下一个过程必须等待,这就是阻塞在了这个地方。当同步运行的时候,会等待同步操作完成才会返回,否则会一直阻塞在同步操作处。
  相反的,异步即非阻塞,当异步调用某个函数时,函数会立刻返回,而不会阻塞在那。

怎么判断异步操作是否已经完成?通常有3种方式:

  1. 状态:异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成;

  2. 通知:异步操作完成会给调用者发送特定信号;

  3. 回调:异步操作完成时会调用回调函数。

所以同步即阻塞,异步即非阻塞。
  
6、线程阻塞的常见情况

  1. 调用sleep()进入睡眠状态,释放CPU,不释放锁;

  2. 用wait()暂停了线程,释放CPU,释放锁。收到notify()或notifyAll()唤醒线程;详细见:https://mp.youkuaiyun.com/postedit/81045401

  3. 线程正在等待一些IO操作;

  4. 线程正在试图调用被锁起来了的对象。

二、线程的生命周期

线程在一定条件下,状态会发生变化。线程一共有以下几种状态:

  1. 新建状态(New):新创建了一个线程对象;

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得;

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序
    代码;

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

  5. 死亡状态(dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
    在这里插入图片描述

三、多线程与单线程

1、多线程与单线程的区别

单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解。

多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。

多线程和传统的单线程在程序设计上最大的区别在于,由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,由此带来的线程调度,同步等问题。
  
线程调度:计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。

2、多线程是否一定比单线程效率高?

一提到多线程一般大家的第一感觉就是可以提升程序性能,在实际的操作中往往遇到性能的问题,都尝试使用多线程来解决问题,但多线程程序并不是在任何情况下都能提升效率,在一些情况下恰恰相反,反而会降低程序的性能。对于单核CPU计算密集型任务,多线程反而并不能带来效率的提升。线程本身由于创建和切换的开销,采用多线程不会提高程序的执行速度,反而会降低速度。但是对于频繁IO操作的程序,多线程可以有效的并发。对于包含不同任务的程序,可以考虑每个任务使用一个线程。这样的程序在设计上相对于单线程做所有事的程序来说,更为清晰明了,比如生产、消费者问题。

在实际的开发中对于性能优化的问题需要考虑到具体的场景来考虑是否使用多线程技术。

四、为什么要使用多线程

1、什么时候使用多线程?

线程必然不是越多越好,线程切换也是要开销的,当你增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,这才叫物有所值。

什么时候该使用多线程呢?这要分四种情况讨论:

  1. 多核CPU——计算密集型任务。此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置;

  2. 单核CPU——计算密集型任务。此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作;

  3. 单核CPU——IO密集型任务,使用多线程还是为了人机交互方便;

  4. 多核CPU——IO密集型任务,这就更不用说了,跟单核时候原因一样。

2、什么时候不使用多线程?

知道什么情况下不使用并发同样重要。从根本上来说,不使用并发的唯一原因就是并发带来的效益小于它带来的代价。在许多情况下,使用并发会使代码难以理解,编写、维护并发代码需要更多的脑力成本,并发带来的复杂性可能会增加bug。除非并发带来性能的提升足够打,或者模块划分足够清楚,否则不要使用并发。

使用并发带来性能上的提升可能不如预期。并发编程也需要额外的开销,在创建一个线程时,系统要分配内核资源、栈空间,然后把新线程加入到任务队列。如果线程运行时间小于线程的创建时间,这时使用多线程可能会使性能变差。

进一步来说,线程资源是有限的。如果同时有太多线程,会占用太多系统资源,会使整个系统变慢。使用太多线程会消耗尽内存或处理器的地址空间,因为线程需要独立的栈空间。

在C/S架构下,如果为每个连接创建一个线程,在连接比较少时性能很好,但是如果要同时处理太多连接的话会创建太多线程。这时使用线程池(thread pools)可以提高性能。在Linux下可以使用I/O多路复用:select、poll、epoll。

线程的切换也需要时间,如果线程切换时间比线程运行时间还长,就会降低整体性能。

五、多线程优缺点

1、优点

  1. 提高CPU的使用率:例如朋友圈发表图片,当你上传9张图片的时候,如果开启一个线程用同步的方式一张张上传图片,假设每次上传图片的线程只占用了CPU 1%d的资源,剩下的99%资源就浪费了。但是如果你开启9个线程同时上传图片,CPU就可以使用9%的资源了;

  2. 提高程序的工作效率:还是拿朋友圈发表图片来说,假设开启一个线程上传一张图片的时间是1秒,那么同步的方式上传9张就需要9秒,但是你开启9个线程同时上传图片,那么就只需要1秒就完成了。

2、缺点

  1. 如果有大量的线程,会影响性能,因为CPU需要在它们之间切换;

  2. 更多的线程需要更多的内存空间;

  3. 多线程操作可能会出现线程安全或者死锁等问题。

六、线程安全及解决方法

线程安全:简单的来说,就是在多个线程访问一个类的时候,该类始终保持着正确的执行行为。(线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。)

1、线程安全出现的根本原因

  1. 存在两个或者两个以上的线程对象共享同一个资源;

  2. 多线程操作共享资源代码有多个语句。

2、线程安全的解决方法

  1. 无状态对象永远是线程安全的;

  2. 原子性与竞争条件:原子性,顾名思义,指的就是不可分割的操作,多个操作要么一起执行,要么就都不执行。原子性保证了程序的执行不会因为执行的时序问题而引发的线程安全问题。而经常引起原子性问题的就是竞争条件。比如常见的检查再运行(check-then-act),当我创建一个文件夹时,会先判断文件夹是否存在,不存在再创建。这个在单线程的情况下不会出现问题,但是在多线程下,就可能会因为当我检查文件夹不存在后,另一个线程先创建了该文件夹,从而导致此线程创建文件错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值