1_线程基础知识

img

1. 线程、进程和管程

进程:是程序的一次执行,是系统进行资源分配和调度的独立单位,每一个进程都有自己的内存空间和系统资源。

**线程:**在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程。⼀个进程会有1个或多个线程。

管程:Monitor(监视器),也就是平时所说的


什么是Monitor?

Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,

Object o = new Object();
new Thread(new Runnable() {
  @Override
  public void run() {
    synchronized (o) {
    }
  }
},"t2");

Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。

2. 用户线程和守护线程

Java线程分为用户线程和守护线程

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
                (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
    }
}, "t1");
t1.setDaemon(true);
t1.start();

Thread.currentThread().isDaemon(),线程的daemon属性为true表示是守护线程,false表示是用户线程。

t1.setDaemon(true);可以设置线程类型。


**守护线程:**是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。

**用户线程:**是系统的工作线程,它会完成这个程序需要完成的业务操作。

我们一般创建或使用的线程都是用户线程,除非用t1.setDaemon(true);设置。


重点:

  1. 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出

如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。

  1. 设置守护线程,需要在start()方法之前进行。

代码1:t1为用户线程:

public class DaemonDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
                        (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
                while (true) {

                }
            }
        }, "t1");
//        t1.setDaemon(true);
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"\t ---- end主线程");
    }
}
截屏2022-07-13 16.14.35

此时程序没有结束运行,因为t1为用户进程。

代码2:t1为守护线程:

只需将上述t1.setDaemon(true);注释打开即可。

截屏2022-07-13 16.16.47

此时主线程结束后,代码结束运行。

3. 线程的创建方式

首先明确一点,Java无法自己开启线程。

为什么?

让我们看一下Thread类的start方法:

public synchronized void start() {
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
			...
    }
}

这里主要调用了start0(),而start0()则是一个本地方法:

private native void start0();

所以创建线程的方式有以下四个方法:

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
  4. 线程池

3.1 继承Thread

//注意:打印出来的结果会交替执行,但交替顺序是随机的。
public class ThreadDemo{
    public static void main(String[] args) {
        //4.创建Thread类的子类对象
        MyThread myThread=new MyThread();
        //5.调用start()方法开启线程
        //[ 会自动调用run方法这是JVM做的事情,源码看不到 ]
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("我是主线程"+i);
        }
    }
}
class MyThread extends Thread{
    //2.重写run方法
    public void run(){
        //3.将要执行的代码写在run方法中
       for(int i=0;i<100;i++){
           System.out.println("我是线程"+i);
       }
    }
}

3.2 实现Runnable接口

public class RunnableDemo {
    public static void main(String[] args) {
        //4.创建Runnable的子类对象
        MyRunnale mr=new MyRunnale();
        //5.将子类对象当做参数传递给Thread的构造函数,并开启线程
        //Runnale taget=mr; //多态,传入target也可以。
        new Thread(mr).start();
        for (int i = 0; i < 100; i++) {
            System.out.println("我是主线程"+i);
        }
    }
}

//1.定义一个类实现Runnable
class MyRunnale implements Runnable{
    //2.重写run方法
    @Override
    public void run() {
        //3.将要执行的代码写在run方法中
        for (int i = 0; i < 100; i++) {
            System.out.println("我是线程"+i);
        }
    }
}

实现Runnable接口原理:

调用start方法的时候,会自动调用run方法这是JVM做的事情,源码看不到。

img


继承Thread和实现Runnable接口方式的区别

	(1).查看源码
		a.继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()方法
    b.实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法
  (2).继承Thread
    a.好处是:可以直接使用Thread类中的方法,代码简单
    b.弊端是:如果已经有了父类,就不能用这种方法
  (3).实现Runnable接口
    a.好处是:即使自己定义的线程类有了父类也没有关系,因为有了父类可以实现接口,而且接口可以多现实的
    b.弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂

3.3 Callable+FutureTask

3.3.1 Callable接口的call方法

Callable接口中的call方法和Runnable接口中的run方法的区别如下:

  1. 是否有返回值(Runnable接口没有返回值 Callable接口有返回值)
  2. 是否抛异常(Runnable接口不会抛出异常 Callable接口会抛出异常)
  3. 落地方法不一样,一个是call() ,一个是run()
