【Java高级程序设计学习笔记】多线程

 

目录

1 线程概述

1.1线程相关概念

1.2 线程的创建与启动

1.3 线程的常用方法

1.3.1 currentThread()方法

1.3.2 setName()和getName()

1.3.3 isAlive()

1.3.4 sleep()

2 线程安全问题 

2.1 原子性

2.2 可见性

2.3 有序性

2.3.1 重排序

2.3.2 指令重排序

2.3.3 存储子系统重排序

3 线程同步

3.1 线程同步机制简介

3.2 锁概述

3.2.1 锁的作用

3.2.2 锁相关的概念

3.3 内部锁:synchronized关键字

 3.3.1 synchronized同步代码块


线程是进程中的一个独立控制单元,线程在控制着进程的执行,一个进程中至少有一个线程。多线程可以更好地利用cpu的资源,线程之间还能进行数据共享。在Java中,一个线程是指进程中的一个执行流程,一个进程可以运行多个线程,Java中每个线程都有一个调用栈,即使不在程序中创建任何新的线程,也有一个main()方法运行在一个线程内,称为主线程,一旦创建一个新的线程,就产生一个新的调用栈。本课程是系列专题课程,内容包括线程读写锁ReadWriteLock、线程组ThreadGroup等。

1 线程概述

1.1线程相关概念

 进程

   进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。

   可以把进程简单理解为正在操作系统中运行的一个程序。

 线程

   线程(thread)是进程的一个执行单元

   一个线程是进程中一个单一顺序的控制流,进程的一个执行分支

   进程是线程的容器,一个进程至少有一个线程,也可以有多个线程

   操作系统中,以进程为单位分配资源,如虚拟存储空间,文件描述等。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储

主线程与子线程

  JVM启动时会创建一个主线程,负责执行main方法。

  Java中线程不孤立,A线程中创建了B线程,B线程为A的子线程,A线程为B的父线程。

串行,并发与并行

【Java高级程序设计学习笔记】多线程_第1张图片

 

1)串行Sequential,先做任务A,完成后做B,完成后做C,所有任务逐个完成,共耗时35min

 

【Java高级程序设计学习笔记】多线程_第2张图片

 

2)并发 Concurrent,先做任务A,准备了五分钟,在等待A完成的这段时间内开始做任务B,准备任务B两分钟,等待B完成的过程,开始做任务C。共耗时17min

 

【Java高级程序设计学习笔记】多线程_第3张图片

 

3)并行 Parallel,三个任务同时开始总耗时取决于需要时间最长的任务,总耗时15min

【Java高级程序设计学习笔记】多线程_第4张图片

 

并发可以提高事物的处理效率,即一段时间内可以处理或完成更多事情。

并行是一种更为严格理想的并发。

从硬件角度来说,如果是单核CPU,一个处理器一次只能执行一个线程的情况下。处理器可以使用时间片轮转技术,可以让CPU快速的在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时进行,如果是多核CPU,可以为不同的线程分配不同的CPU内核。

1.2 线程的创建与启动

  在Java中,创建一个线程就是创建一个Thread类的对象(实例)

  Thread类有两个常用的构造方法:Thread()与Thread(Runnable),对应创建线程的两种方式:定义Thread类的子类,定义一个Runnable接口的实现类,无本质区别

1.定义Thread类的子类

**
 * 1定义类继承Thread
 */
public class MyThread extends Thread {
    //2) 重写父类中run()方法
    @Override
    public void run() {
        System.out.println("这是子线程打印的内容");
    }
}

public class text1 {
    public static void main(String[] args) {
        System.out.println("JVM启动main线程,main线程执行main方法");
        //3)创建子线程对象
        MyThread thread = new MyThread();
        //4) 启动线程
        thread.start();
        /**
         * 调用线程的start()方法,启动线程的实质是请求JVM运行相应线程,
         * 这个线程具体在声明时候运行由线程调度器(Scheduler)决定
         * 注意:start方法调用结束不意味着子线程开始运行。
         *      启动新开启的线程会执行run方法。
         *       如果开启了多个线程,start调用的顺序并不一定就是线程启动的顺序
         *       多线程运行结果与代码执行顺序或调用顺序无关
         */
        System.out.println("main线程后面其他的代码");
    }
}

