【Java基础】多线程编程

5 篇文章 0 订阅
5 篇文章 0 订阅

1.5并发编程

多线程编程是一种可以使程序同时执行多个任务的编程技术,以实现程序的并发和并行性,进而提高程序的性能和响应时间。

1.5.1 概念
  • 程序:就是为了完成特定的任务,用某种编程语言编写的一系列指令的集合;即一组计算机能识别的和执行的静态代码、静态对象;
  • 进程:指一个在内存中运行的应用程序;进程是操作系统分配资源的最小单位,一个进程中可以包含多个线程。
  • 线程:线程是进程的一个执行流程,是CPU调度和分派的最小单位,一个进程可以由多个线程组成,多个线程可以共享一个进程的内存空间;
  • 多线程:指的是一个程序在运行时产生了不止一个线程,产生的多个线程同时或者交替的运行;
    • 使用多线程的优点
      1. 提高程序的响应,增加用户的体验。
      2. 最大限度的利用CPU的空闲时间来处理其他任务。
      3. 提高程序的效率。注意:它不是提高运行速度,而是提高CPU的使用率,从而使程序的运行效率加快了。
      4. 系统创建进程需要为该进程重新分配系统资源,创建线程的代价则小的多,因此多任务并发时,多线程效率高。
1.5.2 创建多线程

使用多线程技术可以通过继承Thread类、实现Runnable接口、实现Callalble接口。与Runnable接口不同的是,Callable接口的call()方法可以返回结果或者抛出异常。

  • 继承Thread类创建线程,该类不能再继承其他的类,因为Java是单继承的;
  • 实现Runnable接口创建线程,该类还可以通过实现其他的类,更加完善功能;
  • 实现Callable接口,可以将该类封装到FutureTask中,将FutureTask对象传递给一个Thread对象,并启动线程。最后,使用FutureTask的get()方法获取线程执行的结果。

注意:FutureTask的get()方法是一个阻塞方法,调用该方法会等待线程执行完成再返回结果。

多线程编程中关于线程同步,目的是保证多线程在访问同一个资源时的正确性和一致性,Java中通过synchronizedLock来实现;

  • synchronized是Java内置的一种特性,可以对代码块、方法进行修饰;
  • Lock是一个接口,提供了更加广泛、更加灵活的多线程同步机制;
1.5.3 线程的生命周期
  • 传统的线程模型的五种线程状态:
    • 新建New:new Thread()创建线程实例;
    • 就绪Runnable:Thread实例调用了start()方法后线程被启动,等待CPU分配资源阶段,谁先抢到CPU资源谁开始执行;
    • 运行Running:就绪的线程被调度并获得CPU资源的时;
    • 阻塞Blocked:线程在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。
    • 死亡Dead:线程正常执行完毕或线程被提前强制性终止或出现异常结束,线程要被销毁释放资源。自然终止和异常终止。
  • JDK定义的六种线程状态:
    • NEW,
    • RUNNABLE,
    • BLOCKED,
    • WAITING,
    • TIMED_WAITING,
    • TERMINATED;
1.5.4 线程中常用方法
1.5.4.1 Thread类中
  • currentThread():返回对当前正在执行的线程对象的引用。
  • getId():返回此线程的标识符。
  • getName():返回此线程的名称。
  • getPriority():返回此线程的优先级。
  • isDaemon():判断这个线程是否是守护线程。
  • isAlive():判断当前线程是否存活。
  • interrupt():将当前线程置为中断状态。
  • sleep(long millis):使当前运行的线程进入睡眠状态,睡眠时间至少为指定毫秒数,此时线程处于阻塞状态,当时间到了之后就进入就绪状态而不是执行状态。
  • yield():放弃当前CPU资源,从运行状态进入就绪状态,让其它线程去用CPU资源。
  • join():表示等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