在这里插入图片描述

3.3.2 Future接口

  1. FutureTask是Future接口的唯一的实现类
  2. FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Futrue得到Callable的返回值
截屏2022-07-13 17.20.00

有了Runnable,为什么还要有Callable接口?

我们假设一共有四个程序需要执行,第三个程序时间很长。

Runnable接口会按照顺序去执行,会依次从上到下去执行,会等第三个程序执行完毕,才去执行第四个。

Callable接口会把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响。

3.3.3 创建线程代码

/*
创建线程的方式三: 实现callable接口 ---JDK 5.0 新增
1.创建一个实现Callable接口的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建callable接口实现类的对象
4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star
6.获取callable接口中call方法的返回值
* */
public class CallableDemo {
    public static void main(String[] args) {
        //3.创建callable接口实现类的对象
        NumThead m=new NumThead();
        //4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象

        FutureTask futureTask = new FutureTask(m);
        //5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        //FutureTask类继承了Runnable接口
        //new Runnable = futrueTask;
        new Thread(futureTask).start();

        //6.获取callable接口中call方法的返回值
        try {
            //get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
            Object sum = futureTask.get();
            System.out.println("总和是:"+sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//1.创建一个实现Callable接口的实现类
class  NumThead implements Callable {
    // class  NumThead implements Callable<Integer>{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        //public Integer call() throws Exception {
        int sum=0;
        for(int i=1;i<=100;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

3.3.4 注意事项

  1. get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞)
  2. 一个FutureTask,多个线程调用call( )方法只会调用一次
  3. 如果需要调用call方法多次,则需要多个FutureTask
  4. 如果直接new FutureTask,不创建线程,则不会调用Callable的call方法
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread t1 = new Thread(futureTask, "t1");
        Thread t2 = new Thread(futureTask, "t2");
        t1.start();
        t2.start();
        System.out.println("结果是:"+futureTask.get());
    }
}
class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("---call方法---");
        return 666;
    }
}

isDone()轮询(后面我们会用CompletableFuture来解决get()阻塞的问题)

  1. 轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果
  2. 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞
public class FutureTaskTest {
    public static void main(String[] args) throws Exception{
        FutureTask futureTask = new FutureTask(()->{
            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"\t"+"coming......");
            return 1024;
        });
        new Thread(futureTask).start();
        //1.果futureTask.get()放到main线程前面,会导致main线程阻塞
        //Object o = futureTask.get();

        /*Object o = futureTask.get();//不见不散,只要出现了get()方法就会阻塞
        System.out.println("不见不散,只要出现了get()方法就会阻塞,获取到的值为:"+o);*/
        //2.过时不候
//        System.out.println(Thread.currentThread().getName()+"\t"+"线程来了.....");
//        Object o2 = futureTask.get(2L, TimeUnit.SECONDS);
        //3.使用轮询
        while(true){
            if(futureTask.isDone()){
                System.out.println("使用轮询来解决,值为:"+futureTask.get());
                break;
            }else{
                System.out.println("阻塞中**********");
            }
        }
    }
}

3.4 线程池

线程池比较重要且内容较多,将会单独做一份笔记。

如下是线程池的内容目录。

截屏2022-07-14 09.05.23

4. 线程优先级

线程有两种调度模型:

  1. 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]。

Thread类中设置和获取线程优先级的方法

  1. public final void setPriority(int newPriority):更改此线程的优先级
  2. public final int getPriority():返回此线程的优先级
  3. 线程默认优先级是5;线程优先级范围是:1-10;
  4. 线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
      ThreadPriority tp1 = new ThreadPriority();
      ThreadPriority tp2 = new ThreadPriority();
      ThreadPriority tp3 = new ThreadPriority();
			
			//除了构造器,还有setName方法可以设置名字
      tp1.setName("高铁");
      tp2.setName("飞机");
      tp3.setName("汽车");
      //设置正确的优先级
      tp1.setPriority(5);
      tp2.setPriority(10);
      tp3.setPriority(1);

      tp1.start();
      tp2.start();
      tp3.start();
在这里插入图片描述

5. 线程状态

5.1 操作系统线程状态-5种

截屏2022-07-14 17.10.19

  1. 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联

  2. 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行 【运行状态】指获取了 CPU 时间片运行中的状态。

    当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 【阻塞状态】
    如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们

  3. 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。