public class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("sub thread:" + i);
            int time = (int) (Math.random() * 1000);
            try {
                Thread.sleep(time); //线程睡眠,单位是毫秒,1s=1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


/**
 * 演示线程运行结果的随机性
 */
public class text {
    public static void main(String[] args) {

        MyThread2 thread2 = new MyThread2();
        thread2.start(); //开启子线程
        for (int i = 1; i <= 10; i++) {
            System.out.println("main:" + i);
            int time = (int) (Math.random() * 1000);
            try {
                Thread.sleep(time); //线程睡眠,单位是毫秒,1s=1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


 

运行结果:

【Java高级程序设计学习笔记】多线程_第5张图片

 

2.实现Runnable接口创建线程

public class MyRunnable implements Runnable {

    // 重写Runnable接口中的抽象方法
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("sub thread-->" + i);
           
        }  
}

}
public class Text {
    public static void main(String[] args) {
        // 创建Runnable 的实现类对象
        MyRunnable runnable = new MyRunnable();
        // 创建线程对象
        Thread thread = new Thread(runnable);
        // 启动线程
        thread.start();

        for (int i = 1; i <= 100; i++) {
            System.out.println("main-->" + i);
        }
    }
}

有时Thread(Runnable)构造方法,实参也会传递匿名内部类对象

Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i =1;i<=100;i++){
            System.out.println(i);
        }
    }
});

1.3 线程的常用方法

1.3.1 currentThread()方法

    Thread.currentThread()方法可以获得当前线程

     Java中的任何一段代码都是执行在某个线程当中的,执行当前代码的线程就是当前线程

     同一段代码可能被不同的线程执行,因为当前线程是相对的.

public class MyRunnable implements Runnable {

    // 重写Runnable接口中的抽象方法
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("sub thread-->" + i);
           
        }

    }
}
/**
 * 定义线程类,分别在构造方法中和run方法中打印当前线程
 */
public class SubSThread1 extends Thread {
    public SubSThread1() {
        System.out.println("构造方法打印当前线程的名称:" + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run方法中打印当前线程名称:" + Thread.currentThread().getName());
    }

}

1.3.2 setName()和getName()

  设置线程名称,有助于程序调试,提高程序可读性,建议为每个线程都设置一个能够体现线程功能的名称。

1.3.3 isAlive()

  判断当前线程是否处于活动状态,活动状态是线程已启动,且尚未终止。

1.3.4 sleep()

  让当前线程休眠指定的毫秒数

  当前线程值得是Thread.currentThread()

利用sleep制作倒计时1.3.5 getUd

public class TextSleep {
    public static void main(String[] args) {
        int remaining = 60;

        while (true) {
            System.out.println("Remaining: " + remaining);
            remaining--;

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

1.3.5 getid()     

  返回线程的唯一编号

1.3.6 yield()

  放弃当前cpu资源。

1.3.7 setPrority()

  thread.setPriority(num);设置线程的优先级。

   java线程的优先级取值范围为1~10,如果超出则会抛出异常。

   在操作系统中,优先级较高的线程获得CPU资源越多。

   线程的优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程,注意不能保证优先级高的线程先运行。

  Java优先级设置不得当或滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。

 线程的优先级不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级。

1.3.8 interrupt()

  中断线程,调用该方法只是在当前线程打一个停止标志,并不是真正的停止线程。如果想真正的中断,可以在run中利用isInterrupted()判断是否有中断标志,自行设置令run方法体执行完毕,如直接return。

1.3.9 setDaemon()

   Java中的线程分为用户线程与守护线程

    守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。

    守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出

1.4 线程的生命周期

   线程的生命周期是线程对象的生老病死,即线程的状态

    线程的生命周期可以用getState()获得,线程的状态是Rhread.State枚举类型定义的,有以下几种:

 NEW,新建状态,创建了线程对象,在调用start()启动之前的状态;

 RUNNABLE,可运行状态,复合状态,包含READYRUNNING两个状态,READY状态该线程可以被线程调度器进行调度使它处于RUNNING状态,RUNNING状态表示该线程正在执行,正在执行run方法,Thread.yield()可以把线程从RUNNING转为READY状态

  BLOCKED阻塞状态,线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态。处于阻塞状态的线程不会占用CPU资源,当阻塞I/O操作执行完,或者线程获得了其申请的资源,线程可转换为RUNNABLE。

  WAITING等待状态,线程执行了object.wait(),thread.join()会把线程转换为等待状态,执行object.noify()或加入的线程执行完毕,线程会转换为RUNNABLE状态。

  TIMED_WAITING状态,等待状态,但是处于该状态的线程不会无限等待,如果线程没有在指定的时间范围内完成期望的操作,该线程会自动转换为RUNNABLE。

  TERMINATED状态,终止状态。

【Java高级程序设计学习笔记】多线程_第6张图片

 1.5 多线程编程的优势与所存在的风险

优势:1)提高系统吞吐率(Throughout),可以使一个进程由多个并发,即同时进行的操作

2)提高响应性(Responsiveness)。Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间

3)充分利用多核处理器资源,通过多线程可以充分利用多核CPU资源

风险:

  1. 线程安全问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生一致性问题,如读取脏数据(过期的数据),如数据丢失更新。
  2. 线程活性问题,由于程序自身的缺陷或者由于资源稀缺性导致线程一直出于非RUNNABLE状态,这就是线程活性问题,常见的火星故障有以下几种:
  1. 死锁(Deadlock),类似于鹬蚌相争。家门钥匙在家里,备用钥匙在车里但是车在家里。
  2. 锁死(Lockout),类似于睡美人中王子挂了。
  3. 活锁(Livelock),类似于猫咬自己的尾巴
  4. 饥饿(Starvation),类似于健壮的雏鸟总是从母鸟嘴里抢到食物。

   3. 上下文切换,处理器从执行一个线程切换到执行另外一个线程

   4. 可靠性,可能会由一个线程导致JVM意外终止,其他线程也无法执行。

2 线程安全问题 

  非线程安全主要指多个线程对同一个对象的实例变量进行操作的时候,会出现值被更改,值不同步的情况。

  线程安全问题表现为三个方面:原子性,可见性,有序性

​​​​​​​2.1 原子性

  原子就是不可分割的意思,原子操作的不可分割有两层含义:

  1. 访问某个共享变量的操作从其他线程来看,该操作要么以及执行完毕,要么尚未发生,其他线程看不到当前操作的中间结果。
  2. 访问同一组共享变量的原子操作是不能够交错的。如现实生活中从ATM机取款,要么用户拿到钱余额减少,要么没拿到没相当于操作没发生。

  Java中有两种方式实现原子性:锁和利用处理器的CAS(Compare和Swap)指令。

  锁具有排它性,保证共享变量某一时刻只能被一个线程访问

  CAS指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。


/**
 * 线程的原子性问题
 */
public class Test01 {
    public static void main(String[] args) {

        //启动两个线程,不断调用getNum()方法
        MyInt myInt = new MyInt();

        for (int i = 1; i <= 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        System.out.println(Thread.currentThread().getName() +
                                "-->" + myInt.getNum());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }

    }

    static class MyInt {
        int num;

        public int getNum() {
            return num++;
        }
    }
}

【Java高级程序设计学习笔记】多线程_第7张图片

 

 

  ++自增操作实现步骤是先读取num,num自增,把自增的值再赋值给num变量。
    在java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性,将MyInt类改为:

static class MyInt {

    AtomicInteger num = new AtomicInteger();

    public int getNum() {
        return num.getAndIncrement();
    }
}

2.2 可见性

 在多线程环境中,一个线程对某个共享变量进行更新后,后续其他线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性(visbility)

  如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。

  多线程的程序因为可见性问题让其他线程读到脏数据


import java.util.Random;

/**
 * 测试线程可见性
 */
public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        MyTask task = new MyTask();
        new Thread(task).start();

