你见过不长毛的羊吗

Java多线程-基础篇

2019.11.07

基本概念

进程

  • 正在执行的程序,拥有独立的代码和数据空间
  • 进程间的切换会有较大的开销
  • 是资源分配的最小单位
  • 一个进程可以包含一个或多个线程
  • 至少包含一个线程

线程

  • 程序中单独顺序的控制流
  • 线程本身依靠进程进行运行,只能用分配给进程的资源和环境
  • 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC)
  • 是cpu调度的最小单位

单线程

  • 进程中只存在一个线程,实际上主方法就是一个主线程

多线程

  • 一个进程中运行多个线程
  • 目的:更好的使用CPU资源

并行

  • 真正的同时,多个cpu实例或者多台机器同时执行一段处理逻辑。

并发

  • 通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。使用TPS或者QPS来反应这个系统的并发处理能力

线程安全

  • 并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果

同步

  • 保证共享资源的多线程访问成为线程安全

死锁

  • 两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁
  • 结果就是这些线程都陷入了无限的等待中

常见线程名词

主线程
JVM调用程序main()所产生的线程。

当前线程
无特别说明,一般指通过Thread.currentThread()来获取的进程。

后台线程

  1. 为其他线程提供服务的线程,也称为守护线程。
  2. 例如,JVM的垃圾回收线程就是一个后台线程。
  3. 区别在于,守护线程等待主线程,依赖于主线程结束而结束。

前台线程

  1. 接受后台线程服务的线程。
  2. 前台后台线程关系就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。
  3. 由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

Java中线程基础知识

  1. main()方法也是一个线程,Thread.currentThread().getName() 得到的值为main,所有俗称main线程。
  2. Java中,每次程序至少启动2个线程。一个main线程,一个是垃圾收集线程。
  3. 每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,而每一个JVM就是在操作系统中启动了一个进程。(待验证
  4. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。

线程的实现

继承java.lang.Thread类

public class MyThread01 extends Thread{
    // 自定义线程名称
    private String name;
    public MyThread01(String name){
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 1000; i++) {
            System.out.println(name+":"+i);
        }
    }

    public static void main(String[] args) {
        MyThread01 t1 = new MyThread01("A");
        MyThread01 t2 = new MyThread01("B");
        // 此时程序依然是顺序执行
        //t1.run();
        //t2.run();

        // System.out.println("----------------华丽分割线----------------");

        // 通过start()方法启动线程,此时t1、t2线程交替执行
        t1.start();
        t2.start();
    }
}

说明

  1. 程序启动运行main()时候,Java虚拟机启动一个进程,主线程在main()方法调用时候被创建。
  2. 随着调用t1、t2的start()方法,另外两个线程也启动了,这样,整个应用就在多线程下环境下运行了。
  3. 调用start()方法并不会立刻执行线程的代码,而是使该线程变为可运行状态(后面线程生命周期章节会有讲解),什么时候执行是由操作系统决定的。
  4. 线程的启动是通过start()方法,且不能重复调用。直接通过对象调用run()方法程序依然是顺序执行。
  5. 多运行几次代码,你会发现,多线程的执行顺序是不固定的,每次执行哪个线程是随机的。
  6. 查看Thread类的源码,可以发现Thread类是Runable接口的一个实现类。

实现Runable接口

public class MyRunable02 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

    public static void main(String[] args) {
        MyRunable02 r1 = new MyRunable02();

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);

        // 通过start()方法启动线程,此时t1、t2线程交替执行
        t1.start();
        t2.start();
    }
}

说明

  1. 通过实现Runable接口,使普通的Java类具有了多线程的特性。
  2. run()方法是多线程程序的一个约定,所有多线程的代码都在run()方法里面。
  3. 仔细观察代码可以发现,最终线程的启动还是通过Thread类的start()方法。
  4. 实际上Java中所有多线程代码都是通过Thread.start()方法来启动的。因此,熟悉Thread类的API是Java并发编程的基础。

推荐使用Runable的方式

  1. 避免Java单继承的限制。
  2. 降低数据与代码的耦合度,代码与数据独立,代码逻辑可被多个线程共享。
  3. 线程池不能直接放入Thread类对象,可以放入Runable、Callable对象。

实现Callable接口

  1. 这里先简单普及一下,在Java中Runnable的run()方法没有返回值,而Callable接口里的call()方法可返回值。
  2. Java常用Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类。
  3. FutureTask类同时实现了Future、Runnable接口。

