《Java网络编程》第二章—线程基础与使用

线程

第二章 线程的简介与使用



前言

线程是什么已经一个老生常谈话题了,还是把基础知识先放在前面,让大家做个复习哈哈哈

线程是什么

线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。每个线程都拥有独立的执行路径,但共享进程中的资源,如内存和文件句柄等。Java线程机制提供了多线程编程的能力,使得开发者可以编写出能够同时执行多个任务的应用程序。

线程与进程的区别

进程: 是操作系统分配资源的基本单位,它包含运行一个程序所需的所有资源。
线程: 是进程中的一个执行单元,负责执行进程中的一段序列化的程序指令。


一、网络场景下线程是怎样引出的

在90年代,我们没有Web,没有HTTP,也没有图形界面的浏览器。那时候存在Usenet新闻,FTP命令行接口,当时虽然只有几百万用户而不是数十亿用户的时候,反而比现在还容易超负荷使得网站拥塞。其问题的原因就是因为大多数FTP服务器会为每个连接创建(fork)一个进程。这样FTP服务器一眼就能看出实现得不行了。
而早期的Web服务器也有这个问题,但因为HTTP连接当时不具备持久性而将其掩盖。通过Web浏览器获取的数据较小,并且获取文件后会“挂起”连接,而不是保持连接。所以Web服务器的压力远远小于FTP服务器。而这样并不能解决根本问题,当有大量连接时,Web服务器还是会承受不住压力。
那么就引出如何解决这个问题,至少有两种方案可以解决,分别是:

  1. 重用进程,服务器启动时就创建固定数量的进程(多个)处理请求。当请求进入服务器,就放入一个队列,这些进程就从队列中取出请求进行服务。乍一听感觉像是笨笨的办法,是在一台机器上多个进程的方式来,提升解决速度,并在一定程度上限制了资源的使用避免大量进程创建导致服务器运行缓慢甚至崩溃,再将其方法进行优化,便把这些进程分散在多台机器,这样就是实现了负载均衡。当然这样就会又带来设计上的问题,特别是一致性方面的。
  2. 使用线程来处理连接,每个单独的进程都有自己的一块内存,线程会共享内存,使用线程替代进程,可以使得服务器性能提升三倍。同样硬件与网络连接条件下,再结合可重用线程池,服务器的运行可以快9倍以上。如果并发线程数达到4000-20000时,大多数java虚拟机可能会由于内存耗尽而崩掉。不过通过使用线程池,而不是为每个连接生成新线程,服务器每分钟就可以用不到100个线程来处理数千个短连接。

使用多线程可能会遇到什么问题

  1. 复杂多线程逻辑与设计问题。
  2. 由于线程共享相同内存,而导致内存中变量或数据结构使用冲突的问题。
  3. 对于资源上锁的设计问题,避免死锁等。

二、使用java线程

在虚拟机中执行的线程与虚拟机构造的Thread对象之间是一一对应的。
启动一个最简单的线程在虚拟机中运行。

代码示例

Thread t = new Thread();
t.start();

使线程完成一些自己的操作,除使用线程池外有以下3种办法:

  1. 继承Thread类,通过创建一个新的类来继承Thread类,并重写其run()方法。在run()方法中编写线程需要执行的代码。然后,创建该类的实例,并调用start()方法来启动线程。
public class MyThread extends Thread {
   @Override
   public void run() {
       // 线程需要执行的代码
   }

   public static void main(String[] args) {
       MyThread thread = new MyThread();
       thread.start(); // 启动线程
   }
}
  1. 实现Runnable接口,创建一个实现了Runnable接口的类,并实现其run()方法。同样,在run()方法中编写线程需要执行的代码。然后,创建一个Thread对象,将实现了Runnable接口的类的实例作为参数传递给Thread对象的构造函数。最后,调用Thread对象的start()方法来启动线程。
public class MyRunnable implements Runnable {
   @Override
   public void run() {
       // 线程需要执行的代码
   }

   public static void main(String[] args) {
       MyRunnable myRunnable = new MyRunnable();
       Thread thread = new Thread(myRunnable);
       thread.start(); // 启动线程
   }
}
  1. 使用Callable和Future,与Runnable类似,Callable也是一个接口,但它可以返回结果并且可以抛出异常。要使用Callable,你需要创建一个实现了Callable接口的类,并实现其call()方法。然后,使用ExecutorService来提交一个Callable任务,并获取一个Future对象,该对象可以用于获取任务的执行结果或检查任务是否完成。
import java.util.concurrent.*;

public class MyCallable implements Callable<String> {
   @Override
   public String call() throws Exception {
       // 线程需要执行的代码,并返回结果
       return "Task result";
   }

   public static void main(String[] args) {
       ExecutorService executor = Executors.newSingleThreadExecutor();
       Future<String> future = executor.submit(new MyCallable());

       try {
           // 获取任务的执行结果
           String result = future.get();
           System.out.println(result);
       } catch (InterruptedException | ExecutionException e) {
           e.printStackTrace();
       } finally {
           executor.shutdown();
       }
   }
}

当run方法完成时,线程也就消失了。run()对于线程就像main()方法对于非线程化传统程序一样。

1.派生Thread

代码如下(示例):
这个类的主要功能是计算并打印出指定文件的 SHA-256 哈希值。

package com.example.gobuy.utils;

