首页 > 编程语言 >Java多线程

Java多线程

时间:2024-06-03 15:32:17浏览次数:17  
标签:Java synchronized Thread 线程 new 多线程 public

线程的定义

Java线程是Java编程语言中的执行单元。在Java中,线程可以看作是轻量级的进程,它独立运行,具有自己的执行路径。

线程的原理

Java线程的实现基于操作系统的线程模型,但Java虚拟机(JVM)对线程的管理和调度做了封装和优化,使得Java线程更加可控和可靠。下面是Java线程的一些基本原理:

  • Java线程模型:Java线程模型是基于内核级线程模型的,即每个Java线程对应一个底层操作系统的线程。Java的线程管理由JVM负责,而不是由操作系统直接管理。

  • 线程调度:Java线程的调度由JVM的线程调度器完成,它根据线程的优先级和调度算法来决定线程的执行顺序。Java线程的优先级范围是1到10,默认值为5。尽管可以设置线程的优先级,但操作系统可能不一定会严格按照优先级来调度线程。

  • 线程同步:Java提供了多种机制来实现线程同步,包括synchronized关键字、Lock接口及其实现类、volatile关键字、wait()、notify()和notifyAll()等。这些机制可以确保多个线程之间对共享资源的访问是安全的,避免了竞态条件和数据不一致的问题。

  • 线程状态转换:Java线程具有多个状态,如新建、就绪、运行、阻塞、等待和终止等。线程在不同状态之间转换的规则由JVM管理,开发人员通常无需直接控制线程的状态转换。

  • 线程间通信:Java提供了多种机制来实现线程间的通信,包括共享内存、管道、消息队列、信号量、条件变量、读写锁等。其中,最常用的是共享内存和wait()/notify()机制。

  • 守护线程:Java中的线程分为守护线程(Daemon Thread)和用户线程(User Thread)。守护线程是为其他线程提供服务的线程,当所有的用户线程结束后,守护线程会自动销毁。典型的守护线程包括JVM的垃圾回收线程和后台IO线程等。

总的来说,Java线程的原理是基于操作系统提供的线程机制,通过JVM对线程的封装和管理实现了更高级别的抽象和控制。Java的多线程编程模型相对较为简单,但开发人员仍需了解底层原理和掌握合适的线程管理技巧,以确保程序的正确性和性能。

线程的创建方式

Runnable接口实现

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的任务逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread is running: " + i);
            try {
                Thread.sleep(1000); // 模拟任务执行时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建一个实现了Runnable接口的实例
        MyRunnable myRunnable = new MyRunnable();

        // 创建一个线程,传入实现了Runnable接口的实例
        Thread thread = new Thread(myRunnable);

        // 启动线程
        thread.start();
    }
}

Thread类实现

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的任务逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread is running: " + i);
            try {
                Thread.sleep(1000); // 模拟任务执行时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建一个继承自Thread类的实例
        MyThread myThread = new MyThread();

        // 启动线程
        myThread.start();
    }
}

二者区别

在 Java 中,创建线程可以通过实现 Runnable 接口或者扩展 Thread 类两种方式来实现。这两种方式有一些区别:

  • 继承性:
    使用 Runnable 接口实现线程的方式可以更好地利用类的继承,因为 Java 不支持多重继承,但可以实现多个接口。因此,如果一个类已经继承了某个类,仍然可以通过实现 Runnable 接口来创建线程。
    直接扩展 Thread 类创建线程,该类就不再有机会继承其他类了。

  • 资源消耗:
    实现 Runnable 接口创建线程,多个线程可以共享同一个 Runnable 实例,因为线程对象和任务对象是分离的,这样可以节约系统资源。
    直接扩展 Thread 类创建线程,每次创建一个新线程都需要新建一个 Thread 类的实例,这样可能会消耗较多的系统资源。

  • 扩展性:
    实现 Runnable 接口的方式更具有扩展性,因为可以在不修改现有类继承关系的情况下,为类添加新的行为。
    直接扩展 Thread 类创建线程,线程与任务逻辑紧密耦合在一起,不够灵活。

  • 面向对象原则:
    使用 Runnable 接口实现线程更符合面向对象设计原则中的“组合优于继承”的理念,因为任务逻辑与线程本身是分离的,更符合单一职责原则。
    直接扩展 Thread 类创建线程,线程类既承担了线程控制的职责,又包含了任务逻辑,可能违反了单一职责原则。

总的来说,虽然两种方式都可以用来创建线程,但在大多数情况下,推荐使用实现 Runnable 接口的方式来创建线程,因为它更灵活、更节约资源,更符合面向对象设计原则。

