多线程基础知识总结

目录

  • 进程与线程
  • 串行,并发,并行
  • 线程的创建与启动方式
  • 线程的优先级,守护线程
    • 优先级
    • 守护线程
  • 线程的生命周期
  • 线程Join
  • CountDownLatch和CyclicBarrier
  • 线程中断
  • 如何关闭一个线程
  • volatile

进程与线程

首先需要明确三个概念:程序,进程,线程
程序:分为可执行程序,不可执行程序。例如我们写的代码中main函数里的程序为可执行程序。
程序的官方解释:是指令、数据及其组织形式的描述。
进程:运行起来的程序,叫做进程。
进程的官方解释:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是程序的实体。

如下图所示,腾讯QQ在运行,是一个进程。
多线程基础知识总结_第1张图片
线程:让一个进程能够同时处理多件事情。是操作系统能够进行运算调度的最小单位,它被包含在进程当中,是进程中的实际运作单元。

进程与线程之间的联系:

  • 线程是操作系统可识别的最小执行和调度单位。
  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  • 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。处理机分给线程,即真正在处理机上运行的是线程。

进程与线程之间的区别:

  • 地址空间和其他资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。线程间通信:同一个进程中的线程
  • 通信:进程间通信IPC(管道,信号量,共享内存,消息队列,网络),线程间可以直接读写进程数据段(如全局变量)来进程通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  • 调度和切换:线程上下文切换比进程上下文切换快得多。
  • 进程具有独立的空间地址,一个进程崩溃后,在保护模式下不会对其它进程产生影响。保护模式下,程序地址为虚拟地址,然后由OS系统管理内存访问权限,这样每个进程只能访问分配给自己的物理线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。

下图解释了程序,进程,线程之间的关系,它们之间呈现树形结构。
多线程基础知识总结_第2张图片

串行,并发,并行

串行:按照指令顺序依次执行。
并行:真正的同时执行。
并发:CPU的快速切换。

并发和并行都是充分利用cpu的手段。

大部分操作系统都支持多线程并发操作,人们一遍聊天,还能一遍打开网易云听歌,还能同时上网查资料等,这些看上去是在同时进行,事实是cpu在某一时刻只能执行一个程序,即一个进程,它是通过快速切换来完成并发执行。

线程的创建与启动方式

创建线程的方式:new Thread对象。
定义线程执行单元的方式有三种:

  • 继承Thread类下的run方法。
  • 重写runnable接口下的run方法。
  • 实现callable类下的call方法

三种方式的区别:
继承类与实现接口的区别:

  • 线程类继承自Thread则不能再继承其他类,而Runnable/Callable接口可以。
  • 线程类继承自Thread,对于Runnable/callable来说,使用线程类内部的方法方便一些。
  • 实现Runnable/Callable接口的线程类的多个线程 可以更加方便的访问同一个变量,而Thread类则需要内部类进行替代。

Callable和Runnable接口的区别:

  • Callable规定的方法是call(),而Runnable规定的方法是run()。
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  • call()方法可抛出异常,而run()方法是不能抛出异常的。
  • 运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。 提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。

以下代码是Callable的使用:
FutureTask中的get()方法:运行在主线程中,get方法调用之后如果子线程运行结束,正常返回返回值,如果子线程未结束那么将会阻塞在这里直到子线程执行完返回返回值

package com.tulun.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

class CallableThread implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("线程的名字是:" + Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(7);
        return "abc";
    }
}
public class CallableThreadTest {
    public static void main(String[] args) { //主线程
        CallableThread callable = new CallableThread();
        FutureTask<String> task = new FutureTask<>(callable);  //获取未来数据的方式
        Thread thread = new Thread(task);
        thread.setName("子线程");
        thread.start();
        try {
            System.out.println(task.get());//运行在主线程中
            //get方法调用之后如果子线程运行结束,正常返回返回值
            //如果子线程未结束那么将会阻塞在这里直到子线程执行完返回返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

线程的名字是:子线程
abc

start()方法和run()方法的区别:

  • 当启动线程时,使用start()方法,系统会把run()方法当成线程执行体来处理。
  • 当启动线程时,未使用start()方法,而是调用run()方法,run()方法会立刻执行,但是此时系统会把run()方法当成一个普通的方法,线程对象也被当成一个普通对象,无法实现多线程的作用。

线程的优先级,守护线程

优先级

每个线程对应有一个线程优先级的数值。
优先级的取值范围是1-10,一般默认为5。

优先级高的被cpu先执行的概率大,并不是优先级高的一定会被先执行。
在项目开发中不能使用优先级来完成一些特定任务。

优先级的特性:

  • 优先级的继承性:如果线程A中启动线程B,那么线程A和B的优先级是一样的。
  • 优先级的规则性:线程会按优先级大小顺序执行, 是不一定是优先级较大的先执行完。
  • 优先级的随机性:优先级高的被cpu先执行的概率大,并不是优先级高的一定会被先执行。 因为cpu只是尽量将执行资源交给优先级高的线程,优先级高的线程不一定每一次都先执行完。

优先级没有具体的使用场景。

以下代码说明,优先级高的线程并不一定先执行完。

package com.tulun.thread;

import java.util.Random;

class MyThread1 extends Thread {
    @Override
    public void run() {
        long start = System.currentTimeMillis();
        long sum = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 50000; j++) {
                Random random = new Random();
                random.nextInt();
                sum = sum + i;
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("-------------thread 1 use time = " + (end - start));
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        long start = System.currentTimeMillis();
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 50000; j++) {
                Random random = new Random();
                random.nextInt();
                sum = sum + i;
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("*****************thread 2 use time = " + (end - start));
    }
}

public class PriorityTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            MyThread1 myThread1 = new MyThread1();
            myThread1.setPriority(5);
            myThread1.start();
            MyThread2 myThread2 = new MyThread2();
            myThread2.setPriority(6);
            myThread2.start();
        }
    }
}