        Thread.sleep(1000);
        //主线程一秒后取消子线程
        task.cancel();

    }

    static class MyTask implements Runnable {
        private boolean toCancel = false;

        @Override
        public void run() {
            while (!toCancel) {
                if (doSomething()) {

                }
                if (toCancel) {
                    System.out.println("任务被取消");
                } else {
                    System.out.println("任务正常结束");
                }
            }

        }

        private boolean doSomething() {
            System.out.println("执行某个任务");
            try {
                //模拟执行任务的时间
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }

        public void cancel() {
            toCancel = true;
            System.out.println("收到取消线程的消息");
        }
    }
}

可能会出现以下情况:

  main线程中调用了task.cancel()方法,把task对象的toCancel变量修改为true,可能存在子线程看不到main线程对toCancel做的修改,子线程中toCancel变量一直为false。

原因:1)JIT即时编译器可能会对run方法中while循环进行有华为:

if(!toCancel){

while (true) {

        if (doSomething()) {}

}

}

2)可能与计算机的存储系统有关,假设分别有两个CPU内核运行main线程与子线程,一个CPU内核无法立即读取另外一个CPU内核中的数据。

2.3 有序性

有序性,(Ordering)是指什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of order)

  乱序指的是内存访问操作顺序看起来发生了变化

2.3.1 重排序

  重排序,在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:

  1. 编译器可能会改变两个操作的先后顺序;
  2. 处理器也可能不会按照目标代码的顺序执行、

  这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序。

  重排序是对内存访问有序操作的一种优化,可以在不影响单线程正确的情况下提示程序的性能,但可能对多线程程序的正确性产生影响,即可能导致线程安全问题,重排序与可见性问题类似不是必然出现的。

  与内存操作有关的几个概念:

  1. 源代码顺序,源码中指定的内存访问顺序
  2. 程序顺序,处理器上运行的目标代码所指定的内存访问顺序
  3. 执行顺序,内存访问操作在处理器上实际执行顺序
  4. 感知顺序,给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序。

可以把重排序分为指令重排序存储子系统重排序两种:

 指令重排序主要由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样。

  存储子系统是高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致。

2.3.2 指令重排序

  在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序。

  指定重排是一种动作,确实对指令的顺序做了调整,重排序的对象是指令。

  javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序。

  CPU处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致。

  指令重排不会对单线程程序的结果正确性产生影响,

2.3.3 存储子系统重排序

  存储子系统是指写缓冲器与高速缓存。

  高速缓存是CPU中为了匹配与主内存处理速度不匹配而设计的一个高速缓存。

  写缓冲器用来提高写高速缓存操作的效率。

  即使处理器严格按照程序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。

  存储子系统重排序并没有真正的对指令执行的顺序进行调整,而是造成一种指令执行顺序被调整的假象。

   存储子系统重排序的对象是内存操作的结果。

  从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为load操作;写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作,内存冲排序有一下四种可能

  1. LoadLoad重排序,一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2->L1
  2. StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2->W1
  3. LoadStore重排序,一个处理器先执行读内存操作L1再执行写内存操作W1,其他处理器对两个内存操作的感知顺序可能是W1->L1
  4. StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作L1,其他处理器对两个内存操作的感知顺序可能是L1->W1。

  内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同。

  内存重排序可能会导致线程安全问题。

2.4 Java内存模型

【Java高级程序设计学习笔记】多线程_第8张图片

 

  1. 每个线程都有独立的栈空间
  2. 每个线程都可以访问堆内存
  3. 计算机的CPU不直接从主内存中读取数据,CPU读取数据时,先把主内存的数据读到Cache缓存中,把Cache中的数据读到Register 寄存器中。
  4. JVM中共享的数据可能会被分配到Register寄存器中,每个CPU都有自己的Register寄存器,一个CPU不能读取其他CPU上寄存器中的内容,如果两个线程分别运行在不同的处理器上,而这个共享的数据被分配到寄存器上,会产生可见性问题。
  5. 即使JVM中共享数据分配到主内存中,也不能保证数据的可见性,CPU不直接对主内存访问,而是通过Cache高速缓存进行的。一个处理器上运行的线程对数据的更新只是更新到处理器的写缓冲器,还没有到达Cache缓存,会产生运行在另外一个处理器上的线程无法看到该处理器对共享数据的更新。
  6. 一个处理器的高速缓存的Cache不能直接读取另外一个处理器的Cache,但是一个处理器可以通过缓存一致性协议来读取其他处理器缓存中的数据,并将读取的数据更新到该处理器的Cache中。这个过程称为缓存同步。缓存同步使得一个处理器上运行的线程可以读取到另外一个处理器上运行的线程对共享数据所做的更新,即保障了可见性,为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的Cache,这个过程称为冲刷处理器缓存

