Java 多线程

2、多线程

1、基本概念

1、程序、进程、线程

程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process):是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。

  • 运行中的QQ,运行中的MP3播放器;
  • 程序是静态的,进程是动态的;
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的;
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患;
  • 每个Java程序都有一个隐含的主线程:main方法。

2、并发和并行

单核CPU,其实是一种假的多线程,因为在同一个时间单元内,只能执行一个线程的任务,实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。

总结:微观串行,宏观并行。

如果是多核的话,才能更好的发挥多线程的效率,现在的服务器都是多核的。多核CPU每个核都可以调度运行线程,这时候线程可以是并行的。

并发concurrent:同一时间应对多件事情的能力;

并行parallel:同一时间动手做多件事情的能力。

举例:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发;
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待);
  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行。

3、多线程应用场合

程序需要同时处理多个任务;

比如用户输入、文件读写、网络操作或搜索等,这些任务可能需要等待。

此外,还需要一些程序在后台运行。

2、线程的创建和使用

在Java中,JVM允许程序运行多个线程,通过java.lang.Thread类实现。每个线程通过Thread对象的run()方法来完成操作,通常将run()方法的主体称为线程体。线程的启动是通过Thread对象的start()方法,而不是直接调用run()

Thread类的构造器包括:

  • Thread():创建新的Thread对象。
  • Thread(String threadname):创建线程并指定线程实例名。
  • Thread(Runnable target):指定创建线程的目标对象,该对象实现了Runnable接口中的run方法。
  • Thread(Runnable target, String name):创建新的Thread对象。

创建线程的方式有四种:

  • 继承Thread类。
  • 实现Runnable接口。
  • 实现Callable接口。
  • 使用线程池创建线程。

1、继承Thread类

步骤:

  1. 定义子类继承Thread
  2. 子类重写Thread类中的run方法
  3. 创建Thread子类对象,即创建线程对象
  4. 调用线程对象start方法(启动线程,调用run方法)

run() 和 start() 的区别?

  • start():用来启动线程,通过该线程调用 run 方法执行 run 方法中所定义的逻辑代码。start 方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

2、实现Runnable接口

步骤:

  1. 定义子类实现 Runnable 接口;
  2. 子类中重写 Runnable 接口中的 run 方法;
  3. 通过 Thread 类构造方法创建线程对象;
  4. 将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造方法中;
  5. 调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法。

优势:

  • 避免了单继承的局限性;
  • 多个线程可以共享一个接口实现类的对象,适合多个相同线程处理同一份资源。(但不是独有的)

3、Thread类的相关方法

  1. start(): 启动线程,并执行对象的 run() 方法。
  2. run(): 线程在被调用时执行的操作。
  3. getName(): 返回线程的名称。
  4. setName(): 设置线程的名称。
  5. currentThread(): 返回当前线程。

3、线程的调度和生命周期

1、线程的调度

调度策略:

  • 基于时间片轮转;
  • 抢占式:高优先级的线程抢占CPU。

调度方法:

  • 同优先级线程组成先进先出队列,使用时间片策略;
  • 对高优先级,使用优先调度的抢占式策略;
  • Java中线程优先级的范围是1~10,默认的优先级是5:
    • MAX_PRIORITY(10)
    • MIN_PRIORITY(1)
    • NORM_PRIORITY(5)

涉及的方法:

  • setPriority(int newPriority):设置线程的优先级;
  • getPriority():获取线程的优先级;
  • yield():线程让步:
    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;
    • 若队列中没有同优先级的线程,忽略此方法。
  • join():
    • 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
    • 低优先级的线程也可以获得执行
  • sleep(long millis):
    • 令当前线程在规定时间内放弃对CPU的控制,使其他进程能有机会被调用,事件结束后重排队
  • stop():强制线程生命期结束
  • isAlive():判断线程是否还活着

2、线程的生命周期

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

新建 (NEW)

  • 线程对象创建后,但未调用 start 方法时,处于新建状态。
  • 此时未与操作系统底层线程关联。

可运行 (RUNNABLE)

  • 调用 start 方法后,线程从新建状态进入可运行状态。
  • 此时与底层线程关联,由操作系统调度执行。

终结 (TERMINATED)

  • 线程内代码执行完毕后,由可运行状态进入终结状态。
  • 此时会取消与底层线程的关联。

阻塞 (BLOCKED)

  • 当获取锁失败后,线程从可运行状态进入 Monitor 的阻塞队列,处于阻塞状态,不占用 CPU 时间。
  • 当持锁线程释放锁时,会唤醒阻塞队列中的线程,使其进入可运行状态。