并发执行同一个FutureTask

public class MyCallable03 implements Callable<String> {
    private int num = 5;
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum+=i;
            System.out.println(Thread.currentThread().getName()+": "+sum);
        }
		
		// 虽然是并发执行,但是最终只会执行其中一个
        if("Thread-0".equals(Thread.currentThread().getName())){
            this.num = 0; // 此处this就是c1
            System.out.println("now Thread: Thread-0");
        }else if("Thread-1".equals(Thread.currentThread().getName())){
            this.num=10; // 此处this就是c1
            System.out.println("now Thread: Thread-1");
        }
        return Thread.currentThread().getName()+" result:"+sum;
    }

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

        MyCallable03 c1 = new MyCallable03();
        FutureTask<String> f1 = new FutureTask<String>(c1);

		// t1、t2并发执行
        Thread t1 = new Thread(f1);
        Thread t2 = new Thread(f1);

        t1.start();
        t2.start();

        System.out.println(f1.isDone()); // f1执行完毕才是true

        Thread.sleep(2000);  //  main线程sleep,保证t1、t2执行完毕
        if(f1.isDone()){
            System.out.println("结果: "+f1.get());  // 结果: Thread-1 result:45
            System.out.println(t1.getState().toString());  // TERMINATED
            System.out.println(t2.getState().toString());  // TERMINATED
        }

        System.out.println("num:"+c1.num);  // 执行的t1为num:0,执行的t2为num:10
    }
}

说明

  1. 由t1、t2的状态得出,两个线程都执行了。
  2. 根据num的值与输出的“now Thread: Thread-?”结果得出,一个FutureTask只会被执行一次。

先后执行同一个FutureTask

public class MyCallable0302 implements Callable<String> {

    private static int num = 5;
    
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum+=i;
            System.out.println(Thread.currentThread().getName()+": "+sum);
        }
        
        // 最终只会执行其中一个
        if(Thread.currentThread().getName().equals("Thread-0")){
            this.num = 0; // 此处this就是c1
            System.out.println("now Thread: Thread-0");
        }else if("Thread-1".equals(Thread.currentThread().getName())){
            this.num=10; // 此处this就是c1
            System.out.println("now Thread: Thread-1");
        }
        
        return Thread.currentThread().getName()+" result:"+sum;
    }

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

        MyCallable0302 c1 = new MyCallable0302();
        FutureTask<String> f1 = new FutureTask<String>(c1);

        Thread t1 = new Thread(f1);

        t1.start();

        // 此时会继续执行主线程的代码。
        System.out.println(f1.isDone()); // f1执行完毕才是true

        Thread.sleep(2000); //  main线程sleep,保证t1执行完毕
        if(f1.isDone()){
            System.out.println("第一次: "+f1.get());  // 第一次: Thread-0 result:45
            System.out.println("num:"+c1.num); // num:0
            System.out.println(t1.getState().toString());  // TERMINATED

            Thread t2 = new Thread(f1);// 此时,f1中的call方法都不会被执行,相当于没传f1
            t2.start();
            Thread.sleep(2000); // main线程sleep,保证t2执行完毕
            System.out.println("第二次: "+f1.get());  // 第二次: Thread-0 result:45(依然是t1执行f1的结果)
            System.out.println(t2.getState().toString()); // TERMINATED
        }

        System.out.println("num:"+c1.num); // num:0 (依然是t1执行f1的结果)

        // 第二个线程执行第一个线程已经执行完的f1,第二个线程会执行,但是不是执行f1中的call方法
    }
}

说明

  1. 由两个“依然是t1执行f1的结果”进一步验证了:一个FutureTask只会被执行一次。

线程中常用方法