线程的转换具体行为会在 3_线程安全 章节展开

5.2 Java API线程状态-6种

根据 Thread.State 枚举,分为六种状态

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

截屏2022-07-14 17.18.55

  1. NEW 线程刚被创建,但是还没有调用 start() 方法

  2. RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的

    【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)

    BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述

  3. TERMINATED 当线程代码运行结束.

如下代码展示了六种不同的线程状态:

public class TestState {
    public static void main(String[] args) throws IOException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while(true) { // runnable

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (TestState.class) {
                    try {
                        Thread.sleep(1000000); // timed_waiting
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join(); // waiting
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (TestState.class) { // blocked
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state {}", t1.getState());
        log.debug("t2 state {}", t2.getState());
        log.debug("t3 state {}", t3.getState());
        log.debug("t4 state {}", t4.getState());
        log.debug("t5 state {}", t5.getState());
        log.debug("t6 state {}", t6.getState());
        System.in.read();
    }
}

6. 线程控制

6.1 Start、Run

public class StartAndRun {
    @Test
    public void testStart() {
        MyThread thread1 = new MyThread();
        thread1.setName("thread1");
        thread1.start();
        System.out.println("---"+Thread.currentThread().getName());
    }//输出thread1和主线程main
  
    @Test
    public void testRun() {
        MyThread thread1 = new MyThread();
        thread1.setName("thread1");
        thread1.run();
        System.out.println("---"+Thread.currentThread().getName());
    }//输出全都是主线程main
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

上述testStart()testRun()区别仅在于调用的方法不同,

  1. run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行

  2. start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码

说明:线程控制资源类

run() 方法中的异常不能抛出,只能 try/catch

  • 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
  • 异常不能跨线程传播回 main() 中,因此必须在本地进行处理

6.2 sleep、yield

sleep:

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • sleep() 方法的过程中,线程不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread("t1") {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                throw new RuntimeException();
            }
        }
    };
    thread.start();
    System.out.println(thread.getName()+"---"+thread.getState());
    Thread.sleep(500);
    System.out.println(thread.getName()+"---"+thread.getState());
}

输出:说明sleep会使线程由Running到TIMED_WAITING状态。

t1---RUNNABLE
t1---TIMED_WAITING

yield:

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器
  • 会放弃 CPU 资源,锁资源不会释放

yield和sleep的区别:

  1. sleep有休眠时间
  2. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),休眠时无法获得时间片。
  3. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,仍有可能获得时间片。

6.3 join

public final void join():等待这个线程结束

原理:调用者轮询检查线程 alive 状态,t1.join() 等价于:

public final synchronized void join(long millis) throws InterruptedException {
    // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
    while (isAlive()) {
        wait(0);
    }
}
  • join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁

  • 当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕


测试join方法代码:

public class testJoin {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                try {
                    sleep(2000);
                    i = 10;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        t1.start();
        t1.join();
        System.out.println(Thread.currentThread().getName()+"--i结果是:"+i);
    }
}

输出结果是:main--i结果是:10

6.4 interrupt

打断线程

public void interrupt():打断这个线程,异常处理机制

public static boolean interrupted():判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false

public boolean isInterrupted():判断当前线程是否被打断,不清除打断标记

打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)

  • sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)

    @Test
        public void test01() throws InterruptedException {
            Thread thread = new Thread("t1") {
                @Override
                public void run() {
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
            thread.start();
            System.out.println(thread.getState());
            sleep(500);
            thread.interrupt();
            System.out.println(" 打断状态:" + thread.isInterrupted());
        }
    
  • interrupt不会打断正常运行的线程,但是会改变isInterrupted()的布尔值,所以我们需要用这特点自主结束运行。

    public static void main(String[] args) throws Exception {
        Thread t2 = new Thread(()->{
            while(true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if(interrupted) {
                    System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        t2.interrupt();
    }
    

打断 park

park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        System.out.println("park...");
        LockSupport.park();
        System.out.println("unpark...");
        System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
    }, "t1");
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}

如果打断标记已经是 true, 则 park 会失效

LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行

可以修改获取打断状态方法,使用 Thread.interrupted(),清除打断标记

LockSupport 类在 同步 → park-un 详解


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值