1.5.4.2 Object类中
  • wait():让当前线程进入等待阻塞状态,直到其他线程调用此对象的notify()或notifyAll()方法后,才被唤醒进入就绪状态

  • notify():唤醒在此对象监控器(锁)上等待的单个线程。

  • notifyAll():唤醒在此对象监控器(锁)上等待的所以线程。

注:wait()、notify()、notifyAll()都依赖于同步锁,而同步锁是对象持有的,且每个对象只有一个,所以这些方法定义在Object类中,而不是Thread类中。

  • wait():让线程从运行状态进入等待阻塞状态,会释放它所持有的同步锁
  • sleep():让线程从运行状态进入阻塞状态,不会释放它所持有的同步锁
  • yield():让线程从运行状态进入就绪状态,不会释放它所持有的同步锁
1.5.5 线程类别

Java中线程分为两类:用户线程和守护线程

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程;
  • 守护线程:运行在后台,为其他前台线程服务;也可以说守护线程是JVM中非守护线程的 “佣人”。
  • 用户线程是独立存在的,不会因为其他用户线程退出而退出。
  • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
1.5.6 线程安全问题

产生线程安全的主要原因是:存在共享资源和多个线程共同操作共享数据;就是当多个线程同时操作一个共享资源的时候,会导致一系列的问题,主要是数据不一致和数据正确性问题,这样就产生了线程安全问题,就需要我们解决线程安全问题,即使用线程同步

避免产生线程安全问题的三种解决方式:

  • 同步代码块(synchronize)
  • 同步方法
  • 锁机制(Lock)
1.5.6.1 同步代码块
synchronized(锁对象) {
    //线程安全问题的代码
}

使用同步代码块时注意:

  • 同步代码块的锁对象,可以是非null的任意对象。
  • 必须保证多个线程使用的锁对象必须是同一个。
1.5.6.2 同步方法

这种方式就是把操作共享资源的代码抽取出来,放到一个用synchronize关键字修饰的方法中。

修饰符 synchronized 返回值类型 方法名称(参数列表) {
    //方法体
}

(1)同步非静态方法

此时的锁对象不再是任意对象了,此时的锁对象是this,也就是说谁调用了方法,那么this就是谁;

(2)同步静态方法

同步静态方法和同步方法很像,就是在同步方法前面加了个static关键字,添加之后的锁对象就不再是this了,而是类对象(ClassName.class)。

总结synchronize关键字修饰不同地方的锁对象(非常重要!!!):

  • 修饰代码块,指定加锁对象,对给定对象加锁,线程进入同步代码前要获得给定对象的锁,这个锁可以是实例锁,也可是类对象锁。
  • 修饰实例方法,作用于当前实例加锁,线程进入同步代码前要获得当前实例的锁,即this锁。
  • 修饰静态方法,作用于当前类对象加锁,线程进入同步代码前要获得当前类对象的锁,即 类**.**class锁。
1.5.6.3 同步的原理

当多个线程同时对一共享数据进行操作时,只会有一个线程拿到锁对象,因为一个对象只有一把锁,当某线程获取该对象的锁后,其它线程就无法再获取该对象的锁了,所以只能等待某线程释放该对象的锁,其它线程才能访问,这样便是实现了对临界区的互斥访问,保证了共享数据的安全。当然这也是锁的竞争问题,也是死锁产生的条件。

1.5.6.4 死锁

线程同步虽然解决了线程安全的问题,但是由于线程同步满足了互斥条件,而互斥条件是产生死锁的必要条件,所以就可能出现死锁;

死锁产生的四个必要条件:

  • 互斥条件:某资源只能被一个进程使用,其它进程请求该资源时,只能等待,直到该资源被用完后释放;
  • 请求和保持条件:当某进程因为请求某资源而被阻塞时,对已经获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源在未使用完之前不能被剥夺,只能在使用完时自己主动释放;
  • 环路等待条件:在发生死锁时,必然存在一个进程申请资源的环形链;