使用场景

  • 并发编程:线程最常见的用途之一是实现并发编程。当一个程序需要同时执行多个任务时,可以使用多线程来实现并发执行,提高系统的响应速度和吞吐量。比如,一个网络服务器可以使用多线程同时处理多个客户端的请求,一个图形界面程序可以使用多线程来保持界面的响应性,同时执行耗时的任务。
    比如下载图片,一共有A、B、C三个图片需要下载,在传统过程中,如果先下载A图片,如果A没下载完B是不能下载的,那么我们就可以利用并发编程来实现同时操作。代码如下

    public class ImageDownloader implements Runnable {
      private String imageUrl;
    
      public ImageDownloader(String imageUrl) {
          this.imageUrl = imageUrl;
      }
    
      @Override
      public void run() {
          System.out.println("Downloading image from: " + imageUrl);
          // 实现图片下载逻辑,这里用一个简单的休眠代替
          try {
              Thread.sleep(2000); // 模拟下载耗时
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Download completed for: " + imageUrl);
      }
    
      public static void main(String[] args) {
          String[] imageUrls = {"https://example.com/image1.jpg", "https://example.com/image2.jpg", "https://example.com/image3.jpg"};
    
          for (String url : imageUrls) {
              Thread thread = new Thread(new ImageDownloader(url));
              thread.start();
          }
      }
    }
    
    
  • 异步编程:线程也常用于实现异步编程,即在执行耗时的操作时不会阻塞主线程,而是使用新的线程来执行这些操作,使得程序能够同时执行其他任务。异步编程在网络通信、文件读写、数据库访问等场景下特别有用,可以提高程序的性能和用户体验。

    import java.util.concurrent.CompletableFuture;
    
    public class AsyncExample {
      public static void main(String[] args) {
          System.out.println("Main thread started");
    
          // 创建第一个异步任务
          CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
              System.out.println("Task 1 started");
              try {
                  Thread.sleep(2000); // 模拟耗时操作
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("Task 1 completed");
          });
    
          // 创建第二个异步任务
          CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
              System.out.println("Task 2 started");
              try {
                  Thread.sleep(1000); // 模拟耗时操作
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("Task 2 completed");
          });
    
          // 等待所有任务完成
          CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);
    
          // 阻塞直到所有任务完成
          allOf.join();
    
          System.out.println("Main thread completed");
      }
    }
    //        输出内容:
    //        Main thread started
    //        Task 1 started
    //        Task 2 started
    //        Task 2 completed
    //        Task 1 completed
    //        Main thread completed
    

    在这个示例中,我们使用CompletableFuture.runAsync()方法创建了两个异步任务future1和future2,它们分别模拟了耗时的操作。然后,我们使用CompletableFuture.allOf()方法来等待所有的任务都完成,最后使用join()方法来阻塞主线程直到所有任务完成。

  • 任务分解:有些任务可以被分解成多个子任务,并行或者并发地执行,从而提高整体的执行效率。线程可以用来并行执行这些子任务,然后将它们的结果合并起来得到最终的结果。这种方式在科学计算、图像处理、数据分析等领域经常被使用。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    
    public class TaskDecompositionExample {
      public static void main(String[] args) throws InterruptedException, ExecutionException {
          // 创建一个线程池
          ExecutorService executor = Executors.newFixedThreadPool(4);
    
          // 创建任务列表
          List<Callable<Integer>> tasks = new ArrayList<>();
          tasks.add(new SubTask(1, 100));
          tasks.add(new SubTask(101, 200));
          tasks.add(new SubTask(201, 300));
          tasks.add(new SubTask(301, 400));
    
          // 提交任务并获取Future列表
          List<Future<Integer>> futures = executor.invokeAll(tasks);
    
          // 关闭线程池
          executor.shutdown();
    
          // 合并结果
          int totalResult = 0;
          for (Future<Integer> future : futures) {
              totalResult += future.get();
          }
    
          System.out.println("Total Result: " + totalResult);
      }
    
      // 子任务类
      static class SubTask implements Callable<Integer> {
          private int start;
          private int end;
    
          public SubTask(int start, int end) {
              this.start = start;
              this.end = end;
          }
    
          @Override
          public Integer call() throws Exception {
              int result = 0;
              for (int i = start; i <= end; i++) {
                  result += i;
              }
              return result;
          }
      }
    }
    

    以上代码是求1~400的和,并且分成四个子任务同时求和。

  • 事件驱动编程:许多 GUI 应用程序都使用事件驱动的编程模型,即通过监听事件来响应用户的操作。线程可以用来处理事件,以保持界面的响应性,同时执行其他任务。比如,在一个多线程的图形界面程序中,可以使用一个线程来监听用户的输入事件,另一个线程来更新界面的显示。

  • 后台任务:有些任务不需要用户直接参与,可以在后台默默地执行。比如,定时任务、数据备份、日志记录等工作都可以通过线程来实现,而不影响用户的正常操作。

