Java 线程

简介: Java 线程

一  简述

  1. 是指从软件或者硬件上实现多个线程并发执行的技术。
  2. 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
  3. 多线程技术就是同时执行多个应用程序,多线程技术需要硬件的支持

二  概念

  1. 并行:在同一时刻,有多个指令在多个CPU上同时执行
  2. 并发:在同一时刻,有多个指令在单个CPU上交替执行
  3. 进程:是正在运行的软件
  1. 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
  2. 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
  3. 并发性:任何进程都可以同其他进程一起并发执行
  1. 线程:是进程中的单个顺序控制流,是一条执行路径(线程就是进程里面做的事)
  1. 单线程:一个进程如果只有一条执行路径,则称之为单线程程序
  2. 多线程:一个进程如果有多条执行路径,则称之为多线程程序

三  实现方法

  1. 继承Thread类
  1. 定义一个类MyThread继承Thread类
  2. 在MyThread中重写run()方法
  3. 创建MyThread对象
  4. 启动线程 start()方法
public class Test {
    public static void main(String[] args) {
        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        //启动线程
        t1.start();
        t2.start();
    }
}
class MyThread extends Thread{
    //run中代码,为线程在开启后执行的代码
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("线程开启了" + i);
        }
    }
}
  1. 运行结果:俩个线程之间的运行顺序由CPU决定,不存在固定顺序image.png
  2. 为什么要重写run()方法?因为run()方法是用来封装被线程执行的代码
  3. run()与start()方法的区别?run()方法:封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。 start()方法:启动线程,然后由JVM调用此线程的run()方法。
  1. 实现Runnable接口
  1. 定义一个类MyRunnable实现Runnable接口
  2. 在MyRunnable中重写run()方法
  3. 创建MyRunnable对象
  4. 创建Thread类对象,把MyRunnable对象作为构造方法的参数
  5. 启动线程
public class Test {
    public static void main(String[] args) {
        //创建线程对象,通过实现Runnable接口的对象作为参数
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());
        //启动线程
        t1.start();
        t2.start();
    }
}
class MyRunnable implements Runnable{
    //run中代码,为线程在开启后执行的代码
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("线程开启了" + i);
        }
    }
}
  1. image.png
  1. Callable与FutureTask
  1. 定义一个类MyCallable实现Callable<>接口,注意:Callable接口存在泛型表达式,其泛型定义的类为 call()方法中的返回值类型
  2. 在MyCallable类中重写call()方法(与run()方法类似)但是call方法存在线程结束的返回值
  3. 创建MyCallable类的对象
  4. 使用FutureTask<>类创建对象,参数为MyCallable类的对象,泛型类与Callable相同。在FutureTask中存在get()方法,用来接收线程结束的返回值。注意:在一段代码开始运行后:先使用main线程调用main方法,如果使用get那么需要对应线程结束运行后才能得到结果,否则将死等结果,造成代码无法停止
  5. 创建Thread对象,把FutureTask<>类对象作为构造方法参数
  6. 启动线程
  7. 注意:在使用是需要抛出异常
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test {
    public static void main(String[] args) throws Exception {
        //创建MyCallable类对象
        MyCallable mc = new MyCallable();
        //通过MyCallable类对象创建FutureTask类对象
        FutureTask<String> ft1 = new FutureTask<>(mc);
        FutureTask<String> ft2 = new FutureTask<>(mc);
        //通过FutureTask对象创建Thread类对象
        Thread t1 = new Thread(ft1);
        Thread t2 = new Thread(ft2);
        //启动线程
        t1.start();
        t2.start();
        //输出线程结束的结果
        System.out.println(ft1.get());
        System.out.println(ft2.get());
    }
}
class MyCallable implements Callable<String> {
    //call中代码,为线程在开启后执行的代码,与run方法不同,其存在返回值
    public String call() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程开启了" + i);
        }
        return "MyCallable类";
    }
}
  1. image.png
  1. 三种方法的比较:其中只有实现Callable接口时,在结束线程时有放回值

优点

缺点

实现Runnable

或Callable接口

扩展性强,实现该接口的同时

还可以继承其他类

