Java (三)多线程

1  线程概述

1.1  进程

  在操作系统中,每个独立执行的程序都可称为一个进程,也就是 “正在运行的程序”。

  实际上,进程不是同时运行的,对于一个 CPU 而言,某个时间段只能运行一个程序,也就是只能执行一个进程。操作系统会为每个进程分配一段有限的 CPU 使用时间,CPU 在这段时间内执行某个进程,然后会在下一段时间切换到另一个进程中去执行。

1.2  线程

  在一个进程中还可以有多个执行单元同时运行,这些执行单元被称为线程。

  操作系统中至少存在一个线程。

  多线程成语运行时,每个线程之间都是独立的,他们可以并发执行。和进程一样,也是由CPU轮流执行的。

1.3  进程和线程的区别

  每个进程都有独立的代码和数据空间。线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间。

  最根本区别:进程是资源分配的单位,线程是调度和执行的单位。

  多进程:在操作系统中能同时运行多个任务(程序)。

  多线程:在同一应用程序中有多个顺序流同时进行。

2  线程的创建

  Java 提供了两种多线程实现方式,一种是继承 java.lang 包下的 Thread 类;另一种是实现 java.lang.Runnable 接口。

2.1  继承 Thread 类创建多线程

  JDK 中提供了一个线程类 Thread,通过继承 Thread 类,并重写 Thread 类中的 run() 方法便可实现多线程。

  在 Thread 类中,提供了一个 start() 方法用于启动新线程,线程启动后,系统会自动调用 run() 方法。

【例1-1】实现一个简单的多线程

public class Example01 {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        while (true) {
            System.out.println("main()方法正在运行");
        }
    }
}
class MyThread extends Thread {
    public void run() {
        while (true) {
            System.out.println("Mythread()的run()方法正在运行。");
        }
    }
}

运行结果如下:

  从图中的运行结果可以看出,两个 while 循环中的打印语句轮流执行。说明该实例实现了多线程。

2.2  实现 Runnable 接口创建多线程

  Thread 有个缺陷:Java 中只支持单继承,一个类如果继承了某个父类就不能再继承 Thread 类了。为了克服弊端,Thread 类提供了另一个构造方法 Thread(Runnable target),该方法中,Runnable 是一个接口,它只有一个 run() 方法。当应用时,只需要为该方法提供一个实现了 Runnable 接口的实例对象,这样创建的线程将调用实现了 Runnable 接口中的 run() 方法作为运行代码。

【例2-1】Runnable 接口实现多线程

public class Example02 {
    public static void main(String[] args) {
        MyThread2 mt = new MyThread2();
        Thread thread = new Thread(mt);
        thread.start();
        while (true) {
            System.out.println("main()方法正在运行。");
        }
    }
}

class MyThread2 implements Runnable {

    @Override
    public void run() {     //当调用start()方法时,线程从此处开始执行
        while (true) {
            System.out.println("MyThread类的run()方法正在运行。");
        }
    }
}

运行结果如下:

   MyThread 类实现了 Runnable 接口,并重写了 Runnable 接口中的run() 方法,通过 Thread 类的构造方法将 MyThread 类的实例对象作为参数传入。由运行结果可以看出,实现了多线程。

2.3  两种方法对比

  实现 Runnable 接口相对于继承 Thread 类来说,有如下好处:

    适合多个相同程序代码的线程处理同一个资源的情况。

    避免 Java 单继承带来的局限性。

【例2-2】使用 Runnable 实现四个售票窗口同时售票

public class Example03 {
    public static void main(String[] args) {
        TicketWindow tw = new TicketWindow();
        new Thread(tw, "窗口1").start();
        new Thread(tw, "窗口2").start();
        new Thread(tw, "窗口3").start();
        new Thread(tw, "窗口4").start();
    }
}
class TicketWindow implements Runnable {
    private int tickets = 100;
    @Override
    public void run() {
        while (true) {
            if (tickets >= 0) {
                Thread th =Thread.currentThread();
                String th_name = th.getName();
                System.out.println(th_name + ":正在发售第 " + tickets-- + " 张票");
            }
        }
    }
}

 

运行结果如下:

 

  示例2-2中只创建了一个 TicketWindow 对象,然后创建了四个线程,在每个线程上都去调用这个 TicketWindow 对象中的 run() 方法,这样就可以确保四个线程访问的是同一个 tickets 变量,共享100张票。

2.4  后台线程

  新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了 setDaemon(true) 语句,这个线程就变成了一个后台线程。

  如果一个进程中只有后台线程运行,这个进程就会结束。

 

3  线程的生命周期以及状态转换

3.1  生命周期图

(两张图,结合看效果更佳)

 

