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. 建议:不要写锁的嵌套,防止死锁发生


目录
相关文章
|
6天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
17天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
79 6
【Java学习】多线程&JUC万字超详解
|
2天前
|
Java
深入理解Java中的多线程编程
本文将探讨Java多线程编程的核心概念和技术,包括线程的创建与管理、同步机制以及并发工具类的应用。我们将通过实例分析,帮助读者更好地理解和应用Java多线程编程,提高程序的性能和响应能力。
15 4
|
11天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
2天前
|
安全 Java 调度
Java 并发编程中的线程安全和性能优化
本文将深入探讨Java并发编程中的关键概念,包括线程安全、同步机制以及性能优化。我们将从基础入手,逐步解析高级技术,并通过实例展示如何在实际开发中应用这些知识。阅读完本文后,读者将对如何在多线程环境中编写高效且安全的Java代码有一个全面的了解。
|
10天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
7天前
|
Java 调度 开发者
Java中的多线程基础及其应用
【9月更文挑战第13天】本文将深入探讨Java中的多线程概念,从基本理论到实际应用,带你一步步了解如何有效使用多线程来提升程序的性能。我们将通过实际代码示例,展示如何在Java中创建和管理线程,以及如何利用线程池优化资源管理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧,帮助你更好地理解和应用多线程编程。
|
12天前
|
缓存 监控 Java
java中线程池的使用
java中线程池的使用
|
11天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
16天前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。