执行结果:

-------------thread 1 use time = 510
*****************thread 2 use time = 637
-------------thread 1 use time = 658
*****************thread 2 use time = 635
*****************thread 2 use time = 638
-------------thread 1 use time = 656
-------------thread 1 use time = 773
-------------thread 1 use time = 848
*****************thread 2 use time = 747
*****************thread 2 use time = 747

守护线程

守护线程是一个特殊的线程。
在后台执行的线程,当所有非守护线程结束之后,守护线程会自动结束。

守护线程必须在守护前设置,若在守护后设置就会报错。

应用场景:

  • 垃圾回收(用户创建出来的线程产生垃圾,用户线程如果结束,就没必要回收了)
  • 游戏收集金币,对手等级,需要一个线程不停的去同步信息,该线程为守护线程。
  • 连接池:杀死所有超时的空闲连接,连接池工作时会启动定时的程序,不停的扫描哪个连接是空闲的并且超时,该定时程序可设置为守护线程。

以下代码是守护线程测试代码:
将线程t设置为守护线程while死循环。当主线程和r线程结束了的时候,t线程会自动结束。这就是守护线程。

package com.tulun.thread;

import java.util.concurrent.TimeUnit;

/**
 * 守护线程test
 */
class DaemonThread implements Runnable{

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

public class DaemonTest {
    public static void main(String[] args) {
        DaemonThread runable = new DaemonThread();
        Thread r = new Thread(runable);
        //匿名内部类
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println(Thread.currentThread().getName());
                }
            }
        });
        t.setDaemon(true);
        t.start();
        r.start();
        try {
            TimeUnit.SECONDS.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

注:线程默认命名方式:Thread-编号

线程的生命周期

线程的生命周期:指一个线程从生到死的过程。

一个线程从生到死经历的过程:New,Runnable,blocked,waiting,time_waiting,terminated。

线程都会经历的状态:New,Runnable,terminated。

下面这幅图描述了线程的生命周期:
多线程基础知识总结_第3张图片
以下对线程生命周期的每个状态进行解释:
New状态:
还没有执行,只是new出来了。

当我们用关键字new创建一个thread对象时,此时它并不是出于执行状态,因为没有调用start方法启动该线程,那么线程的状态为new状态。

准确地说,它只Thread对象的状态,因为没有start之前,该线程根本就不存在,与你用关键字new创建一个普通的java对象没有什么区别new 状态通过start方法进入runnable状态。

Runnable状态:
Runnable状态:

分成了两种状态可执行(就绪态)和运行态

  • 运行态。
  • 就绪态,已经调用了start方法,但是线程还没有被cpu调度执行。只是具有了运行的资格。

线程对象进入就可执行(就绪态)态必须调用start方法,那么此时才是真正的在jvm进程中创建一个线程。

线程一经启动就可以立即得到执行?
线程的运行和进程一样都要听命于cpu的调度,那么我们把中间状态成为可执行状态(就绪态),它是具备执行的资格,但是并没有真正的执行起来而是在等待cpu的调度。

由于存在运行状态,所以不会直接进入blocked、waiting、timed waiting和terminated状态,即使是在现层的执行逻辑中调用wait、sleep或者其他block的io操作等,也必须获得cpu的调度执行权才可以,严格来讲,可执行(就绪态)的线程只能意外终止或者进入运行状态;

Blocked状态:
线程阻塞状态
进行某个阻塞的io操作,比如因网络数据的读写而进入blocked状态。
获取某个锁资源,从而加入到该锁的阻塞队列中进入blocked状态。

Waiting状态:
一个线程在等待执行一个(唤醒)动作时,该线程进入waiting状态。
进入这个状态不能自动唤醒,必须等待另一个线程发出特定的指令才能够被唤醒。

Timed_waiting状态:
和上面的类似,只是这个状态的线程不会一直等下去,会有一个超时时间。

Terminated状态:
terminated是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入terminated状态,意味着该线程的整个生命周期结束了。

三种结束情况:

  • 线程运行正常结束,结束生命周期;
  • 线程运行出错意外结束;
  • JVM crash导致所有的线程都结束。

线程Join

简言之,能让线程插队。
一个线程处于waiting态时,是不会运行的。

以下用代码来说明:
子线程ReceiveThread1 调用join方法,可以让子线程插到主线程之前运行,主线程此时处于waiting态,等待子线程结束唤醒。

主线程waiting态,那么后面的代码在这段时间将不会被执行,直到调用join方法的这个线程执行完之后才会执行后续代码。

package com.tulun.thread;

/**
 * 测试join方法
 */
class ReceiveThread1 extends Thread {//子线程的定义
    Thread thread;

    public ReceiveThread1(String str, Thread thread) {
        super(str);
        this.thread = thread;
    }

    @Override
    public void run() {  //线程的执行单元
        for (int i = 0; i < 20; i++) {
            System.out.println(this.getName());
            System.out.println("state: " + thread.getState());  
        }
    }
}
public class ThreadTestJoin {
    public static void main(String[] args) {  //主线程
        //处理发送数据
        ReceiveThread1 thread = new ReceiveThread1("接收数据",Thread.currentThread());
//        thread.setName("接收线程");
        thread.start(); //启动子线程
        try {
            /*
            子线程插到了主线程之前,主线程处于waiting态,等待子线程结束唤醒
             */
            thread.join();
            //thread.join(1);//超时时间为1ms,如果超时了,即1ms内子线程未结束,主线程会被唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当子线程结束,主线程被唤醒
        //主线程waiting  那么后面的代码在这段时间将不会被执行 直到调用join方法的这个线程执行完之后才会执行后续代码
        System.out.println("state: " + Thread.currentThread().getState());   //主线程被唤醒执行后的状态
        for (int i = 0; i < 5000; i++) {
            System.out.println("发送数据");
        }
    }
}

CountDownLatch和CyclicBarrier

CountDownLatch

解释:

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

源码:
构造方法

//参数count为计数值
public CountDownLatch(int count) {  };  

常用方法

//什么时候count减为0,后面的代码才开始执行。
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1,一个线程执行完进行减一操作
public void countDown() { };  

示例

package com.tulun.thread;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;

class AirPlaneS implements Runnable{
    private List<String> list;
    private CountDownLatch latch;
    public AirPlaneS(List<String> list,CountDownLatch latch) {
        this.list = list;
        this.latch = latch;
    }
    @Override
    public void run() {
        list.add(Thread.currentThread().getName()+"8:00");
        latch.countDown();
    }
}
public class AirLineCountDown {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(3);
        AirPlaneS airPlaneS = new AirPlaneS(list,latch);
        Thread t1 = new Thread(airPlaneS,"南方航空");
        Thread t2 = new Thread(airPlaneS,"东方航空");
        Thread t3 = new Thread(airPlaneS,"海南航空");
        t1.start();
        t2.start();
        t3.start();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
    }
}

注:如果count数大于实际线程数,调用await方法时就永远不会减为0,主线程会一直等待。
CyclicBarrier

解释
让所有线程都等待完成后才会继续下一步行动。

源码:
构造方法

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

常用方法

//所有线程相互等待直到把barrier中的值减为0
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

示例

package com.tulun.thread;
/**
 * CyclicBarrier
 */

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

class AirPlaneSearch implements Runnable {
    List<String> list;
    CyclicBarrier barrier;

    public AirPlaneSearch(List<String> list, CyclicBarrier barrier) {
        this.list = list;
        this.barrier = barrier;
    }
    
    @Override
    public void run() {   //查询各个航空公司的信息
        System.out.println("开始从" + Thread.currentThread().getName() + "查询航班信息");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (list) {
            list.add(Thread.currentThread().getName() + "8:00");
        }
        try {
            //所有的线程相互等待直到把barrier中的值减为0      A: 2   B  1   C
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

public class AirLineCircle {
    public static void main(String[] args) {
        final List<String> list = new LinkedList<>();
         //有几个子线程就设置位几
        // 什么时候Runnable 减为0 Runnable中的方法被执行
        CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                Iterator<String> iterator = list.iterator();
                System.out.println("--------------------------------------------");
                System.out.println("所查询的航班信息如下: ");
                while (iterator.hasNext()){
                    System.out.println(iterator.next());
                }
            }
        });
        AirPlaneSearch runnable = new AirPlaneSearch(list,barrier);
        Thread t1 = new Thread(runnable, "南方航空");
        Thread t2 = new Thread(runnable, "东方航空");
        Thread t3 = new Thread(runnable, "海南航空");
        t1.start();
        t2.start();
        t3.start();
    }
}

两者区别:

  • CountDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
  • CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,提供reset功能,可以多次使用。

线程中断

在Java中,停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出。

interrupt方法不一定会真正"中断"线程,它只是一种协作机制,如果不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程。

join(),会判断线程中断位,如果中断位为true,就会抛出异常,从而结束线程的等待状态。

public boolean isInterrupted();//测试此线程是否已被中断。此方法不影响线程的中断状态
public void interrupt();//中断线程,将线程中断位从false改为true。
public static boolean interrupted();//测试此线程是否已被中断,线程中断位会被擦除。

实际应用:心跳机制,用来维护客户端和服务器之间的连接。

能否抛出异常是判断能否中断的一个标志。

线程中断的方法:
Object的wait/wait(long)/wait(long,int)方法
Thread的sleep(int)/sleep(long,int)
Thread的join/join(long)/join(long,int)方法

示例:
使用join方法,使子线程先运行,设置计数器i,当i计数到20时,调用interrupt方法,中断主线程的等待,并且将线程中断位由false变为true。

主线程处于waiting状态,会判断线程中断位,当线程中断位为true时,就会抛出异常从而结束线程的等待状态。

package com.tulun.thread;

import java.util.concurrent.TimeUnit;

class ReceiveThread11 extends Thread {//子线程的定义
    Thread thread;
    private int i = 0;
    public ReceiveThread11(String str, Thread thread) {
        super(str);
        this.thread = thread;
    }

    @Override
    public void run() {  //线程的执行单元
        while (true){
            i++;
            System.out.println(this.getName());
            if(i == 20){
            //中断了主线程的等待状态  将线程中断位 由flase  --》 true
                thread.interrupt();  
            }
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadInterrupt {
    public static void main(String[] args) {  //主线程
        //处理发送数据
        ReceiveThread11 thread = new ReceiveThread11("接收数据",Thread.currentThread());
        thread.start(); //启动子线程
        try {
            thread.join();
            //主线程处于waiting状态  会判断线程中断位
            // 如果中断位位true --》 就会抛出异常从而结束线程的等待状态
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (true){
            System.out.println("发送数据");
            try {
            //主线程处于waiting状态  会判断线程中断位
            // 如果中断位位true --》 就会抛出异常从而结束线程的等待状态
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

如何关闭一个线程

如何让一个线程结束:
正常结束:一个线程的生命周期正常结束掉。
非正常结束:有以下几种方式。
法一:让子线程运行5s后调用interrupt方法改变其中断标识位,通过判断中断标志位为true来使子线程break,结束。
缺点:若有sleep(),join等方法,中断状态位会被擦除,使得判断无效
法二:利用sleep方法可抛出异常的特点,5s后调用interrupt方法,sleep抛出异常,此时break退出循环,结束线程。
缺点:每次打印的时候都会隔1nm,改变了本来的执行逻辑,对效率有很大的影响
法三:使用volatile关键字,设置flag,让子线程运行5s后,改变flag的值,子线程通过判断flag的值来break出循环,结束线程。

示例:
以下代码目的是让子线程运行5s后关闭。

package com.tulun.thread;

import java.util.concurrent.TimeUnit;
//经常用来结束线程的方式
class BThread implements Runnable{
    //存在线程安全问题
    private static volatile boolean flag = false;

    @Override
    public void run() {
        /**
         * 法一
         * 缺点:若有sleep(),join等方法,中断状态位会被擦除,使得判断无效
         */
//        while(true){
//            if(Thread.currentThread().isInterrupted()){
//                break;
//            }
//            System.out.println("接受数据");
//        }
        /**
         * 法二
         * 缺点:每次打印的时候都会隔1nm,改变了本来的执行逻辑,对效率有很大的影响
         */
//        while(true){
//            try {
//                TimeUnit.NANOSECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//                break;
//            }
//            System.out.println("接受数据");
//        }
        /**
         * 法三
         */
        while(true){
            if(flag){
                break;
            }
            System.out.println("接受数据");
        }
    }
    public static void closeThread(){
        flag = true;
    }
}
public class CloseThread {
    public static void main(String[] args) {
        BThread runable = new BThread();
        Thread t = new Thread(runable);
        t.start();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
        BThread.closeThread();//只是将主线程副本中flag变为true
        System.out.println(t.isInterrupted());
    }
}

volatile

volatile能够解决缓存一致性问题。

以上代码中的flag,主线程和子线程都要操作flag。
加上volatile之后,主线程改变flag的值之后立马会将这个值同步到主内存中。
子线程会一直判断主内存中的值是否被修改,如果被修改的话立刻进行同步。
多线程基础知识总结_第4张图片

你可能感兴趣的:(java,多线程)