等待 (WAITING)

  • 当获取锁成功后,但由于条件不满足,调用 wait() 方法,线程从可运行状态释放锁进入 Monitor 的等待集合等待,同样不占用CPU时间
  • 当其它持锁线程调用 notify()notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态

有时限等待(TIMED_WAITING)

  • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入Monitor等待集合进行有时限等待,同样不占用CPU时间
  • 当其它持锁线程调用 notify()notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
  • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
  • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与Monitor无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

4、线程同步

现象:

  1. 多个窗口卖同一张票;
  2. 票的编号出现了0或者负值

问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方式:同步机制。

1、synchronized代码块

语法:

synchronized (唯一的对象){
    代码块;
}

A进程占用此 唯一对象 时,B、C…等其他进程都要在A进程完成 代码块 后释放了锁,才能挨个的执行该代码块

2、synchronized方法

语法:

synchronized public void 方法() {
        方法体;
    }

A占有了该锁,那么只有A进程完成了该方法,才轮到其他进程调用。

3、Lock锁

从JDK 5.0开始,Java引入了更强大的线程同步机制,通过显式定义同步锁对象来实现同步,这个锁对象就是 Lock

java.util.concurrent.locks.Lock 接口用于控制多个线程对共享资源的访问,确保每次只有一个线程可以对 Lock 对象加锁,从而实现对共享资源的独占访问。

ReentrantLock 类实现了 Lock 接口,它与 synchronized 关键字具有相同的并发性和内存语义,但在实现线程安全控制时,ReentrantLock 更为常用,因为它允许显式地加锁和释放锁。

4、线程安全的单例模式懒汉式

public class SingleObj {
    private static SingleObj singleObj;
    private SingleObj() {

    }
    synchronized public static SingleObj getSingleObj() {
        if (singleObj == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            singleObj = new SingleObj();
        }
        return singleObj;
    }
}

当A线程运行至创建新对象前停止,B进程进行时,obj仍为null,此时就会出现,单例设计模式无法实现的情况,懒汉式是线程不安全的。饿汉式则是安全的

5、死锁

应注意是否两个进程被彼此所限制,从而造成无法解决的死锁。

5、线程通信

方法:

  • wait() 方法让当前线程挂起并释放 CPU,同时放弃对同步资源的控制,等待其他线程调用 notify()notifyAll() 方法来唤醒。线程被唤醒后,需要重新获得对监视器的所有权才能继续执行。
  • notify() 方法用于唤醒正在等待同步资源的线程中优先级最高的那个线程,使其结束等待状态。
  • notifyAll() 方法会唤醒所有正在等待该资源的线程,让它们都结束等待状态。

这些方法只能在 synchronized 方法或 synchronized 代码块中使用。

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的生产者和消费者——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者在缓冲区添加数据之后,再唤醒消费者。

Java中waitsleep方法的不同点和共同点。

共同点

  • wait()wait(long)sleep(long) 的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。

不同点

  • 方法归属不同

    • sleep(long)Thread 的静态方法。
    • wait()wait(long) 都是 Object 的成员方法,每个对象都有。
  • 醒来时机不同

    • 执行 sleep(long)wait(long) 的线程都会在等待相应毫秒后醒来。
    • wait(long)wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去。
    • 它们都可以被打断唤醒。
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃CPU,但你们还可以用)。
    • sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃CPU,你们也用不了)。

6、JDK5.0新增创建线程的方法

1、实现Callable接口

与使用 Runnable 相比,Callable 功能更强大些

  • 相比 run() 方法,可以有返回值;
  • 方法可以抛出异常;
  • 支持泛型的返回值;
  • 需要借助 FutureTask 类,比如获取返回结果。

关于 Future 接口

  • 可以对具体 RunnableCallable 任务的执行结果进行取消、查询是否完成、获取结果等;
  • FutureTaskFuture 接口的唯一的实现类;
  • FutureTask 同时实现了 RunnableFuture 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

Runnable 和 Callable 的区别?

  • Runnable 接口的 run 方法没有返回值,Callable 接口的 call 方法有返回值,是个泛型,和 FutureFutureTask 配合可以用来获取异步执行的结果;
  • Callable 接口支持返回执行结果,需要调用 FutureTask.get() 得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞;
  • Callable 接口的 call() 方法允许抛出异常,而 Runnable 接口的 run() 方法的异常只能在内部消化,不能继续上抛。

2、使用线程池

1、存在问题及解决思路

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

好处

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理:
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止
2、线程池相关API

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
  • Future submit(Callable task):执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值