多线程

2018-12-20  本文已影响0人  上杉丶零

线程与进程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一般来说进程之间不允许相互通信,一个进程包含1~n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程都有独立的运行栈和程序计数器(PC),线程间的切换开销较小,线程之间允许相互通信。(线程是CPU调度的最小单位)
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个程序。
多线程是指在同一程序中有多个顺序流在执行,但多线程没有提高效率,而是提高了资源的利用率。
一个程序至少有一个进程,一个进程至少有一个线程。
并行:多个CPU或者多台机器同时处理一段逻辑,是真正的同时。
并发:通过CPU调度算法,让用户看上去是在同时执行,实际上从CPU操作层面来看并不是真正的同时,而是通过CPU的调度,在不同的任务之间进行高速切换。

继承java.lang.Thread类

继承Thread类是比较常用的一种方法,如果说只是想创建一条新的线程,没有什么其它要求,那么可以选择使用继承Thread类,并重写run()方法。下面来看一个简单的实例:

package leif;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread1 = new MyThread("A");
        MyThread myThread2 = new MyThread("B");
        myThread1.start();
        myThread2.start();
    }
}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "线程运行:" + i);

            try {
                sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
image.png

程序启动时,JVM会启动一个进程,主线程在main()方法调用时被创建。随着调用MyThread的两个对象的start()方法,另外两个线程将启动,这样整个程序就在多线程下运行。
调用start()方法后并不会立即变成运行状态(Running),而是使该线程变为就绪状态(Runnable),什么时候运行是由操作系统调度决定的。
从程序运行的结果可以发现,多个线程是乱序执行的。因此,只有乱序执行的程序才有必要设计成多线程。
调用Thread.sleep()方法的目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间让其他线程有运行的机会。
实际上所有的线程执行顺序都是不确定的,所以每次执行的结果都是随机的。
如果start()方法重复调用的话,就会出现java.lang.IllegalThreadStateException异常。

实现java.lang.Runnable接口

实现Runnable接口也是创建线程非常常见的一种方式,同样只需要重写run()方法即可。下面也来看个实例:

package leif;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new MyThread(), "C");
        Thread thread2 = new Thread(new MyThread(), "D");
        thread1.start();
        thread2.start();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + "线程运行:" + i);

            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
image.png

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定,所有的多线程代码都在run()方法里。Thread类其实也是实现了Runnable接口的类。
在启动多线程的时候,需要先通过Thread类的构造方法构造出线程对象,然后调用线程对象的start()方法来启动该线程。
实际上所有的线程都是通过调用Thread类的start()方法来运行的,因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是要通过Thread对象的API来控制线程,所以熟悉Thread类的API是进行多线程编程的基础。

Thread和Runnable的区别

如果一个类继承Thread,则不适合实现资源共享,但是如果实现了Runable接口的话,则可以很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:

  1. 增强程序的健壮性,资源可以被多个线程共享
  2. 可以避免单继承限制
  3. 线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread类的线程

main()方法其实也是一个线程,每次程序运行至少启动两个线程,一个是main线程,一个是垃圾回收线程。在Java中哪个线程先执行,完全取决于谁先抢到CPU的资源。

线程的生命周期

image.png
  • 初试状态(New):创建了一个新的线程
  • 就绪状态(Runnable):线程创建后,调用该线程的start()方法,该线程就会由初始状态变为就绪状态,该状态的线程位于可运行线程池中,等待获取CPU的使用权。
  • 运行状态(Running):就绪状态的线程获取了CPU的使用权,变为运行状态,并开始执行程序代码
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃了CPU的使用权,并停止运行,且直到线程再次进入就绪状态,才有机会变为运行状态。阻塞的情况分三种:
    1. 等待阻塞:运行状态下的线程执行wait()方法,JVM会把该线程放入等待池中(wait()方法会释放线程持有的锁)
    2. 同步阻塞:运行状态下的线程在获取对象的同步锁时,若该同步锁已被别的线程占用,则JVM会把该线程放入锁池中
    3. 其他阻塞:运行状态下的线程执行sleep()或join()方法,或者发出了输入请求时,JVM会把该线程置为阻塞状态(sleep()方法不会释放线程持有的锁)
  • 死亡状态(Dead):若线程的run()方法执行完毕或者发生了异常退出,则该线程结束生命周期

线程的调度

  • 线程优先级
    Java线程有优先级,优先级高的线程会获得更多的运行机会,但不保证一定能抢到CPU资源。
    Java线程的优先级用整数表示,取值范围是1~10。Thread类有以下三个静态常量:static int MAX_PRIORITY; 线程可以具有的最高优先级,取值为10static int MIN_PRIORITY; 线程可以具有的最低优先级,取值为1
    static int NORM_PRIORITY; 分配给线程的默认优先级,取值为5
    可以通过Thread类的setPriority()/getPriority()方法设置/获取线程的优先级。每个线程都有默认的优先级。主线程的默认优先级为5。
    线程的优先级有继承关系,比如在线程A中创建了线程B,那么线程B将和线程A具有相同的优先级。
    JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类中三个静态常量作为优先级,这样能保证同样的优先级在不同的操作系统中可以采用同样的调度方式。
  • 线程睡眠
    Thread.sleep()方法可以使线程转到阻塞状态,millis参数为线程睡眠的时间,以毫秒为单位。当睡眠结束后,线程就会转为就绪状态。sleep()方法平台移植性好。
  • 线程等待
    Object对象的wait()方法会导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法。这个两个唤醒方法也是Object对象的方法,等价于调用wait(0)。
  • 线程让步
    Thread.yield()方法可以将当前正处于运行状态的线程置为就绪状态,把资源让给相同或者更高优先级的线程,但不保证其它线程一定能抢夺资源成功,也就是说,被yield()方法置为就绪状态的线程有可能因为再次抢到资源而变为运行状态。
  • 线程加入
    Thread对象的join()方法可以选择线程的执行顺序。如果在线程A中调用线程B的join()方法,则线程A转入阻塞状态,直到线程B执行结束,线程A才能由阻塞状态转为就绪状态。
  • 线程唤醒
    Object对象的notify()方法可以唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会任意选择唤醒其中某一个线程。
    被唤醒的线程在抢夺、锁定此对象方面没有任何特权或劣势,将以常规方式与在该对象上主动同步的其它所有线程进行公平竞争。
    Object对象还有一个notifyAll()方法,可以用来唤醒在此对象监视器上等待的所有线程。
  • 注意:Thread对象的suspend()方法和resume()方法因为有死锁倾向,所以并不推荐使用。

