关于技术技能的准备,我相信大家都会有同样的问题:我们希望像考试一样,我到底准备成什么样才算是准备了85分,很多同学其实并不追求100分,但是就想知道85分的量在哪里。这个问题其实我觉得是个很好的问题,如果你没有一个边界限定,准备起来就像是一个无底洞,永远在心怀忐忑,永远在准备的路上,但永远也不自信。所以大家一定要做到心中有数,我到底知道哪些知识点才算是准备充分,可以停止阅读新的书籍,只复习这一本书就可以了呢?本章回答的就是关于java,在测试开发工程师这个岗位上大家应该准备到的一些知识点。

大家自己在学习的过程中也需要进行自我梳理,希望大家能做到在面试官问你:“你Java学习的怎么样呀、你会java吗”这类问题的时候,你能够不需要和面试官互动的讲上10分钟你在Java学习过程中所做的一些总结和梳理,凸显自己的总结和思考能力(包括你都从那几个方面学习java的相关知识,然后每个方面中你的收获是什么,如果做到这一步那你其实就真正的掌握了java),如果有相关的小的开发实践就也可以顺带提出来,形成“学习-总结-实践”的闭环,这样做的效果应该会好于单纯的一问一答的面试。其实对于其他任何一门语言的学习也可以采用类似的思路。很多时候我们都说自己会XXX,但是你怎么才能衡量和说明你会呢?答案就是:需要输出一些东西,来证明你输入过的那些东西在你身上。本篇内容会比较长,我可能需要分批补齐。

概要

我自己总结下来,在java的学习中可以分为这几个大的模块:
(1)首先是括程序设计语言的基础知识:关键词、基本数据类型、基本语法的相关知识(对齐C语言,有C语言功底的话这个部分可以很快的掌握),
(2)然后是java中的面向对象的设计,类的封装、继承和多态、接口的概念;
(3)然后需要掌握java jdk帮我们封装好的常用的集合类(工作中比较实用的知识)
(4)然后要学习java中的io操作(这个也是工作中使用的比较实用的知识,但是初次接触起来特别复杂,至少在我当初学习的时候我一直觉得有很多东西,搞的自己有点云里雾里);
(5)学习java中网络请求相关的实现(比较实用)
(6)学习反射(这也是在后面学习AOP的时候要用到的知识)、类加载机制、注解、泛型编程等进行框架以及工具类开发时比较实用的知识
(7)学习多线程编程的相关知识,这部分内容在进行深入一点测试的时候我们需要了解相关的知识背景。
(8)Java 8的典型的特性。
(9)还有一个大头是关于jvm的面试知识,这个部分我们会另起一个章节来介绍。


在书籍推荐上,很多人会列举诸如 java核心编程思想之类的书籍,但从我个人角度来讲,应对面试,推荐大家可以看看李刚的《疯狂java讲义》,这本书虽然是一部大头书,但是关于java的细节都会有涉及到,也像一部工具书。在学习的时候我们往往习惯从头开始看以至于后面的多线程、反射的相关知识可能大家都草草的看了过去(或许大家没有这个坏习惯,只是我有),所以本篇内容会从最后面到最前面开始一点一点的学习,也算是给大家准备过程中的一个互补。

java的多线程

在我们日常的测试过程中,多线程编程是有相应的实践空间的。通常会用来模拟多个用户同时访问相同的接口时的表现是否正常:比如有的时候可能前端没有做防止多次点击的交互、用户多次点击领取任务是否会异常;多个用户同时领取同一条任务是否会异常;在审核工作台上,审核任务是基于后端自动分发模式的,多个用户同时刷新页面认领任务是否会异常等。多线程编程能够帮助我们在这种场景下进行并发请求的测试,这也是单纯的手工测试无法实现的,同时也是为什么做高阶测试也需要写代码的原因。

关于多线程,我曾经也问过我自己,我到底掌握了什么才算是掌握了java多线程的学习呢?我理解我们应该掌握3点内容:(1)首先你肯定要知道在java中多线程实现的几种方法,知道线程的生命周期是怎么样(2)由于多个线程使用相同的进程空间,会存在同时读写相同变量的可能,所以会有线程安全问题,我们要知道一些线程同步的技巧;(3)java里面封装了很多可用的工具,面试官也会经常考,比如ThreadLoca这些类是怎么用等等,对于常见的多线程工具我们要知道和了解。

