Java学习笔记(六):多线程

多线程

一、线程概述

1.程序、进程、线程

  1. 程序(program):静态的指令集合
  2. 进程(process):运行中的程序
    • 独立性:独立地进行资源分配和调度
    • 动态性:活动的指令集合,拥有自己的生命周期和不同状态
    • 并发性:多个进程可在单个处理器上并发执行,多个进程之间不会相互影响
  3. 线程(thread):独立并发的执行流
    • 线程是进程的组成部分,一个进程可以拥有一到多个线程,一个线程必须有一个父进程
    • 线程拥有自己的堆栈、程序计数器、局部变量,但不拥有系统资源。多个线程间共享父进程的全部资源
    • 线程是独立运行的,它并不知道进程中是否有其他线程存在。线程是抢占式的,当前运行的线程在任何时候都可能被挂起,以便另外一个线程运行
    • 多个线程之间可以并发执行,一个线程可以创建和撤销另一个线程
    • 一个程序运行后至少有一个进程,一个进程里至少有一个线程
  4. 并发与并行
    • 并行:同一时刻,有多条指令在多个处理器上执行
    • 并发:同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行

2.多线程的优势

  • 进程之间不能共享内存,但线程之间共享内存十分容易
  • 系统创建进程需要重新为其分配系统资源,但创建线程代价小很多,因此使用多线程比多进程效率高

二、线程的创建和使用

1.继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
  • 创建Thread子类的实例,即创建了线程实例
  • 调用线程对象的start()方法来启动该线程
  • Java程序运行时自带默认的主线程,main()方法的方法体就是主线程的线程执行体
  • 使用继承Thread类来创建线程类时,多个线程之间无法共享线程对象的实例变量

2.实现Runnable接口创建线程类

  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  • 创建Runnable实现类的实例,并以此作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象
  • 调用线程对象的start()方法来启动该线程
  • Runnable实现类的对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体。实际的线程对象依然是Thread实例,只是该Thread线程负责执行其targetrun()方法
  • 使用实现Runnable接口来创建线程类时,多个线程之间可以共享一个target,所以多个线程可以共享同一个target对象的实例变量

3.使用Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例
  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
  • Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可以使用Lambda表达式创建Callable对象

4.继承方式和实现方式的异同

  • 推荐使用实现Runnable接口、Callable接口的方式来创建多线程
  • 实现方式的特点:
    • 只是实现了Runnable接口或Callable接口,还可以继承其他类
    • 多个线程可以共享一个target,适用于多个相同线程处理同一份资源的情况
    • 如果要访问当前线程,只能使用Thread.currentThread()方法
  • 继承方式特点:
    • 已经继承了Thread类,不能再继承其他类
    • 如果要访问当前线程,可以使用this引用

5.线程的调度

  • 同优先级线程,使用时间片策略
  • 高优先级线程,使用抢占式策略
  • 线程优先级,低优先级只是获得调度的概率低,并非一定是在高优先级线程后才被调用,子线程会继承父线程优先级
    • MAX_PRIORITY:10
    • MIN_PRIORITY:1
    • NORM_PRIORITY:5

6. 线程生命周期

  • 在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程启动以后,CPU需要在多条线程之间切换,于是线程状态也会在运行、就绪之间切换

  • 新建:程序使用new关键字创建一个线程后,该线程就处于新建状态,仅仅由JVM为其分配了内存,初始化了成员变量

  • 就绪:线程对象调用了start()方法后,该线程就处于就绪状态,JVM为其创建方法调用栈和程序计数器,表示该线程可以运行了

  • 运行:线程对象获得了CPU资源后,该进程就处于运行状态

  • 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并终止自己的执行,进入阻塞状态。被阻塞线程的阻塞解除后,线程进入就绪状态

  • 死亡:线程会以如下三种方式结束,结束后就处于死亡状态

    • run()call()方法执行完成,线程正常结束
    • 线程抛出一个未捕获的ExceptionError
    • 程序调用该线程的stop()方法来结束该线程——容易导致死锁,不推荐使用
  • 主线程死亡时,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,它就拥有和主线程相同地地位,它不受主线程的影响

  • 不能对死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次效用start()方法也是错误的,都会引发IllegalThreadStateException异常

7.线程同步

  1. synchronized锁
  • 当两个进程并发修改同一个文件时就有可能造成异常,Java引入了同步监视器来解决这个问题,同步监视器就是synchronized锁。
    • Java程序运行时所有的对象都存储在JVM中,而在JVM中所有对象都可以作为内置锁对象
    • synchronized修饰的不论是方法还是代码块,想要执行其中的内容,必须先获取对应的内置锁才行,因此synchronized的锁定对象不能是null
    • synchronized方法不能阻止其他线程通过普通方法去访问其内置锁对象,所以应将所有涉及修改同一对象的操作都用synchronized修饰
  1. 同步代码块和同步方法
  • 同步代码块就是用synchronized修饰的代码块,同步方法就是使用synchronized修饰的方法。synchronized关键字只能修饰代码块和方法
  • synchronized修饰代码块的时候可以获取内置锁的对象包括thisXXX.class以及其他对象。this指向的对象本身,XXX.class指向的是对象的类型类
  • synchronized修饰的方法时候可以获取的内置锁的对象包括thisXXX.class,修饰普通方法时获取的内置锁是调用该方法的对象本身,修饰静态方法时获取的内置锁是调用该方法的对象的类型类
  • 不要对线程安全类的所有方法都进行同步,只对哪些会改变竞争资源的方法进行同步
  • 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本,在单线程环境中使用线程不安全版本保证性能,在多线程环境中使用线程安全版本
  1. 释放同步监视器的锁定
  • 线程无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定
    • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
    • 当前线程在同步方法、同步代码块中遇到break、return终止了该代码块、该方法的继续执行
    • 当前线程在同步方法、同步代码块中遇到未处理的Error或Exception,导致代码块、方法异常结束
    • 当前线程在同步方法、同步代码块中执行了wait()方法,当前线程暂停,并释放同步监视器
  • 在以下情况中,线程不会释放同步监视器
    • 线程在执行同步方法、同步代码块时,程序调用Thread.sleep()Thread.yiled()来暂停当前线程的执行,当前线程不会释放同步监视器
    • 线程在执行同步方法、同步代码块时,程序调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器
  1. Lock锁
  • Java5开始,Java提供了功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁对象为Lock对象
  • 在实现线程同步的方法中,常用的是ReentrantLock锁(可重入锁),可以显式地加锁、释放锁。ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法
  • 使用ReentrantLock锁时,若加锁和释放锁出现在不同的作用范围内时,建议使用finally块来确保在必要时释放锁
  • Lock是显式锁,只有代码块锁,synchronized是隐式锁,有代码块锁和方法锁
  1. 死锁
  • 当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机么有监测,也没有采取措施来处理死锁

三、线程通信

1.传统的线程通信

  • 使用synchronized锁时,为了实现线程通信,Object类提供了wait()notify()notifyAll()三个方法,这三个方法必须由同步监视器对象来调用。分为以下两种情况:
    • synchronized修饰的方法:可以在方法中直接使用这三个方法
    • synchronized修饰的代码块:必须使用同步监视器对象来调用这三个方法

2.使用Condition控制线程通信

  • 使用Lock锁时,为了实现线程通信,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。Conditon实例必须被绑在一个Lock对象上,要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可
  • Condition类提供了如下三个方法:await()signal()signalAll()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值