多线程

一颗蔬菜 2019-05-05 AM 172℃ 0条

进程与线程

进程是系统进行资源调度(CPU、内存、I/O等)和分配的独立单位,每个进程都有私有的内存空间和系统资源。

一个CPU在一个时间点只能执行一个进程,多进程并不是同时运行而是切换着运行,由于进程之间切换的速度非常快,用户感觉不到进程的切换,误认为所有的应用程序都是同时执行的。

一个进程内部可以执行多个线程,线程是程序中的控制流,是程序使用CPU的基本单位。

并行和并发

并行是逻辑上同时发生,指在某一个时间内运行多个程序。

并发是物理上同时发生,指在某个时间点同时运行多个程序。

如何实现多线程?

由于线程是依赖进程存在的,因此首先要创建一个进程。

而进程是由操作系统创建,所以我们需要调用系统功能来创建进程。

Java本身不能调用系统功能,因为Java语言是基于Java虚拟机的。

但Java可以通过调用C/C++程序间接地创建进程。

多线程的实现

一、简单案例

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.print(i+"  ");
        }
    }

}
package Threads;
public class TestThread {
    public static void main(String[] args) {
        MyThread thread1= new MyThread();
        MyThread thread2 = new MyThread();
        /* 调用start()方法的本质是将当前线程置于就绪态,
         * 当run()方法被执行时的瞬间,线程从就绪态转化为运行态
         */
        thread1.start();
        thread2.start();
    }
}
运行结果:0  1  0  1  2  3  4  2  5  6  7  8  3  9  4  10  5  11  6  12  7  13  8  14  9  15  10  16  11  17  12  18  13  19  14  20  15  21  16  22  23  17  24  18  25  19  26  20  27  21  28  22  29  23  30  24  31  25  32  26  33  27  34  28  35  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  36  45  46  47  48  37  49  38  39  40  41  42  43  44  45  46  47  48  49  
Process finished with exit code 0

从运行结果来看,两个线程的执行是无规律地切换着的。

另外我们可以通过setName(String name)方法为线程设置名称属性,也可以通过调用Thread父类的有参构造方法来设置名称属性。另外,还可以为主线程设置名称属性。

public class MyThread extends Thread{

    public MyThread() {}
    // 声明有参构造方法前,需要申明无参构造方法
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+i);
        }
    }

}
public class TestThread {

    public static void main(String[] args) {
        MyThread thread1= new MyThread();
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
        thread1.setName("线程A:   ");
        MyThread thread2 = new MyThread("线程B:  ");
        thread1.start();
        thread2.start();
    }
}

运行结果

主线程
线程A:   0
线程B:  0
线程A:   1
线程B:  1
线程A:   2
线程B:  2
线程A:   3
线程B:  3
线程A:   4
......

二、线程的调度

假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,而线程只有得到使用权才能执行指令。在操作系统中,讲到了两种资源调度模型:

  1. 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果两个线程的优先级一样高,那么会随机选择一个,优先级高的线程并不是一定比优先级低的线程先执行,而是它们抢占到时间片的概率比较大。

而Java采用的是第二种方式,继续在之前的代码上进行迭代:

package Threads;
public class MyThread extends Thread{

    public MyThread() {}
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+i);
        }
    }
}
package Threads;
public class TestThread {
    public static void main(String[] args) {
        MyThread thread1= new MyThread();
        thread1.setName("线程A:   ");
        // 为线程A设置优先级
        thread1.setPriority(10);
        MyThread thread2 = new MyThread("线程B:  ");
        // 为线程B设置优先级
        thread2.setPriority(1);
        thread1.start();
        thread2.start();
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
    }
}
主线程
线程A:   0
线程A:   1
线程A:   2
线程A:   3
线程A:   4
线程A:   5
线程A:   6
线程A:   7
线程A:   8
线程A:   9
线程A:   10
线程A:   11
......
线程B:   38

源码解读

 private volatile String name;
    private int            priority; 
/**
  * The minimum priority that a thread can have.
  */
 public final static int MIN_PRIORITY = 1;

/**
  * The default priority that is assigned to a thread.
  */
 public final static int NORM_PRIORITY = 5;

 /**
  * The maximum priority that a thread can have.
  */
 public final static int MAX_PRIORITY = 10;

 /**
  * Returns a reference to the currently executing thread object.
  *
  * @return  the currently executing thread.
  */
 public static native Thread currentThread();
  public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

    /**
     * Returns this thread's priority.
     *
     * @return  this thread's priority.
     * @see     #setPriority
     */
    public final int getPriority() {
        return priority;
    }

这是源码中的部分代码,MIN_PRIORITYNORM_PRIORITYMAX_PRIORITY指的是线程的优先级,它们被finalstatic修饰,是静态常量。结合setPriority()方法,可以看出,优先级的范围是[1, 10],如果程序员设置的优先级不在此区间内,就会抛出IllegalArgumentException()异常。

三、线程控制

线程休眠

线程休眠非常简单,只需调用sleep(long millis)方法就可以实现休眠。

package Threads;
public class MyThread extends Thread{

    public MyThread() {}
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            try {
                // 休眠
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+i);
        }
    }

}

再来看它的底层源码:

public static native void sleep(long millis) throws InterruptedException;

通过分析源码可知,Thread类的sleep()方法是一个本地方法,它的具体细节使用C/C++来实现的,并且被编译成了DLL,Java只需要调用它即可。这些函数的实现体在DLL中,JDK的源代码中并不包含,我们是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