关于这三点内容,我们需要一个一个的来看。我这边总结的可能不如专业的java开发写的细致,但是应对测试开发工程师这个岗位应该是足够了,这里的内容可能也非常考验大家的记忆力和理解力。相关的知识需要自我总结后能够重述给别人讲解那么就说明你已经学会了。

不知道大家的操作系统的底子如何,再开始下面的内容之前我们先来看一下在多线程这一块最常见的第一类问题就是:你了解进程和线程吗?进程和线程的区别是什么?

关于这个问题如果面试官问我的话,我来说一下我的理解:进程是应用程序在操作系统中一次生命周期的体现,他可以被操作系统启动,运行,阻塞,终止等。具体地来说,比如我们在电脑或手机上点击开启一个应用程序之后,操作系统就为这个应用程序分配了一个进程ID,以及相应的程序中所使用的存储空间,并把这些空间和这个进程关联了起来,随后开启了这个进程的从出生到死亡的生命周期。而线程是进程的一部分,一个进程中包含了大于等于1个数量的线程,线程的诞生主要是为了提高程序的并发处理能力,进一步充分利用CPU资源,线程拥有自己独立的运行时所需要的空间,多个线程之间会共用相同进程资源,如内存空间以及打开的文件。

多线程的几种实现方式

那么接下来我们可以看看官网的java文档上如何写java的多线程的,在“Java Concurrency Utilities”章节中列举了所有java中并发编程相关的内容,我们可以先从Tutorial开始看起,本节的内容主要参考来源也是本章内容。

在java中多线程实现有3种方式:

  • 第一种是继承Thread类,并重写其中的run方法。使用的时候用new初始化一个线程,然后通过调用start方法线程就开始它自己的生命周期了。
public class HelloThread extends Thread {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new HelloThread()).start();
    }

}
  • 第二种是实现Runable接口中的run方法,然后用该类初始化一个Thread对象,同样也是调用Thread类中的start方法来开启线程的生命周期。

    public class HelloRunnable implements Runnable {
    
      public void run() {
          System.out.println("Hello from a thread!");
      }
    
      public static void main(String args[]) {
          (new Thread(new HelloRunnable())).start();
      }
    }
  • 第三种方式 使用Callable接口实现,该接口运行线程在运行结束时携带执行结果。但是因为要存储线程返回的结果,所以一般的threa调用来初始化并start线程的方法对该类不适用,Callable的实现类要和FutureTask这个继承了Future接口的类一起使用。在前两种方式中,官方推荐大家使用的是Runnable方式,因为Thread方式需要继承,而java中一个类只能继承一个父类,然而可以实现多个接口,所以推荐大家使用的是第二种方式。

  • 第四,在java 5之后,java提供了基于Execotor接口的更加灵活的线程启动方式。原话是这样的:“To abstract thread management from the rest of your application, pass the application's tasks to an executor.”,大意就是利用一个高级别的executor来进行线程池的启动和管理。 主要涉及到了Executor、ExecutorService这个两个接口,以及Executors这个工具类,来帮助我们创建一系列可使用的线程池。具体内容可参考博客:https://blog.csdn.net/weixin_40304387/article/details/80508236, 写的还是十分详细的。我们解析来就简单的写个例子来练习Callable、FutureTask、ExecutorService。因为只有ExecutorService中submit方法可兼容Callable接口 所以在管理Callable类的线程时要使用ExecutorService接口类。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @项目: basic.demo
 * @描述: 学习
 * @作者: 一叶浮尘
 * @创建日期: 2020-08-09
 **/

//Callable是一个支持泛型的接口(因为我们不知道线程想返回的是什么),所以需要支持泛型
// 里面只有一个call需要重载实现 并且返回值就是我们在类定义中指定的泛型,这个方法是入参为空
class CallableDemo implements Callable<Integer>{

    @Override
    public Integer call() {

        System.out.println("call...");

        return 1;
    }
}

public class ExecutorsTest {