总的来说,线程在编程中的使用场景非常广泛,可以用来实现并发编程、异步编程、任务分解、事件驱动编程以及后台任务等各种功能。

并行、串行、并发的区别

串行


从开始到结束一直都是赛车A第一名、赛车B第二名、赛车C第三名、赛车D第四名

并行

从始至终,A、B、C、D四辆赛车肩并肩都是第一名

并发

并发也就是正常中的比赛,在比赛过程中,不同的时刻每辆车的名次不同,有可能超车,有可能在同一时刻两辆车平行前进

线程的生命周期

JDK5

JDK8

线程安全-synchronized

  • synchronized是Java中的一个关键字,它的存在主要是为了解决线程安全问题。

  • 可以修饰在方法和静态方法上、或者修饰代码块

    public class TestThread {
        // 实例方法
        public synchronized void testA() {
            ...
        }
    
        // 静态方法
        public synchronized static void testB() {
            ...
        }
        // 修饰代码块
        synchronized(同步监视器){
            ...
        }
    }
    

    同步监视器

    在Java中,通过使用 synchronized 关键字来创建同步代码块或者同步方法,以确保在同一时刻只有一个线程可以访问被同步的代码块或方法。当一个线程进入同步代码块或方法时,它会尝试获取同步监视器(也称为锁),如果同步监视器已经被其他线程占用,则该线程会被阻塞,直到获取到同步监视器为止。

    在使用 synchronized 关键字时,可以指定同步监视器。对于普通的同步方法,同步监视器是当前对象实例(即使用 synchronized 的方法所属的对象实例),而对于同步代码块,同步监视器可以是任意对象。

    • 皇上御赐令牌 -> 锁(同步监视器)

    • 城门外 便是 synchronized 修饰的代码块内容

    • 如果线程A拿上令牌(独一份),一直不放回来,那么别人就别想出这个城门。

    synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上是给 Class 类上锁。

    synchronized 关键字加到实例方法上是给对象实例上锁。

      解释:Class类,就相当于一个模子,而对象实例就是从模子中刻出来的实物。模子只有一个,但是实物可以有多个。
    

    经典案例:火车站有100张票可售卖,一共有三个窗口,直到卖完为止

    public class WindowsMain {
        public static void main(String[] args){
            SaleTicketWindow sale = new SaleTicketWindow();
    
            Thread thread01 = new Thread(sale);
            Thread thread02 = new Thread(sale);
            Thread thread03 = new Thread(sale);
    
            thread01.start();
            thread02.start();
            thread03.start();
        }
    }
    

    线程不安全情况

    public class SaleTicketWindow implements Runnable{
        static int ticket = 100;
        @Override
        public void run(){
            while(true){
                if(ticket > 0){
                    System.out.println("窗口" 
                    + Thread.currentThread().getName() 
                    + "售出第" 
                    + ticket-- 
                    + "张票");
    
                    try {
                        Thread.sleep(500L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    
                }else {
                    break;
                }
            }
        }
    }
    

    使用synchronized ,解决线程安全

    public class SaleTicketWindow implements Runnable{
        static int ticket = 100;
        @Override
        public void run(){
            while(true){
                synchronized(this){
                    if(ticket > 0){
                        System.out.println("窗口" 
                        + Thread.currentThread().getName() 
                        + "售出第" 
                        + ticket-- 
                        + "张票");
    
                        try {
                            Thread.sleep(500L);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else {
                        break;
                    }
                }
            }
        }
    }
    

    这里 同步监视器 使用 this的原因是:this指向当前的SaleTicketWindow实例,而在主方法中我们只 new了一个实例,所以可以用this,如果new 了多个SaleTicketWindow,那么就不是同一把锁了。

wait和notify和sleep

  • wait方法属于Object类中的,sleep属于Thread类中的

  • wait方法释放锁资源和执行权,而sleep释放执行权,但不释放锁

  • wait方法一定要放在synchronized代码块中,sleep并没有强制要求加synchronized同步锁

  • sleep方法是指定时间自动唤醒的,而wait方法是需要notify方法唤醒或者notifyAll唤醒的

    比如张三说“我睡三个小时,自然醒”,而狗蛋说:“我睡觉没准,必须有人给我喊醒,要不然一直睡”

两条线程交替打印1~100

class PrintOneHundred implements Runnable{
  static int i = 0;
  @Override
  public void run() {
      while (i <= 100){
          synchronized (this) {
              notify();
              if (i < 100){
                  i++;
                  System.out.println(Thread.currentThread().getName() + "打印了" + i);
              }
              else {
                  break;
              }
              try {
                  wait();
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
          try {
              sleep(50L);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
        }
    }
}

class PrintMain{
    public static void main(String[] args) {
        PrintOneHundred printOneHundred = new PrintOneHundred();
        Thread t01 = new Thread(printOneHundred);
        Thread t02 = new Thread(printOneHundred);

        t01.start();
        t02.start();
    }
}

生产者&消费者&产品

class Clerk {
    static int proCount = 0;

    public synchronized void add(){
        if (proCount >= 20){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        proCount++;
        System.out.println("生产者生产了第" + proCount + "个产品");
        notify();
    }

    public synchronized void reduce() {
        if (proCount <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("消费者消费了第" + proCount + "个产品");
        proCount--;
        notify();
    }

}

class Customer extends Thread{
    Clerk clerk;
    public Customer(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true){
            clerk.reduce();
            try {
                sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Productor extends Thread{
    Clerk clerk;
    public Productor(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true){
            clerk.add();
            try {
                sleep(500L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Main02{
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Customer customer = new Customer(clerk);
        Productor productor = new Productor(clerk);
        customer.start();
        productor.start();
    }
}

标签:Java,synchronized,Thread,线程,new,多线程,public
From: https://www.cnblogs.com/dragon-925/p/18224988

相关文章

  • 2.3Docker部署java工程
    2.3Docker部署java工程1.导入jar包2.在Docker部署jdk(容器名为myjdk17)3.修改jar包名mv原包名新包名4.配置启动脚本Dockerfile是一个文本文件,其中包含了构建Docker镜像所需的一系列步骤和指令。通过编写Dockerfile文件,可以生成我们想要的镜像。基于JDK17镜像使......
  • Java18新特性有哪些
    Java18于2022年3月22日正式发布,它带来了一些新特性和改进,主要包括以下几点:JEP400:UTF-8byDefault123:JDK将UTF-8设置为默认字符集,这使得依赖于默认字符集的API在所有实现、操作系统、区域设置和配置中保持一致。JEP408:SimpleWebServer123:引入了一个简......
  • TypeScript与JavaScript之间的关系
    TypeScript是一种JavaScript的超集语言,这意味着任何有效的JavaScript代码在TypeScript中都是合法的。TypeScript的设计并不意图打破或改变JavaScript的语法,因此您可以将现有的JavaScript代码直接放入TypeScript文件中,无需担心代码的结构或格式。然而,TypeScript不仅仅是JavaScrip......
  • JavaEE初阶--锁进阶理解
    目录一、引言二、锁的分类1.乐观锁vs悲观锁2.重量级锁vs轻量级锁3.自旋锁vs挂起等待锁4.公平锁vs非公平锁5.可重入锁vs不可重入锁6.读写锁三、CAS1.什么是CAS?2.CAS伪代码3.CAS的实现4.CAS的应用5.CAS的ABA问题四、总结一、引言 前面的博客我们......
  • Java邮件发送的基本流程是什么?如何实现?
    Java邮件发送的SMTP服务器如何配置?怎么配置发信?Java邮件发送是在Java应用程序中通过邮件协议(如SMTP)发送电子邮件的过程。它是许多企业级应用程序中常见的功能,用于向用户发送通知、确认邮件等。AokSend将介绍Java邮件发送的基本流程,以及其中涉及的关键步骤。Java邮件发送:创建......
  • 【Java数据结构】详解Stack与Queue(一)
    ......
  • java冒泡法代码
    importjava.util.Arrays;publicclasshhh{publicstaticint[]pao(int[]arr){for(inti=0;i<arr.length-1;i++){for(intj=0;j<arr.length-1-i;j++){if(arr[j]>arr[j+1]){......
  • JavaScript第三讲:解锁JavaScript的数据世界:基本数据类型与类型转换的奥秘
    前言:hello,大家好,在JavaScript的编程世界中,数据是构成一切的基础。无论是构建复杂的Web应用,还是处理用户输入,我们都无法绕开数据这一核心元素。而在JavaScript中,数据以不同的形式存在,我们称之为数据类型。理解这些数据类型以及它们之间的转换关系,对于编写高效、健壮的代码至关......
  • Java基础知识
    2.第一个Java应用2.1创建Java源文件Java应用由一个或多个扩展名为.java的文件构成。publicclassDoll{/**福娃的名字*/privateStringname;/**构造方法*/publicDoll(Stringname){this.name=name;//设置福娃的名字}......
  • java中SimpleDateFormat解析日期格式的问题
    在日常写代码的过程中,我们经常要处理各种格式的日期,常见的日期格式有:“20240601”,“2024-06-01”,“2024-6-1”。如何正确地处理日期格式,尤其是对外接口中参数的日期格式,就很重要了,一个不小心就可能出现意想不到的问题。举一个我遇到的真实例子:我们提供的对外接口中有一个参数是......