3.2  线程状态

  1、新建状态(new)

   使用new关键字和Thread类或其子类建立一个线程对象后,该对象就处于新建状态。此时对象不能运行,仅仅由Java虚拟机为其分配了内存。

  2、就绪状态(Runnable)

  线程对象调用了start()方法之后,线程就进入了就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  3、运行状态(Running)

   当处于就绪状态的线程获得CPU使用权时,该线程就开始执行run()方法。当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。

  4、阻塞状态(Blocked)

  一个正在执行的线程在某些特殊情况下(如执行耗时的输入/输出操作时)会放弃CPU的使用权,进入阻塞状态。

  • 线程试图获取对象的同步锁时,如果该锁被其他线程持有,则当前线程会进入阻塞状态。
  • 当线程调用一个阻塞式的IO方法时,该线程会进入阻塞状态。
  • 当线程调用了某个对象的wait()方法时,会进入阻塞状态。需要使用notify()方法唤醒该线程。
  • 当线程调用了sleep()方法时,会进入阻塞状态
  • 当一个线程中调用了另一个线程的join()方法时,会进入阻塞状态。需要等另一个线程结束后,该线程才会结束阻塞状态。

  5、死亡状态(Terminated)

  run()方法执行完,或者抛出一个异常(Exception)或错误(Error),线程就进入死亡状态。(死了也就死了,别想着复活啥的)

3.3  终止线程的典型方式(重要)

   终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。

【示例3-1】终止线程

public class TestTermination implements Runnable{
    private String name;
    private boolean live = true;// 标记变量,表示线程是否可中止;
    private TestTermination(String name) {
        super();
        this.name = name;
    }
    public void run() {
        int i = 0;
        //当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
        while (live) {
            System.out.println(name + (i++));
        }
    }
    public void terminate() {   //终止方法
        live = false;
    }

    public static void main(String[] args) {
        TestTermination tt = new TestTermination("线程A:");
        Thread t1 = new Thread(tt);// 新生状态
        t1.start();// 就绪状态,此时会执行run()方法
        for (int i = 0; i < 15; i++) {
            System.out.println("主线程" + i);
        }
        tt.terminate();
        System.out.println("tt 终止!");
    }
}

输出结果如下:

  这里发现一个有意思的事情,当 i 的值很小的时候,多运行几次会出现一种情况:for循环在一个CPU分配的时间片内已经运行结束,并执行terminate()方法终止了进程,run()方法都没来得及运行。

 

4  线程调度

4.1  线程的优先级

  在应用程序中,最直接的线程调度方式就是设置线程的优先级。优先级越高,线程获得CPU执行的机会越大。线程的优先级用1~10之间的整数来表示,数字越大优先级越高,默认为5。

  Thread类中还提供了三个静态常量表示线程优先级,如下表所示:

Thread类的静态常量

功能描述

static int MAX_PRIORITY 表示线程的最高优先级,相当于10
static int MIN_PRIORITY 表示线程的最低优先级,相当于1
static int NORM_PRIORITY 表示线程的缺省优先级,相当于5

注意:

  1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选
  2. 使用下列方式获得或设置线程对象的优先级。
    1. int getPriority();
    2. void setPriority(int newPriority);
  3. 优先级低知识意味着获取调度的可能性低。并不意味着:优先级高的一定比优先级低的先调用。

关键代码如下:

t1.setPriority(1);
t2.setPriority(10);

 

4.2  暂停线程

 暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:

      1. sleep()方法:线程休眠,可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。

      2. yield()方法:线程让步,可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

【示例4-1】sleep()方法

public class TestThreadState {
    public static void main(String[] args) {
        StateThread thread1 = new StateThread();
        thread1.start();
        StateThread thread2 = new StateThread();
        thread2.start();
    }
}
//使用继承方式实现多线程
class StateThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
            try {
                Thread.sleep(2000);//调用线程的sleep()方法;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  这里的输出结果就不展示了,因为实际效果只有运行时可以看到。其效果大致是每隔两秒打印一次语句。

【示例4-2】yield()方法

public class TestThreadState {
    public static void main(String[] args) {
        StateThread thread1 = new StateThread();
        thread1.start();
        StateThread thread2 = new StateThread();
        thread2.start();
    }
}
//使用继承方式实现多线程
class StateThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
            Thread.yield();//调用线程的yield()方法;
        }
    }
}

运行结果如图所示: 

 

  从运行效果来看,该方法并没有明显的延迟,很快就运行完成。该方法可以引起线程切换。

 4.3  线程的联合join()

   也叫线程插队,线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。

【示例4-3】join()方法