可以把Java内存模型抽象为:

【Java高级程序设计学习笔记】多线程_第9张图片

 

3 线程同步

3.1 线程同步机制简介

   线程同步机制是一套用于协调线程之间的数据访问机制,该机制可以保障线程的安全。

  Java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,static关键字,以及相关的API,如Object.waite(),Object.notify()等。

3.2 锁概述

  线程安全问题的产生前提是多个线程并发访问共享数据。

  将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,锁就是复用这种思路保障线程安全的。

  锁(Lock)可以理解为对共享数据进行保护的一个许可证,对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须有许可证,一个线程只有持有许可证的情况下才能对这些共享数据进行访问,并且一个许可证一次只能被一个线程持有,许可证线程在结束对共享数据访问后必须释放其持有的许可证。

  一线程在访问共享数据前必须先获得锁,获得锁的线程称为锁的持有线程;一个锁一次只能被一个线程持有。锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码称为临界区。

  锁具有排他性,即一个锁一次只能被一个线程持有,这种锁称为排他锁和互斥锁。

【Java高级程序设计学习笔记】多线程_第10张图片

 

JVM把锁分为内部锁和显示锁两种锁,内部锁通过synchronized关键字实现,显示锁通过Lock接口的实现类实现。

3.2.1 锁的作用

  锁可以实现对共享数据的安全访问,保障线程的原子性,可见性,与有序性。

  锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区代码一次只能被一个线程执行。使得临界区代码所执行的操作自然而然地具有不可分割的特性,即剧本了原子性。

  可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在java平台中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。

  锁能够保证有序性,写线程在临界区所执行的操作在读线程所执行的临界区看来像是完全按照源码顺序执行的,

  注意:

      使用锁保障线程安全性,必须满足:

  1. 这些线程在访问共享数据时必须使用同一个锁
  2. 即使是读取共享数据也需要使用同步锁。

3.2.2 锁相关的概念

1)可重入性

     描述这样一个问题:一个线程持有该锁的时候能否多次申请

锁。

  如果一个线程持有一个锁的时候还能继续成功申请该锁,称该锁是可重入的,否则就称该锁为不可重入的

2)锁的争用和调度

  java平台中内部锁属于非公平锁,显示Lock锁支持公平锁和非公平锁。

  1. 锁的粒度

 一个锁可以保护的共享数据的数量大小称为锁的粒度,锁保护共享数据量大,称为锁的粒度粗,粒度过粗会导致申请锁的时候会进行不必要的等待,锁的粒度过细会增加所的开销。

3.3 内部锁:synchronized关键字

  Java中每个对象都有一个 与之关联的内部锁,这种锁也成为监视器,这种内部锁是一种排他锁,可以保障原子性,可见性,有序性。

  内部锁是通过synchronized关键字实现的,关键字修饰代码块,修饰该方法。

【Java高级程序设计学习笔记】多线程_第11张图片

 修饰实例的方法称为同步实例方法,修饰静态方法称为同步静态方法。

 3.3.1 synchronized同步代码块

/**
 * synchronized同步代码块
 */
public class Test01 {
    public static void main(String[] args) {
        //创建两个线程,分别调用mm方法
        //先创建Test01对象,通过对象名调用
        Test01 obj = new Test01();

        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm(); //锁对象obj
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm();  //锁对象obj
            }
        }).start();

    }

    //定义方法打印100行字符串
    void mm() {
        synchronized (this) { //经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++)
                System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}


 

你可能感兴趣的:(java,java学习笔记,java,学习,开发语言)