Java网络编程学习笔记(3)——多线程(一)

线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程与进程

以FTP服务器为例,FTP服务器会为每个连接创建一个新的进程(也就是说,100个并发意味着要处理100个额外的进程)由于进程是相当重量级的,太多进程会让服务器吃不消。Web服务器也有类似特性,不过会因为HTTP连接的短暂性有所掩盖。但当处理成千上万个同时连接时,性能就会变得像爬行一样慢了。
这里写图片描述
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

多线程,并行并发

用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。

多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
这里写图片描述

线程安全:

经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。对象的状态是指存储在状态变量(实例或静态域)中的数据。对象的状态还可能包括其他依赖对象的域。

一个对象是否需要线程安全,取决于该对象是否被多线程访问。这指的是程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,要采用同步机制来协同对对象可变状态的访问。Java常用的同步机制是Synchronized,还包括 volatile类型的变量,显示锁以及原子变量。

线程的安全性问题:

因为不同线程共享相同的内存,一个线程完全有可能破坏另一个线程使用的变量和数据结构。这就类似于如果一个程序,在没有内存保护机制的操作系统(如Windows95)中运行可能会破坏整个操作系统。

同步:

Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

运行线程

这里写图片描述
应当把线程要做的所用工作都放在这个方法中,这个方法可以调用其他方法,可以构造其他对象,甚至可以生成其他线程。当run()方法完成时,线程也就消失了。事实上,run()对于线程就像main()方法对于非线程化传统程序的作用一样,单线程程序会在main()方法返回时退出,多线程程序会在main()方法以及非守护进程都返回时才退出。

线程的创建和启动:

实现Runnable的接口方式
定义Runnable接口实现类,并重写run()方法,run()方法的方法体是线程执行体
class SonThread implements Runnable{
public void run(){
}
}
SonThread s1 = new SonThread();//实现类实例
Thread t1 = new Thread(s1); //用该实例来作为Thread的target作为Thread对象
t1.start(); 调用该线程的start方法启动线程。

运行结果:
这里写图片描述

从线程返回信息

传统的单线程过程转向多线程环境时,最难掌握一点就是从线程返回信息,
从结束的线程获得信息:这是多线程最容易被误解的方面之一。
run()方法与start()方法不返回任何值

假如这里摘要线程需要把摘要返回给执行主进程,大多数想到的方法,是使用存取方法返回结果的线程。
存取的方法不行:
这里写图片描述
问题在于主程序会在线程有机会初始化摘要之前就要获取并使用摘要,
单线程控制流可以正常工作,但是多线程情况有所不同。
dr.start()启动的计算可能在main()方法调用dr.getDigest之前结束,也可能没有结束,如果没有结束dr.getdDigest()则会返回null
所以就会抛出NullPoinerException异常。

也就是说main()方法不会等到dr.start()里的run()方法运行完才继续运行,
main()相当于主线程,与dr线程可能会同时运行,只是dr线程是在main()主线程里启动的。
所以这一点上线程并不等于方法。

另一种可能的方法:
这里写图片描述
如果你够幸运可能会正常工作
竞态条件:
程序生成了多少个线程,系统的CPU,和磁盘速度,系统使用多少个CPU,以及Java虚拟机为不同线程分配时间所用的算法
这些都称为race conditions
能够得到正确结果依赖于线程的相对速度。

大多数会选择轮询,让获取方法返回一个标志值,知道设置了结果字段为止,然后主线程定期询问获取方法,查看是否返回了标志值
这个例子需要重复测试digest是否为空。

回调

不用主程序不断询问线程,而是线程告诉主程序何时结束,这是通过调用主类的方法做到的,因此称为回调。
三种模块间调用:
1.同步调用:类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。这种调用方式适用于方法b()执行时间不长的情况,因为b()方法执行时间一长或者直接阻塞的话,a()方法的余下代码是无法执行下去的,这样会造成整个流程的阻塞。
2.异步调用:类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。这种调用方式适用于方法b()执行时间不长的情况,因为b()方法执行时间一长或者直接阻塞的话,a()方法的余下代码是无法执行下去的,这样会造成整个流程的阻塞。(也就是上面的错误事例)
3.回调:类A的a()方法调用类B的b()方法
类B的b()方法执行完毕主动调用类A的callback()方法
这样一种调用方式组成了下图,也就是一种双向的调用方式。
这里写图片描述
实例代码:

//InstanceCallbackDigest.java
import java.io.*;
import java.security.*;

public class InstanceCallbackDigest implements Runnable{

    private String filename;
    private InstanceCallbackDigestUserInterface callback;

    public InstanceCallbackDigest(String filename, InstanceCallbackDigestUserInterface callback) {
        this.filename = filename;
        this.callback = callback;
    }

    @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();
            callback.receiveDigest(digest);
        }catch(IOException | NoSuchAlgorithmException ex) {
            System.err.println(ex);
        }
    }
}
//InstanceCallbackDigestUserInterface.java
import javax.xml.bind.*;

public class InstanceCallbackDigestUserInterface{

    private String filename;
    private byte[] digest;

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

    public void calculateDigest() {
        InstanceCallbackDigest cb = new InstanceCallbackDigest(filename, this);
        Thread t= new Thread(cb);
        t.start();
    }

    void receiveDigest(byte[] digest) {
        this.digest = digest;
        System.out.println(this);
    }

    @Override
    public String toString() {
        String result = filename + ": ";
        if(digest!=null) {
            result += DatatypeConverter.printHexBinary(digest);
        }else {
            result += "digest not available";
        }
        return result;
    }

    public static void main(String[] args) { 
        for(String filename: args) {
            InstanceCallbackDigestUserInterface d = new InstanceCallbackDigestUserInterface(filename);
            d.calculateDigest();
        }
    }
}

代码分析:
用户实例类:
1.构建对象:包含存有digest信息的变量
2.运行线程,线程是(重写runnable的)实例类
3.回调过程,实例类的run方法返回时包含(回调)用户实例的方法,把digest值传递过去。
这样的优点,保证在线程结束时返回提取值,不会出现上面的问题。

总结起来,回调的核心就是回调方将本身即this传递给调用方,这样调用方就可以在调用完毕之后告诉回调方它想要知道的信息。回调是一种思想、是一种机制,至于具体如何实现,如何通过代码将回调实现得优雅、实现得可扩展性比较高,一看开发者的个人水平,二看开发者对业务的理解程度。

猜你喜欢

转载自blog.csdn.net/qq_37423198/article/details/79633611