public class TestThreadState {
    public static void main(String[] args) {
        System.out.println("爸爸和儿子买烟故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
 
class FatherThread implements Runnable {
    public void run() {
        System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买包红塔山大经典");
        Thread son = new Thread(new SonThread());
        son.start();
        System.out.println("爸爸等儿子买烟回来");
        try {
            son.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出门去找儿子跑哪去了");
            // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
            System.exit(1);
        }
        System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");
    }
}
 
class SonThread implements Runnable {
    public void run() {
        System.out.println("儿子出门去买烟");
        System.out.println("儿子买烟需要10分钟");
        try {
            for (int i = 1; i <= 10; i++) {
                System.out.println("第" + i + "分钟");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("儿子买烟回来了");
    }
}

运行结果如下:

  这里忽然就想到了继承里面的初始化方式。这里就额外的提几句。

假设:两个类A和B,B继承A,AB中均有静态方法和静态成员变量,那么初始化过程是这样的:

首先,初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化; 
然后,初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化; 
其次,初始化父类的普通成员变量和代码块,再执行父类的构造方法;
最后,初始化子类的普通成员变量和代码块,再执行子类的构造方法; 
 
获取线程基本信息
// 关键代码如下
Runnable r = new MyThread();
Thread t = new Thread(r,"Test");

Thread.currentThread();
t.isAlive();
t.getPriority();
t.setPriority();
t.setName();
t.getName();

4.4  多线程同步

  处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。

  线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。比如说前面提到的售票服务就是多个线程共享一个对象。

   由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。Java中使用synchronized关键字来修饰共享资源代码块。

A)synchronized 方法

  通过在方法声明中加入 synchronized关键字来声明,语法如下:

public synchronized void accessVal(int newVal);

  synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

B)synchronized 块

  块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。语法格式如下:

synchronized (lock) {
    //操作共享资源代码块
}    

【示例4-4】

public class TestSync {
    public static void main(String[] args) {
        Account a1 = new Account(100, "高");
        Drawing draw1 = new Drawing(80, a1);
        Drawing draw2 = new Drawing(80, a1);
        draw1.start(); // 你取钱
        draw2.start(); // 你老婆取钱
    }
}
/*
 * 简单表示银行账户
 */
class Account {
    int money;
    String aname;
    public Account(int money, String aname) {
        super();
        this.money = money;
        this.aname = aname;
    }
}
/**
 * 模拟提款操作
 *
 * @author Administrator
 *
 */
class Drawing extends Thread {
    int drawingNum; // 取多少钱
    Account account; // 要取钱的账户
    int expenseTotal; // 总共取的钱数

    public Drawing(int drawingNum, Account account) {
        super();
        this.drawingNum = drawingNum;
        this.account = account;
    }

    @Override
    public void run() {
        draw();
    }

    void draw() {
        synchronized (account) {
            if (account.money - drawingNum < 0) {
                System.out.println(this.getName() + "取款,余额不足!");
                return;
            }
            try {
                Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money -= drawingNum;
            expenseTotal += drawingNum;
        }
        System.out.println(this.getName() + "--账户余额:" + account.money);
        System.out.println(this.getName() + "--总共取了:" + expenseTotal);
    }
}

运行效果如下图所示:

解释:

  “synchronized (account)” 意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。 Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。

 4.5  死锁问题

  有这样一个场景:小白和小蓝都要“化妆”,“化妆”需要“镜子”和“口红”。在“化妆”过程中,小白拿了“镜子”,小蓝拿了“口红”。

  小白说:你先给我口红,让我画完,我再给你。

  小蓝说:你先给我镜子,让我画完,我再给你。

  两人据理力争,寸步不让,啊~~~ 天长地久,海枯石烂……

  两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象叫做死锁。

【示例4-5】死锁问题

package com.zzfan.runnable;
class Lipstick {//口红类

}
class Mirror {//镜子类

}
class Makeup extends Thread {//化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    @Override
    public void run() {
        // TODO Auto-generated method stub
        doMakeup();
    }

    void doMakeup() {
        if (flag == 0) {
            synchronized (lipstick) {//需要得到口红的“锁”;
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (mirror) {//需要得到镜子的“锁”;
                    System.out.println(girl + "拿着镜子!");
                }

            }
        } else {
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(girl + "拿着口红!");
                }
            }
        }
    }

}

public class TestDeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup();//小白的化妆线程;
        m1.girl = "小白";
        m1.flag = 0;
        Makeup m2 = new Makeup();//小蓝的化妆线程;
        m2.girl = "小蓝";
        m2.flag = 1;
        m1.start();
        m2.start();
    }
}

运行结果如下图所示:

  从运行状态可以看出,两个线程都在等对方的资源,都处于停滞状态。

死锁的解决方法

      死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。 如上面的死锁案例,修改成示例10-11所示。

【示例4-6】死锁问题的解决

package com.zzfan.runnable;