方法名作用详解
getName()获取线程名称Thread.currentThread().getName()
currentThread()获取当前线程对象currentThread()方法是Thread类的静态方法,如果调用线程对象.currentThread()方法并不能获取到调用的线程对象,反正在哪一个线程里面执行了currentThread()方法得到的就是哪个线程对象
isAlive()线程是否存活如果线程已经启动,并且没有died返回true
join()等待该线程终止在一个线程中调用other.join(),将等待other执行完后才继续本线程。(见补充说明)
sleep()线程的休眠见补充说明
yield()线程礼让当前线程可转让cpu控制权,让别的就绪状态线程运行(见补充说明)
interrupte()友好的终止线程执行保证程序逻辑完整性(见补充说明)
wait()线程挂起,进入等待队列JAVA多线程-Object.wait(),Object.notify(),Object.notifyAll()
notify()唤醒等待队列中任意一个线程JAVA多线程-Object.wait(),Object.notify(),Object.notifyAll()
notifyAll()唤醒等待队列中所有线程JAVA多线程-Object.wait(),Object.notify(),Object.notifyAll()
suspend()线程挂起不会释放对象锁,不推荐使用,常与resume()配套使用
resume()唤醒挂起线程不推荐使用,常与suspend()配套使用。如果 resume() 操作出现在 suspend() 之前执行,很容易造成死锁
activeCount()进程中活跃的线程数
enumerate()枚举程序中的线程
isDaemon()一个线程是否为守护线程
setDaemon()设置一个线程为守护线程用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
setPriority()设置一个线程的优先级取值1-10

补充说明

join()

  1. 这里是指的主线程等待子线程的终止。如果还要其他线程的话,调用join()方法的线程会与除主线程外的其他线程并发执行。所以,当主线程需要用到子线程的处理结果,这个时候就要用到join()方法。

yield()

  1. 让当前运行线程从运行状态(Running)回到可运行状态(Runable),以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

sleep()

  1. 线程休眠,不会释放锁
  2. sleep()方法是Thread类的静态方法,如果调用线程对象.sleep()方法并不是该线程就休眠,反正在哪一个线程里面执行了sleep()方法哪一个线程就休眠。
  3. 在sleep()休眠时间期满后,该线程不一定会立刻获得cpu资源,除非此线程具有更高的优先级。

sleep()、yield()的区别

  1. sleep()使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行。这段时间是通过程序设定的。
  2. yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程可能进入到可执行状态后又马上被执行,这段时间是不可设定的。
  3. 实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
  4. sleep() 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。所以,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行

interrupte()

  1. 不要以为它是中断某个线程!它只是向线程发送一个中断信号。正常运行的程序不去检测状态就不会终止。
    只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

stop()

  1. 是一种"恶意" 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。

综合interrupte()、stop(),建议使用自定义的标志位决定线程的执行情况

class SafeStopThread extends Thread {
    //此变量必须加上volatile
    private volatile boolean stop = true;
    @Override
    public void run() {
        //判断线程体是否运行
        while (stop) {
            // Do Something
            System.out.println("Stop");
        }
    }
    //线程终止
    public void terminate() {
        stop = false;
    }
}

线程优先级

  • Java线程的优先级用整数表示,取值范围是1~10,数值越大优先级越高。优先级高的线程获得更多运行机会的机会越大。
  • Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
  • 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
  • JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
    	Thread.MIN_PRIORITY => 1
    	Thread.MAX_PRIORITY=> 10
    	Thread.NORM_PRIORITY=> 5(默认)
    

说明

  1. 线程的优先级有可能影响线程的执行顺序,不是绝对的。

线程同步

有共享数据时就需要同步!!!

同步代码块

在代码块上加上"synchronized" 关键字,则此代码块就成为同步代码块

synchronized(同步对象){
          需要同步的代码块;
  }

同步方法

在方法返回修饰符之前加上"synchronized" 关键字,则此方法就成为同步方法

synchronized void 方法名(){
	....
}

线程生命周期

  • 新建(New):新建一个线程对象

  • 可运行(Runable)
    其他线程调用该线程的start()方法。不能对同一线程对象两次调用start()方法。
    该线程位于可运行线程池中,等待获取cpu使用权。

  • 运行(running):获取了cpu使用权,执行程序代码

  • 阻塞(block):因某种原因[]放弃了cpu使用权,暂时停止运行。直到线程再次进入可运行状态才有可能获取cpu使用权,转为运行状态。

    • 等待阻塞:执行了wait()方法。jvm把线程放入等待队列,释放锁。被notify(), notifyAll()进入锁池中
    • 同步阻塞:获取同步锁时,该锁被别的线程占用。jvm把线程放入锁池中
    • 其他阻塞:执行sleep(毫秒)、join方法、或者发出I/O请求。不释放锁
  • 死亡(dead):线程执行完成或因异常退出,该线程结束生命周期。死亡的线程不可再次恢复

参考
https://blog.csdn.net/Evankaka/article/details/44153709