编程相对复杂,不能直接

使用Thread类中的方法

继承Thread类

编程比较简单,可以直接使用

Thread类中的方法

可扩展性较差,

不能继承其他类

四  Thread类常用方法

1  String getName(); 返回线程的名字
    注:线程有默认的名字,格式:Thread-编号(没有设置名字的线程,启动时编号由0开始,逐步加一)
2  void setName(String name); 将此线程的名称更改为参数name
    注:通过构造方法也可以设置线程名字,但如果使用继承与Thread类的子类时,
    需要在子类中重写带参构造的方法,因为子类的构造方法需要写入super关键字调用父类的构造方法。
    而不会默认继承
3  public static Thread currentThread(); 返回对当前正在执行的线程对象的引用
    注:使用场景在继承接口时,其没有继承Thread类则不能使用getName()方法,
    可以先使用currentThread获得对象,再使用getName() :Thread.currentThread().getName()
4  public static void sleep(long time); 让线程休眠指定的时间,单位为毫秒
    注:其为类方法,单个对象调用只会使得该类休眠
    在继承Thread类和实现接口的方法中,使用sleep()方法,必须使用try-catch解决异常,不能使用throw
5  public final void setDaemon(boolean on); 设置为守护线程
    注:当普通线程结束时,守护线程也会随之结束

五  多线程的实现

  1. 线程调度
  1. 多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务
  2. 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  3. 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
  1. Java使用的是抢占式调度模型
1  public final void setPriority(int newPriority); 设置线程的优先级
2  public final int getPriority(); 获取线程的优先级
注:优先级 1 - 10;默认值为 5
优先级越高,只是抢夺到CPU的执行权的机率更高,不是绝对的
  1. 线程生命周期:image.png

六  线程的安全问题

  1. 案例:“卖票”,需求:一共100张票,有3个卖票窗口,模拟卖票系统
  1. 错误思路,使用继承Thread有局限性,因为:需要创建3次对象,相当于3个参数对象没有达到预期效果:代码如下:
public class Test {
    public static void main(String[] args) throws Exception {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//创建Ticket类,实现多线程
class Ticket extends Thread{
    //票数
    private int ticket = 100;
    public void run(){
        while (true){
            if (ticket == 0){
                System.out.println("票买完了");
                break;
            }
            else {
                ticket--;
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票");
            }
        }
    }
}
  1. 效果图:从图中可以看出,实际上三个窗口并没有关联到一起,各卖各的,相当于一共卖了300张票image.png
  2. 解决方法:使用Runable接口,使得创建的3个线程,可以共用一个参数或者将共享对象使用继承Thread类设置为static类型:private static int ticket
public class Test {
    public static void main(String[] args) throws Exception {
        //创建实现Runnable接口的对象
        Ticket ticket = new Ticket();
        //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//创建Ticket类,实现多线程
class Ticket implements Runnable{
    //票数
    private int ticket = 100;
    public void run(){
        while (true){
            if (ticket == 0){
                System.out.println("票买完了");
                break;
            }
            else {
                ticket--;
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票");
            }
        }
    }
  1. 效果图:虽然打印的次序与顺序不一,但是三者相关联,总票数为100,但同时也存在着出现重复票的问题,在下面代码中通过阻塞方法sleep进行分析image.png
  1. 若在程序中加入时间延时,在出票时,添加100毫秒延时,使用sleep方法,try-catch解决异常
public class Test {
    public static void main(String[] args) throws Exception {
        //创建实现Runnable接口的对象
        Ticket ticket = new Ticket();
        //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//创建Ticket类,实现多线程
class Ticket implements Runnable{
    //票数
    private int ticket = 100;
    public void run(){
        while (true){
            if (ticket <= 0){
                System.out.println("票买完了");
                break;
            }
            else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票");
            }
        }
    }
}
  1. 结果演示:出现很多相同票,并且出现负票image.png
  2. 结果分析:在代码运行期间,任何时候3个线程都可能出现CPU的抢占使用,并不是一个线程对象在一次代码完整结束后,才会进行下一个代码。这样就会导致出现,同时对票数 private int ticket 的操作,导致运行结果问题的产生。延迟100毫秒,使得问题可以被放大化。
  1. 问题解决
  1. 问题分析:多线程同时操作共享数据导致
  2. 解决思路:使多线程不能同时对共享数据操作,将程序锁起来,即使获得了CPU使用权,若已有线程进行操作,则该线程仍不能使用代码
  3. 实现方式:将操作共享数据的多条代码锁起来,让任意时刻只能有一个线程执行。Java中提供了同步代码块的方式来解决
  4. 同步代码块:锁多条语句的代码块,可以使用同步代码块实现:
synchronized(任意对象/锁对象){
    多条语句操作共享的数据代码
}
  1. 该代码锁:默认情况下是打开的  ,但只要有一个线程进去执行代码了,锁就会关闭,当线程执行完,锁才会自动打开
  2. 同步/锁的好处:解决了多线程的数据安全问题
  3. 同步/锁的弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
  4. 代码演示:
public class Test {
    public static void main(String[] args) throws Exception {
        //创建实现Runnable接口的对象
        Ticket ticket = new Ticket();
        //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//创建Ticket类,实现多线程
class Ticket implements Runnable {
    //票数
    private Integer ticket = 100;
    //创建对象作为锁
    private Object obj = new Object();
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj) { //锁对象
                if (ticket <= 0) {
                    System.out.println("票买完了");
                    break;
                } else {
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票");
                }
            }
        }
    }
}
  1. 效果图:可以看出,没有出现同一票俩次购买的问题,同时打印顺序也与票数相对应image.png
  2. 注意事项只将操作到共享数据的代码放入 synchronized同步块中,(sleep代码放在synchronized()方法外,因为sleep若在synchronized中使用,则该线程休眠时,不会自动释放锁,导致其他线程无法操作)同时synchronized()的参数为一个锁对象,要确保多个线程使用的是同一把锁(即同使用同一个对象作为参数
  3. 若使用的是继承Thread类的方法实现多线程时,一定要确保共享数据,以及锁全是静态数据,才能保证在new一个新对象时,不会创建新的共享数据与锁
//静态票数
private static Integer ticket = 100;
//静态接口
private static Object obj = new Object();
  1. 这是同时new三个对象
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
  1. 使用的仍然时共享数据与锁,可以达到多线程要求
  1. 同步方法:
  1. 格式
修饰符 synchronized 返回值类型 方法名(参数列表){方法体}
  1. 与同步代码块区别:
  1. 同步代码块可以锁住指定的代码块,同步方法则是锁住所有的代码
  2. 同步代码块可以指定锁的对象(参数),同步方法不能指定锁对象
  1. 同步方法的锁是什么?以该类的对象作为锁相当于代码块 synchronized (this)
  2. 特殊:使用静态同步方法时,锁对象为:类名.class,当前类字节码文件对象
  1. Lock锁
  1. 因为使用synchronized方法的锁是自动加锁和释放锁,为了获得更广泛的锁功能java提供了Lock锁
1  void lock(); 获得锁
2  void unlock(); 释放锁
  1. Lock是接口不能被实例化,可采用它的实现类ReentrantLock来创建对象
  2. 代码演示:
synchronized(任意对象/锁对象){
    多条语句操作共享的数据代码
}
转化为:
//创建ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
//上锁
lock.lock();
多条语句操作共享的数据代码
//释放锁
lock.unlock();
  1. 为了防止代码中间报错,而没有释放锁,可将unlock()放入,finally中
    ReentrantLock lock = new ReentrantLock();
    public void run() {
        while (true) {
            //解决sleep使用try - catch - finally
            try {
                Thread.sleep(1000);
                lock.lock();
                多条语句操作共享的数据代码
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //在finally中释放锁,确保锁的释放
                lock.unlock();
            }
        }
    }
}
  1. 死锁:
  1. 概念:由于俩个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往CPU执行
  2. 建议:不要写锁的嵌套,防止死锁发生


目录
相关文章
|
27天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
3天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
26 6
|
18天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
16天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
11天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
11天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
32 3
|
12天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
58 6
|
16天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
26 2