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


目录
相关文章
|
14天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
13天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
11天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
4天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
5天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
5天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
14 1
|
6天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
6 0

热门文章

最新文章