1.5.7 线程通信

线程间通信就是当多个线程处理同一个共享资源,但它们处理的任务不同,此时就需要通过线程间通信来解决多线程之间对同一资源的使用和操作;

常见的三种线程通信方式:

  • ①、使用等待通知机制控制线程通信(synchronized + wait + notify)
  • ②、使用Condition控制线程通信(Lock + Condition + await + signal)
  • ③、使用阻塞队列控制线程通信(BlockingQueue)
1.5.7.1 等待通知机制

在Java线程通信中,等待通知机制是最传统的线程通信方式;就是在一个线程进行规定的操作后,进入等待状态,由其它线程执行完指定的任务后,将之前等待的线程唤醒;在等待通知机制中有三个非常重要的方法:wait()、notify()和notifyAll(),它们都属于Object类,在Java中默认所有的类都继承自Object类,所有所有的类都拥有这三个方法,但是由于在Object类中将三个方法都由final修饰,所以子类不能对其进行重写;

  • wait():让当前线程进入阻塞状态,直到其它线程调用此对象的notify()或者notifyAll()唤醒;
  • notify():唤醒在此对象监视锁上等待的单个线程;
  • notifyAll():唤醒在此对象监视锁上等到的所有线程;

注意:这三个方法必须在同步方法或同步块中调用,而且必须由同步监视器(锁对象)来调用,并且它们的同步监视器(锁对象)必须一致。

1.5.7.2 Condition

Condition是一个接口,其内部基本的方法就是await()、signal()和signalAll()方法。

它的作用就是用来代替传统的Object类中的wait()、notify()和notifyAll()实现线程间的通信。

相比使用Object的wait()、notify()和notifyAll(),使用Condition的await()、signal()和signalAll()这种方式实现线程间协作更加安全和高效,因此通常来说比较推荐使用Condition。

但是需要注意的是Condition依赖于Lock接口,它必须在Lock实例中使用,否则会抛出IllegalMonitorStateException。也就是说调用Condition的await()和signal()和signalAll()方法,必须在lock.lock()和lock.unlock之间才可以使用

1.5.7.3 阻塞队列

BlockingQueue也是一个接口,它继承了Queue接口。

BlockingQueue的底层实际上是使用了Lock和Condition来实现的,它帮我们搞好了一切(已经帮我们调用了lock、unlock、await和signal方法),我们只需调用BlockingQueue中合适的方法即可,所以使用BlockingQueue可以很轻松的实现线程通信。

BlockingQueue具有的特征:如果该队列已满,当生产者线程试图向BlockingQueue中放入元素时,则线程被阻塞。如果队列已空,消费者线程试图从BlockingQueue中取出元素时,则该线程阻塞

BlockingQueue提供如下两个支持阻塞的方法:

  • (1) put(E e):尝试把元素放入BlockingQueue的尾部,如果该队列的元素已满,则阻塞该线程。
  • (2) take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
1.5.8 线程池

线程池通过重用线程,减少线程创建和销毁的额外开销;

1.5.8.1 Executors静态方法

创建线程池使用Executors工具类的静态方法实现线程池的创建和管理。Executors提供了一些静态方法,以创建不同类型的线程池;