常用函数说明

  1. Thread.sleep()

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

  1. Thread对象的join()

当前线程等待加入的线程终止后继续运行。
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

  • 不加join()
package leif;

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "开始执行");
        new Thread(new MyThread()).start();
        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始执行");

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

        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }
}
image.png
  • 加join()
package leif;

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "开始执行");
        Thread thread = new Thread(new MyThread());
        thread.start();

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始执行");

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

        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }
}
image.png
  1. Thread.yield()

让当前运行线程回到就绪状态,以允许其他线程获得运行机会。因此,使用yield()方法的目的是让各线程之间能够适当的轮转执行。但是,实际中无法保证yield()方法能够达到让步的目的,因为让步的线程还有可能被再次选中。

  • sleep()方法和yield()方法的比较

sleep()方法使当前线程进入阻塞状态,所以执行sleep()方法的线程在指定的时间内肯定不会被执行;yield()方法只是让当前线程重新回到就绪状态,所以执行yield()方法的线程有可能在进入到就绪状态后马上又被执行。

  1. Thread对象的setPriority()/getPriority()

设置/获取线程的优先级,取值范围是1~10。Thread类有以下三个静态常量:static int MAX_PRIORITY; 线程可以具有的最高优先级,取值为10static int MIN_PRIORITY; 线程可以具有的最低优先级,取值为1
static int NORM_PRIORITY; 分配给线程的默认优先级,取值为5

  1. Object对象的wait()/notify()/notifyAll()

必须与synchronized(Object)一起使用,可以使当前线程在Object的对象监视器上等待/唤醒。

  • 实例

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

package leif;

public class Test {
    public static void main(String[] args) {
        Object lockAB = new Object();
        Object lockBC = new Object();
        Object lockCA = new Object();
        new Thread(new MyThread(lockCA, lockAB), "A").start();
        new Thread(new MyThread(lockAB, lockBC), "B").start();
        new Thread(new MyThread(lockBC, lockCA), "C").start();
    }
}

class MyThread implements Runnable {
    private Object lock1;
    private Object lock2;

    public MyThread(Object lock1, Object lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();

        if ("B".equals(name)) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else if ("C".equals(name)) {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (int i = 1; i <= 10; i++) {
            synchronized (lock1) {
                synchronized (lock2) {
                    System.out.print(name);
                    lock2.notifyAll();
                }

                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
image.png
  • sleep()方法和wait()方法的比较
  • 共同点:
  1. 都是在多线程的环境下,都可以使线程阻塞指定的毫秒数
  2. 都可以通过interrupt()方法打断线程的阻塞状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
    如果线程A希望立即结束线程B,则可以调用线程B的interrupt()方法。如果此时线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch(InterruptedException e) {}中直接return即可安全地结束线程。
    需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()方法时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程处于阻塞状态后,就会立刻抛出InterruptedException。
  • 不同点:
sleep() wait()
Thread类的方法 Object对象的方法
保持同步锁 释放同步锁
结束后线程进入就绪状态 结束后线程进入锁池等待
  1. Thread对象的isAlive()

判断一个线程是否存活。

  1. Thread.currentThread()

获取当前线程。

  1. Thread对象的isDaemon()

判断一个线程是否是守护线程。

  1. Thread对象的setDaemon()

设置一个线程为守护线程。

  1. Thread对象的setName()

设置线程的名字。

常见线程名词解释

  • 主线程:JVM调用程序的main()方法所产生的线程
  • 当前线程:通过Thread.currentThread()方法获取的线程
  • 后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个守护线程。用户线程和守护线程的区别在于是否依赖于主线程的结束而结束,当所有的用户线程都结束后守护线程就会结束,无论此时守护线程是否执行完毕
  • 前台线程:是指接受后台线程服务的线程,也成为用户线程。由用户线程创建的线程默认也是用户线程

线程同步

线程同步,指某一个时刻,只允许一个线程来访问共享资源,线程同步其实是对对象加锁,如果对象中的方法都是同步方法,那么某一时刻只能执行一个方法。

  • 异步编程模型:线程A执行线程A的,线程B执行线程B的,两个线程之间谁也不等谁
  • 同步编程模型:线程A和线程B先后执行,线程B必须等线程A执行结束之后才能执行

总的来说:

  1. 线程同步的目的是为了保护多个线程同时访问一个资源时对资源的破坏。
  2. 线程同步是通过锁来实现的,每个对象都有且仅有一个锁(对象监视器),某个线程一旦获取了该对象锁,其他访问该对象的线程就无法再访问该对象锁所锁定的同步方法,只能等待该对象锁被释放后再访问。
  3. 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或方法当作锁。
  4. 实现同步需要很大的系统开销,甚至可能造成死锁,所以应尽量避免无谓的同步控制。
上一篇下一篇

猜你喜欢

热点阅读