import javax.xml.bind.DatatypeConverter;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class DigestThread extends Thread{
    private String filename;

    public DigestThread(String filename){
        this.filename = filename;
    }

    @Override
    public void run(){
        try{
            FileInputStream in = new FileInputStream(filename);
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            DigestInputStream din = new DigestInputStream(in,sha);
            while (din.read() != -1);
            din.close();
            byte[] digest = sha .digest();
            StringBuilder result = new StringBuilder(filename);
            result.append(": ");
            result.append(DatatypeConverter.printHexBinary(digest));
            System.out.println(result);
        }catch (IOException ex){
            System.err.println(ex);
        }catch (NoSuchAlgorithmException ex){
            System.err.println(ex);
        }
    }
    public static void main(String[] args) {
        String[] s = new String[2];
        s[0] = "output.txt";
        s[1] = "data.txt";
        for (int i = 0; i < 2; i++) {
            Thread t = new DigestThread(s[i]);
            t.start();
        }

    }
}
部分代码解释:
MessageDigest.getInstance("SHA-256") 获取一个 SHA-256 算法的 MessageDigest 实例
创建一个 DigestInputStream 实例,它包装了 FileInputStream 并使用 MessageDigest 实例来计算文件的哈希值。
这是个过滤器流,它会在读取文件时计算一个加密散列函数。读取结束时从digest()方法中获取的散列值
DatatypeConverter.printHexBinary(digest) 将哈希值的字节数组转换为十六进制字符串

结果如下

data.txt: 12981A5D5D602DCFC34E1DAC029D940F42019B5D8D0D8117042EEEB3E1F241BB
output.txt: D1966A1EE8B32860C9F441DE73947F7798CE0EB3225B3CC76A4654433458235A

由于我的output.txt比data.txt大很多(1MB对1KB),由于从磁盘读取文件速度较慢,可以看到data.txt 比output.txt文件输出更快,这里是for循环每次执行都会建立一个新的线程的执行run()方法。在学习线程之余,也多复习一下java流的使用,很方便计算文件的哈希值。

提示: 如果使用Thread派生子类,就只应覆盖run()方法,而不要覆盖其他方法!其他start(),join()等方法都有特定的语义,它们与虚拟机的交互很难在自己代码中实现。

2.实现Runnable接口

第二种方式是实现Runnable接口,实现这个接口的类必须提供run()方法。除了这个方法外,可以自己创建其他任何方法。
重新改写上述功能,使用实现Runnable接口实现。

package com.example.gobuy.utils;

import javax.xml.bind.DatatypeConverter;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class DigestRunnable implements Runnable{
    private String filename;

    public DigestRunnable(String filename){
        this.filename = filename;
    }

    @Override
    public void run(){
        try{
            FileInputStream in = new FileInputStream(filename);
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            DigestInputStream din = new DigestInputStream(in,sha);
            while (din.read() != -1);
            din.close();
            byte[] digest = sha .digest();
            StringBuilder result = new StringBuilder(filename);
            result.append(": ");
            result.append(DatatypeConverter.printHexBinary(digest));
            System.out.println(result);
        }catch (IOException ex){
            System.err.println(ex);
        }catch (NoSuchAlgorithmException ex){
            System.err.println(ex);
        }
    }
    public static void main(String[] args) {
        String[] s = new String[2];
        s[0] = "output.txt";
        s[1] = "data.txt";
        for (int i = 0; i < 2; i++) {
            DigestRunnable r = new DigestRunnable(s[i]);
            Thread t = new Thread(r);
            t.start();
        }
    }
}

只需要将继承改为实现,再将main方法中创建runnable对象,并传给原Thread构造函数即可。

两种方法都可以创建线程,并没有优劣之分,只有不同场景采用哪种方式便于自己实现功能。当出现run()方法需要放在某个类中,而这个类又需要继承其他类,则必须使用Runnable接口了。总所周知啊java不支持多继承。

3.使用Callable实现

package com.example.gobuy.utils;

import javax.xml.bind.DatatypeConverter;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

public class DigestCallable implements Callable<String> {
    private String filename;

    public DigestCallable(String filename) {
        this.filename = filename;
    }

    @Override
    public String call() throws Exception {
        try {
             FileInputStream in = new FileInputStream(filename);
             MessageDigest sha = MessageDigest.getInstance("SHA-256");
             DigestInputStream din = new DigestInputStream(in, sha);

            while (din.read() != -1); // 读取整个文件以计算摘要

            byte[] digest = sha.digest();
            StringBuilder result = new StringBuilder(filename);
            result.append(": ");
            result.append(DatatypeConverter.printHexBinary(digest));
            return result.toString();

        } catch (IOException ex) {
            throw new IOException("Error reading file: " + filename, ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("SHA-256 algorithm not found", ex);
        }
    }

    public static void main(String[] args) {
        String[] files = {"output.txt", "data.txt"};

        // 使用ExecutorService来管理线程池和Callable任务
        ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(2);
        List<Future<String>> futures = java.util.Arrays.asList();

        for (String file : files) {
            DigestCallable callable = new DigestCallable(file);
            java.util.concurrent.Future<String> future = executor.submit(callable);
            futures.add(future); // 注意:这里需要初始化futures列表或使用ArrayList
        }

        // 等待所有任务完成并获取结果
        for (java.util.concurrent.Future<String> future : futures) {
            try {
                System.out.println(future.get()); // 这将阻塞直到结果可用
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 关闭ExecutorService
        executor.shutdown();
    }
}

总结

以上就是今天要讲的内容,本文仅仅简单介绍了线程的基本信息与使用,并且介绍了使用线程主要有三种方式:继承Thread类、实现Runnable接口、使用Callable和Future。每种方式都有其适用的场景和优缺点,可以根据具体需求选择最适合的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZZZ_Tong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值