深入学习Java多线程(一)多线程的创建方式、线程的状态、sleep和wait、start和run和线程常用方法等

一、JAVA线程实现/创建方式

并发与并行的区别

并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
并发是指两个任务都请求运行,而处理器只能接受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。
如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。

1.继承 Thread 类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方 法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

package com.lsh.createthread;

/**
 * @author :LiuShihao
 * @date :Created in 2021/2/22 10:40 上午
 * @desc : 继承Thread类
 */
public class ExtendsThread  extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread创建多线程");
    }

    public static void main(String[] args) {
        ExtendsThread myThread = new ExtendsThread();
        myThread.start();
    }
}

2.实现 Runnable 接口

package com.lsh.createthread;

/**
 * @author :LiuShihao
 * @date :Created in 2021/2/22 10:45 上午
 * @desc :
 */
public class ImplRunable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口创建多线程");
    }

    public static void main(String[] args) {
        ImplRunable ImplRunable = new ImplRunable();
        //创建Thread对象,构造参数中传入刚刚创建的Runable子类对象
        Thread thread = new Thread(ImplRunable);
        //调用start方法开启新线程
        thread.start();
    }
}

内部类写法:

 //内部类写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口创建多线程");
            }
        }).start();

Lambda表达式写法:

/**
 * Runnable 是一个函数式接口,可以使用lambda表达式
 */
 new Thread(()->{
     System.out.println("使用Lambda表达式形式创建多线程");
 }).start();

3.Callable和Future实现有返回结果的多线程

CallableFuture位于java.util.concurrent 包下。
Callable 是个接口,只有一个方法call()通常配合ExecutorService使用。
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

package com.lsh.createthread;

import java.time.Instant;
import java.util.ArrayList;
import java.util.concurrent.*;

/**
 * @author :LiuShihao
 * @date :Created in 2021/2/22 10:54 上午
 * @desc :
 */
public class CallableThread {
     public static void main(String[] args) throws ExecutionException, InterruptedException {
          //创建一个线程池
          ExecutorService pool = Executors.newFixedThreadPool(4);
          // 创建多个有返回值的任务
          ArrayList<Future> futures = new ArrayList<>();
          for (int i = 0; i < 4 ; i++) {
//               Callable c = new Callable() {
//                    @Override
//                    public Object call() throws Exception {
//                         System.out.println("call方法执行");
//                         return null;
//                    }
//               };
               //Lambda表达式写法
               Callable callable = () -> {
                    System.out.println("call方法执行");
                    return Instant.now();
               };
               // 执行任务并获取 Future 对象
               Future f = pool.submit(callable);
               futures.add(f);
          }
          // 关闭线程池
          pool.shutdown();
          //获取所有的执行结果
          for (Future future : futures) {
               // 从 Future 对象上获取任务的返回值,并输出到控制台
               System.out.println("res:"+ future.get().toString());
          }
     }

}

在这里插入图片描述

4.基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

package com.lsh.createthread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author :LiuShihao
 * @date :Created in 2021/2/22 5:34 下午
 * @desc :
 * 线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
 */
public class NewFixedThreadPool {
    public static void main(String[] args) {
    //使用Executors创建固定线程数的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while(true){
            threadPool.execute(() ->{
                System.out.println(Thread.currentThread().getName()+" is running...");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

线程池详细见下一章
深入学习Java多线程(二)线程池的创建方式

二、线程生命周期(状态)

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。 在线程的生命周期中,它要经过新建(New)就绪(Runnable)运行(Running)阻塞 (Blocked)死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自 运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

2.1 新建状态(NEW)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配 内存,并初始化其成员变量的值

2.2 就绪状态(RUNNABLE)

当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

2.3 运行状态(RUNNING)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状 态。

2.4 阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状 态。阻塞的情况分三种:

  1. 等待阻塞(o.wait->等待对列) : 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。
  2. 同步阻塞(lock->锁池) : 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
  3. 其他阻塞(sleep/join) : 运行(running)的线程执行 Thread.sleep(long ms)t.join()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态。

2.5 线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。

  1. 正常结束 : run()或 call()方法执行完成,线程正常结束。
  2. 异常结束 : 线程抛出一个未捕获的 ExceptionError
  3. 调用 stop : 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

在这里插入图片描述

2.6 终止线程 4 种方式

2.6.1 正常运行结束

程序运行结束,线程自动结束。

2.6.2 使用退出标志退出线程

一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出。

定义了一个退出标志 exit,当 exittrue 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

public class ThreadSafe extends Thread{
    public volatile boolean exit = false;
    @Override
    public void run() {
        while (!exit){
        //do something
        }
    }
}

2.6.3 使用Interrupt()方法中断线程

使用 interrupt()方法来中断线程有两种情况:

  1. 线程处于阻塞状态:如使用了 sleep、同步锁的 wait、socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
  2. 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

注意:
interrupt()方法通过修改了被调用线程的中断状态来告知线程中断,但是线程有没有中断是取决于自己。

2.6.4 stop 方法终止线程(线程不安全)

程序中可以直接使用 Thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:Thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用Thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

三、sleep与wait区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于 Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

四、start与run区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用Thread类的start()方法类启动一个线程,此时线程处于就绪状态,并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

五、线程基本方法

线程相关的基本方法有 waitnotifynotifyAllsleepjoinyield 等。

线程等待(wait)

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

线程睡眠(sleep)

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

线程让步(yield)

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

Join 等待其他线程终止

join() 方法,当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续。
调用join()方法,会插队到当前在执行线程,当前在执行线程被阻塞,直到”加队”的线程全部执行完,当前线程再执行

 @Test
    public void testJoin() throws InterruptedException {
        String name = Thread.currentThread().getName();
        Thread thread2 = new Thread(() -> {
            System.out.println("这时 thread2 执行完毕之后才能执行thread1");
            for (int i = 0; i < 10; i++) {
                System.out.println("线程222正在执行--"+i);
            }
        });
        //  此时为主线程,调用join方法的线程相当于插队的,之前当时正在运行的线程(被插队的线程)需要等待插队的线程执行完后才能继续执行
        for (int i = 0; i < 100; i++) {
            if (i == 5){
                thread2.start();
                thread2.join();
            }
            System.out.println(Thread.currentThread()+"正在执行--"+i);
        }
    }

在这里插入图片描述

线程唤醒(notify)

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

设置线程的优先级(setPriority)

setPriority()方法设置线程的优先级1-10
在这里插入图片描述
在这里插入图片描述

// 设置最小的线程的优先级
thread.setPriority(Thread.MIN_PRIORITY); 
// 设置最大的线程的优先级
thread.setPriority(Thread.MAX_PRIORITY); 
thread.setPriority(1);
thread.setPriority(10);

其他方法

sleep():强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线
程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liu_Shihao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值