Loading

java 并发相关(1) - 线程基本介绍

注 :示例图片来源于网络

1. 线程的基本概念

  • 进程

一个程序启动,Linux系统会给程序分配一个进程,并且分配给进程一些内存资源,启动一个jar包,就会创建这个jar包的进程;线程可以看做进程的一个顺序执行的指令流,一个进程可以创建多个线程,同一个进程创建的线程共享进程的内存资源,不同进程创建的线程是相互隔离的。

进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

  • 线程

线程可以看做一段顺序的指令序列,每个线程都有自己独立的堆栈和局部变量,多个线程之间共享父进程的地址空间和资源;创建线程的资源开销少,线程间的切换损耗少,所以java中采用多线程形式实现并发。

线程是CPU调度的基本单位,依附于进程。

  • java 并发编程

java实现的并发编程,即多个线程可以同时执行工作,提高cpu的使用效率,减少任务执行的总耗时;但多线程编程,也会带来缓存不一致,操作顺序难以控制的线程安全问题。

根据运行环境CPU的不同,可以分为单核CPU并发(时间片轮转)、多核CPU并行两种。

  • 多线程的应用场景

web服务器多个线程处理请求、后台任务、异步处理等场景。

2. 如何创建一个线程

java中主要通过下面三种方式创建一个线程:

注:无论通过哪一种方式创建一个线程,java中都是使用Runnable接口的run()方法做线程的执行体,最后都需要new一个Thread对象来创建线程。

  • 继承Thread类

java创建一个线程是通过新建一个Thread对象实现的,继承Thread类创建线程,会存在java单继承的局限性。

public class MyThread extends Thread {

    public MyThread(){}

    public MyThread(ThreadGroup group, String name){
        super(group,name);
    }

    @Override
    public void run() {
        System.out.println("This is Mythread, name is "+ getName() + ",ThreadGroup is " + getThreadGroup());
    }

}
  • 实现Runnable接口

实现一个Runnable接口创建线程,重写run方法,缺点是没有返回值。

public class MyRunnable implements Runnable{

    @Override
    public void run(){
        System.out.println("This is MyRunnable name is "+Thread.currentThread().getName());
    }
}

  • 实现Callable接口

实现一个call接口创建线程,需要和Future接口及相关的实现工具类FutureTask来实现一个有返回值的线程。

public class MyCallable implements Callable<String> {

    @Override
    public String call() {
        System.out.println("This is MyCallable name is "+Thread.currentThread().getName());

        return "MyCallable";
    }
}

        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        new Thread(futureTask, "futureTask").start();

        try {
            System.out.println("拿到callable返回值:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
  • 三者区别

1、Runnable和Callable采用接口形式执行线程任务,比直接继承Thread执行线程任务更加灵活。
2、Runnable是一个没有返回值的线程任务,Callable是有返回值的线程任务。
3、Callabe接口一般和Future接口的实现类来组合实现一个有返回值的异步编程,Future接口的实现类有:FutureTask、CompletableFuture。

3. 线程的一些关键属性

  • name:线程名称,可以重复,若没有指定会自动生成。

  • id:线程ID,一个全局唯一的正long值,创建线程时指定,终生不变,线程终结时ID可以复用。

  • priority:线程优先级,取值为1到10,线程优先级越高,执行的可能越大(跟CPU的线程调度模式有关),优先级还跟运行环境有关,若运行环境不支持优先级分10级,如只支持5级,那么设置5和设置6有可能是一样的。

    线程调度模式:java中采用抢占式调度

    1、分时调度模式:所有线程轮流使用CPU,平均分配每个线程占用的CPU时间片。

    2、抢占式调度模式:优先让优先级高的线程使用CPU,优先级高的线程获取CPU的时间片会多一些。

  • state:线程状态,Thread.State枚举类型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 6种,后续第4点详细介绍。

  • daemon:布尔值,是否为守护线程,java中线程分为两类:守护线程(true)和用户线程(false)。

    1、用户线程:一般我们创建的线程都是用户线程,当JVM中的用户线程全部执行完毕,JVM也会关闭。

    2、守护线程:只要JVM尚有一个非守护线程没有结束,守护线程就进行工作,守护线程随着JVM关闭而结束,典型例子:GC。

  • ThreadGroup:所属线程组,一个线程必然有所属线程组,线程组被设定为管理线程的一些属性,提供了一些管理线程组旗下线程的api方法和统一异常设置等。

线程组以树形结构存在,每个线程组都有一组子线程和子线程组。

  • UncaughtExceptionHandler:未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。

4 线程的状态转换

  • 线程定义的状态有六种:
Thread.State Thread中定义的几种状态
NEW 尚未启动的线程状态,即线程创建,还未调用start方法
RUNNABLE 就绪状态ready(调用start,等待调度)+正在运行running
BLOCKED 等待监视器锁时,陷入阻塞状态
WAITING 等待状态的线程正在等待另一线程执行特定的操作(如notify)
TIMED_WAITING 具有指定等待时间的等待状态
TERMINATED 线程完成执行,终止状态
  • 状态转化图:

  • 锁池和等待池

其实线程竞争锁都是在线程处于运行态尝试获取CPU资源执行线程任务的时候发生锁的竞争。

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

  • wait()方法和sleep()方法

wait()和sleep()区别

1.wait()来自Object类,sleep()来自Thread类

2.调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;

3.sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;

4.sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用

5.notify()随机释放一个线程从等待池到锁池,notifyAll()释放所有的线程从等待池到锁池。

6.wait()方法只能在持有该对象monitor对象的时候使用。

5. Thread类的一些相关方法的源码

5.1 Thread(...args[])构造函数(实际调用init()方法,构造方法不涉及到初始化逻辑,需要的话进行统一封装)

init()方法(源码省略):可以看出子线程继承父线程的优先级、是否守护线程、父线程的线程组、可继承ThreadLocal等属性。

posted @ 2021-05-19 09:23  逝zxq  阅读(128)  评论(0编辑  收藏  举报