『一般不推荐使用Executors的方式来创建线程池,因为可能会出现OOM(Out Of Memory,内存溢出)的情况,下面我们依次详细的分析这四个方式:』

  • newFixedThreadPool(int nThreads)
    • 创建一个固定大小的线程池,该线程池中最多同时运行nThreads个线程
    • 当线程池中的线程都在执行任务时,如果提交了新的任务,那么这些任务将会等待,直到有空闲的线程可用。
    • 线程池中的线程是无界队列的,即没有回收的任务。
  • newCachedThreadPool()
    • 创建一个可以根据需要自动扩展和回收线程的线程池。
    • 如果线程池中有空闲的线程可用,那么新提交的任务会立即使用该空闲线程执行。
    • 如果线程池中没有空闲的线程,那么会创建一个新的线程来执行任务。
    • 线程池中的线程是有界队列的,即有一定的回收机制,如果线程在60秒内没有执行任务,则会自动回收。
  • newSingleThreadExecutor()
    • 创建一个只有一个线程的线程池。
    • 该线程池保证任务的执行顺序是按照提交的顺序执行的,也就是说任务是串行执行的。
    • 如果该线程异常退出,系统会自动创建一个新的线程来替代。
    • 线程池中的线程是无界队列的,即没有回收的任务。
    • 使用只有一个线程的线程池相较于直接创建一个线程具有更好的可复用性、线程调度控制和异常处理能力。因此,在多任务执行的场景下,使用只有一个线程的线程池更为推荐
  • newScheduledThreadPool(int corePoolSize)
    • 创建一个可以安排延迟任务和定时任务的线程池。
    • 该线程池可以执行定时任务、周期性任务以及一次性任务。
    • corePoolSize参数指定了线程池中的核心线程数,即同时处理定时任务的线程数,默认情况下线程池中没有非核心线程。
    • 线程池中的线程是无界队列的,即没有回收的任务。

推荐使用new ThreadPoolExecutor构造函数来创建线程池

1.5.8.2 ThreadPoolExecutor

在ThreadPoolExecutor这个类中提供了四个构造器,由于前三个构造器都调用了第四个构造器来完成初始化,所以着重来学习第四个构造器;

public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数
                          int maximumPoolSize,//线程池最大线程数
                          long keepAliveTime,//空闲线程的存活时间
                          TimeUnit unit,//是keepAliveTime参数的时间单位
                          BlockingQueue<Runnable> workQueue,//阻塞队列,存储等待执行的任务
                          ThreadFactory threadFactory,//线程工厂,创建新的线程
                          RejectedExecutionHandler handler)//线程饱和策略或拒绝策略。

线程池相关的一些重要参数:

  • corePoolSize:线程池的核心线程数;在创建线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来线程池才创建线程去执行任务;**当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列中;**如果缓存队列中存放的任务满了,则会继续创建新的线程来执行任务,直到创建的线程数目达到maximunPoolSize;

  • maximunPoolSize:线程池的最大线程数;表示线程池中最多能创建多少个线程,它的值肯定不能小于corePoolSize,否则运行时会抛出异常:IllegalAgumentException;

  • keepAliveTime:空闲线程的存活时间;就是当线程数量大于corePoolSize时,如果等待了keepAliveTime时长还是没有新的任务可以执行,则线程终止,直到线程池中的线程数不超过corePoolSize;

  • unit:参数keepAliveTime的时间单位,它在TimeUnit类中有7种静态属性可取;

  • workQueue:一个阻塞队列,用来存储等待执行的任务;一般来说有以下几种选择:

    • ArrayBlockingQueue:数组阻塞队列,是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序,可以指定缓存队列的大小。
    • LinkedBlockingQueue:链表阻塞队列,一个基于链表结构的无界阻塞队列,此队列按FIFO原则对元素进行排序,吞吐量通常要高于ArrayBlockingQueue,比较常用,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:同步阻塞队列
    • PriorityBlockingQueue:优先级阻塞队列,一个具有优先级的无限阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定)。
  • threadFactory:线程工厂,用于创建新的线程;threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

  • handler:线程饱和策略或拒绝策略。当线程池和队列都满了,再加入的任务会执行此策略,它有四种策略。

    • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。线程池默认的拒绝策略。
    • DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • DiscardOldestPolicy:丢弃队列最前面的任务,也就是队列头的元素,然后重新尝试执行任务(重复此过程)。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃。
    • CallerRunsPolicy:既不抛弃任务也不抛出异常,而是由调用线程的主线程来处理该任务。
1.5.8.3 线程池的工作流程

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值