线程加入

只有当调用join()的线程终止后,其他线程才能开始执行。

继续在TestThread类上进行迭代:

package Threads;
public class TestThread {

    public static void main(String[] args) {

        MyThread thread1= new MyThread();
        thread1.setName("线程A:   ");
        thread1.setPriority(1);
        MyThread thread2 = new MyThread("线程B:  ");
        thread2.setPriority(10);

        thread1.start();
        try {
           // 调用join()方法
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
    }
}

运行结果

线程A:   0
线程A:   1
线程A:   2
......
线程A:   47
线程A:   48
线程A:   49
主线程
线程B:  0
线程B:  1
......
线程B:  48
线程B:  49

Process finished with exit code 0

尽管线程B的优先级为10、线程A的优先级为1,只有当线程执行完以后,线程B才能开始执行。

线程礼让

当前线程执行完一次以后,主动放弃CPU资源让给其他线程,使得多个线程的执行更加和谐,但不能保证每一人一次的交替执行。

package Threads;
import org.junit.Test;

public class TestThread {

    public static void main(String[] args) {

        MyThread thread1= new MyThread();
        thread1.setName("线程A:   ");
        thread1.setPriority(Thread.MIN_PRIORITY);
        /*
        * Thread.MIN_PRIORITY 是Thread类中的静态常量,初始值为1
        * public final static int MIN_PRIORITY = 1;
        * public final static int NORM_PRIORITY = 5;
        * public final static int MAX_PRIORITY = 10;
        */
        
        MyThread thread2 = new MyThread("线程B:  ");
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
    }
}
package Threads;

public class MyThread extends Thread{

    public MyThread() {}
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+i);
            // 调用yield();
            Thread.yield();
        }
    }

}

运行结果

......
线程B:  3
线程A:   9
线程B:  4
线程A:   10
线程B:  5
线程A:   11
线程B:  6
线程A:   12
线程B:  7
线程A:   13
线程B:  8
线程A:   14
线程B:  9
线程A:   15
线程B:  10
线程A:   16
线程B:  11
线程A:   17
线程B:  12
线程A:   18
线程B:  13
线程A:   19
线程B:  14
线程A:   20
线程B:  15
线程A:   21
线程B:  16
线程A:   22
线程B:  17
线程A:   23
线程B:  18
线程A:   24
线程B:  19
线程A:   25
线程B:  20
线程A:   26
线程B:  21
线程A:   27
线程B:  22
线程A:   28
线程B:  23
线程A:   29
线程B:  24
线程A:   30
线程B:  25
线程A:   31
线程B:  26
线程A:   32
线程B:  27
线程A:   33
线程B:  28
线程A:   34
线程B:  29
线程A:   35
线程B:  30
线程A:   36
线程B:  31
线程A:   37
线程B:  32
......

场面非常和谐。
sleep(long millis)yield()的区别:
当线程调用sleep(long millis)方法后,该线程会在参数指定的时间内处于阻塞状态,该时间结束后,重新回到就绪态。当线程调用yield()方法时,线程不是处于阻塞状态,而是处于就绪态。

守护线程

小明和小王都喜欢小苍老师,小苍老师辞职了,他们就不学习了。他们想要守护世界上最好的小苍老师。

package Threads;
public class MyThread extends Thread{
    public MyThread() {}
    public MyThread(String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+ "学习打卡+"+i);
        }
    }
}
package Threads;

public class TestThread {

    public static void main(String[] args) {
        MyThread thread1= new MyThread();
        thread1.setName("小明:   ");
        MyThread thread2 = new MyThread("小王:  ");
        thread1.setDaemon(true);
        thread2.setDaemon(true);
        thread1.start();
        thread2.start();
        Thread.currentThread().setName("小苍");
        for (int i = 0; i < 5;i++) {
            System.out.println(Thread.currentThread().getName() + " 教书育人,打卡+"+i);
        }

    }
}

运行结果

小苍 教书育人,打卡+0
小明:   学习打卡+0
小王:  学习打卡+0
小苍 教书育人,打卡+1
小王:  学习打卡+1
小明:   学习打卡+1
小王:  学习打卡+2
小苍 教书育人,打卡+2
小王:  学习打卡+3
小明:   学习打卡+2
小苍 教书育人,打卡+3
小苍 教书育人,打卡+4
小王:  学习打卡+4
小明:   学习打卡+3
Process finished with exit code 0

小苍 教书育人,打卡+0
小苍 教书育人,打卡+1
小苍 教书育人,打卡+2
小苍 教书育人,打卡+3
小苍 教书育人,打卡+4
Process finished with exit code 0

小苍 教书育人,打卡+0
小苍 教书育人,打卡+1
小苍 教书育人,打卡+2
小苍 教书育人,打卡+3
小苍 教书育人,打卡+4
Process finished with exit code 0

从三次运行结果来看,小明和小王都在守护最好的小苍老师,小苍老师的进程结束以后,他们也跟着结束了。

线程中断

在学习初期,先熟悉两种与线程中断相关的方法,stop()方法和interrupt()方法。

当一个线程调用stop()方法时,它直接由当前的状态(运行阻塞就绪)转变为死亡

而当一个线程调用interrupt()方法时,它完成的事情只是将中断标志置为True,并抛出java.lang.InterruptedException异常。

标签: none

非特殊说明,本博所有文章均为博主原创。