class Lipstick {//口红类

}
class Mirror {//镜子类

}
class Makeup extends Thread {//化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    @Override
    public void run() {
        // TODO Auto-generated method stub
        doMakeup();
    }

    void doMakeup() {
        if (flag == 0) {
            synchronized (lipstick) {
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
            }
        } else {
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick) {
                System.out.println(girl + "拿着口红!");
            }
        }
    }
}

public class TestDeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup();// 小白的化妆线程;
        m1.girl = "小白";
        m1.flag = 0;
        Makeup m2 = new Makeup();// 小蓝的化妆线程;
        m2.girl = "小蓝";
        m2.flag = 1;
        m1.start();
        m2.start();
    }
}

运行结果如下图所示:

  程序正常结束。perfect!

5  线程并发

(转载自https://www.sxt.cn/Java_jQuery_in_action/eleven-threadconcurrent-collaboration.html

     

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

Ø 什么是生产者?

      生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是消费者?

      消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是缓冲区?

      消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

图11-17 生产者消费者示意图.png

 

缓冲区是实现并发的核心,缓冲区的设置有3个好处:

Ø 实现线程的并发协作

      有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。

Ø 解耦了生产者和消费者

      生产者不需要和消费者直接打交道。

Ø 解决忙闲不均,提高效率

      生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。

【示例5-1】生产者与消费者模式

public class TestProduce {
    public static void main(String[] args) {
        SyncStack sStack = new SyncStack();// 定义缓冲区对象;
        Shengchan sc = new Shengchan(sStack);// 定义生产线程;
        Xiaofei xf = new Xiaofei(sStack);// 定义消费线程;
        sc.start();
        xf.start();
    }
}
 
class Mantou {// 馒头
    int id;
 
    Mantou(int id) {
        this.id = id;
    }
}
 
class SyncStack {// 缓冲区(相当于:馒头筐)
    int index = 0;
    Mantou[] ms = new Mantou[10];
 
    public synchronized void push(Mantou m) {
        while (index == ms.length) {//说明馒头筐满了
            try {
               //wait后,线程会将持有的锁释放,进入阻塞状态;
               //这样其它需要锁的线程就可以获得锁;
                this.wait();
                //这里的含义是执行此方法的线程暂停,进入阻塞状态,
                //等消费者消费了馒头后再生产。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 唤醒在当前对象等待池中等待的第一个线程。
        //notifyAll叫醒所有在当前对象等待池中等待的所有线程。
        this.notify();
        // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。
        ms[index] = m;
        index++;
    }
 
    public synchronized Mantou pop() {
        while (index == 0) {//如果馒头筐是空的;
            try {
                //如果馒头筐是空的,就暂停此消费线程(因为没什么可消费的嘛)。
                this.wait();                //等生产线程生产完再来消费;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        index--;
        return ms[index];
    }
}
 
class Shengchan extends Thread {// 生产者线程
    SyncStack ss = null;
 
    public Shengchan(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("生产馒头:" + i);
            Mantou m = new Mantou(i);
            ss.push(m);
        }
    }
}
 
class Xiaofei extends Thread {// 消费者线程;
    SyncStack ss = null;
 
    public Xiaofei(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            Mantou m = ss.pop();
            System.out.println("消费馒头:" + i);
 
        }
    }
}

(这个代码没跑过,直接copy过来的,不过应该没问题)

运行结果如下图所示:

      

线程并发协作总结:

      线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:

      1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

      2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。

      3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

      4. 在生产者消费者问题中,仅有synchronized是不够的。

        · synchronized可阻止并发更新同一个共享资源,实现了同步;

        · synchronized不能用来实现不同线程之间的消息传递(通信)。

      5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:

表11-2 线程通信常用方法.png

      6. 以上方法均是java.lang.Object类的方法;

      都只能在同步方法或者同步代码块中使用,否则会抛出异常。

老鸟建议

      在实际开发中,尤其是“架构设计”中,会大量使用这个模式。 对于初学者了解即可,如果晋升到中高级开发人员,这就是必须掌握的内容。

 (鉴于这位老鸟的建议,作为初学者的我就大致看了看然后直接转载过来了,没自己写……)

 

总结

  这里面的很多例子,都是用的参考资料里面的例子,基本能看得懂,然后稍稍改了改。

  主要是想做个笔记,以后看的时候不用各种翻资料,有这一篇笔记就OK啦。

 

参考资料

  《Java基础入门》

  https://www.sxt.cn/Java_jQuery_in_action/eleven-basicconcept.html

  https://www.w3cschool.cn/java/java-multithreading.html

(待续)

posted @ 2019-06-19 15:56  张小凡I4CU  阅读(603)  评论(0编辑  收藏  举报