    public static void main(String[] args) {


        //Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方***阻塞直到任务返回结果。
        //FutureTask  是Future接口的实例
        List<Future> list = new ArrayList<>();
        for(int i=0; i< 10; i++){

            //第一种启动callable线程的方式,使用FutureTask
            //FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
            // new Thread(futureTask).start(); //第一种启动callable线程的烦死后

            //上面的也可以写作下面找各种方式:第二种方式
            //FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
            //ExecutorService  executorService = Executors.newCachedThreadPool();
            //executorService.submit(futureTask);
            //executorService.shutdown();

            //第三种方式:这种方式是借助ExecutorService这个高级管理线程的类拉进行线程的管理,使用Future
            ExecutorService  executorService = Executors.newCachedThreadPool();
            Future<Integer> futureTask = executorService.submit(new CallableDemo());
            list.add(futureTask);
        }
        for(Future temp: list){
            try {
                System.out.println(temp.get());//取回线程执行结果的方式,需要从FutureTask中获取。

            }catch (Exception e){
                System.out.println("null");
            }
        }

    }


}

Thread类中的实用方法

在Thread类中有一些实用的static类型的方法,可以作为了解进行学习。

Thread.sleep(4000); //让该线程sleep 4s中,这个通常在做ui自动化的时候是常用的用于维护ui自动化case稳定性常用的方法。
t.join();//这是一个我一直会搞混淆的一个命令,join主要用来指定程序的运行顺序

我们来看一下下面这段代码来进一步理解join的作用:

public class ThreadUsing {

    public static void main(String[] args) throws Exception{

        System.out.println("MainThread thread run start......");

        Thread thread1 = new MyThred();
        Thread thread2 = new Thread(new MyRunnable());

        thread1.start();
        thread2.start();
        //thread1.join();
        //thread2.join();
        System.out.println("MainThread thread run end......");

    }

}

这段代码在main线程中开启了两个子线程,在没有使用thread1.join()和thread2.join()的时候,我们执行main线程会得到如下的执行顺序:

可以看到main线程没有等待thread1和thread2运行结束,就结束其线程的执行,如果我们把注释放开,则会得到如下的执行结果:

可以看到main线程会等待thread1和thread2运行结束再继续执行,因此join的使用主要是为了让主线程要等待子线程完成后再继续进行执行的一种方式。

多线程同步方式

  • 用synchronized method方法,表明在同一个时间内只允许一个线程执行该方法

    public class SynchronizedCounter {
      private int c = 0;
    
      public synchronized void increment() {
          c++;
      }
    
      public synchronized void decrement() {
          c--;
      }
    
      public synchronized int value() {
          return c;
      }
    }
  • 用 synchronized statement
    在java中每个object都有一个内置都锁,我们可以利用该锁来进行线程同步段落的声明。就像下面这个用this这个Counter.lock 这个object作为同步的一个标识位。如果没有 synchronized (Counter.lock)这个同步的标识,那么执行main函数后,最后count值是非常随机的,但是有了同步的技巧之后,最后输出的count值一定是0。

/**
 * @项目: basic.demo
 * @描述: 共享变量测试
 * @作者: lujuan03
 * @创建日期: 2020-01-13
 **/

class Counter{

    public static int count=0;
    public static final Object lock=new Object();
}

class AddThread extends Thread{

    @Override
    public void run(){
        for(int i=0; i < 10000;i++)
            synchronized (Counter.lock){
                Counter.count+=1;
            }

    }
}

class DecThread extends Thread{

    @Override
    public void run(){
        for(int i=0; i< 10000;i++)
           synchronized (Counter.lock){
                Counter.count-=1;
            }


    }
}

public class ShareValue {

    public static void main(String[] args) throws Exception{

        Thread addth = new AddThread();
        Thread decth = new DecThread();

        addth.start();
        decth.start();

        addth.join();
        decth.join();

        System.out.println(Counter.count);

    }

}
  • 用volatile关键词来声明变量可以保证线程同步
  • 也可以使用object中的wait和notifyAll的使用来达到线程间同步的目的

    线程中的几种常见生命周期问题

一个线程所拥有的基本生命周期如下面这副图所示。

  • 死锁(deadlock)
  • 饥饿(starvation)
  • 活锁(live lock)

其他一些内容

  • 使用jav