Java线程的创建运行和相关方法

1 线程的创建

创建线程的方式分为:创建线程类和匿名内部类

  • start()方法实际上是给CPU注册当前线程,并且触发run()方法;
  • 线程的执行必须是调用start()方法,如果调用run()方法的话就变成了普通类的执行;
  • 应该先创建子线程,再执行主线程的任务,不然子线程会是最先执行完的,而结束。

1.1 直接使用Thread创建

构造器方法:public Thread(), public Thread(String name)

public class ThreadTesting {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run(){
                //执行任务代码
            }
        };
        t1.start();
    }
}

也可以简化创建代码

public class ThreadTesting {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            //需要执行的代码
        },"t1");
        t1.start();
    }
}

优点:编码简单

1.2 使用Runnable配合Thread创建

该方式可以实现线程和要执行的代码分开,将要执行的代码写在Runnable中而创建线程将Runnable对象传入就可以。

构造器方法:public Thread(Runnable target), public Thread(Runnable target, String name)

public class ThreadRunnable {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("runnableTesting");
            }
        };
        Thread t1 = new Thread(runnable);
        //给线程命名
        Thread t2 = new Thread(runnable,"t2");
        t1.start();
        t2.start();
    }
}

缺点:代码稍微复杂

优点:

  • 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性

  • 同一个线程任务对象可以被包装成多个线程对象

  • 适合多个多个线程去共享同一个资源

  • 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立

  • 线程池可以放入实现 Runnable 或 Callable 线程任务对象

1.3 使用FutureTask配合Thread

处理有返回参数的结果,使用线程.get()方法来阻塞获取结果。

步骤:

  1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型

  2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果

  3. 创建一个 Callable 的线程任务对象

  4. 把 Callable 的线程任务对象包装成一个未来任务对象

  5. 把未来任务对象包装成线程对象

  6. 调用线程的 start() 方法启动线程

因为Thread只能执行Runnable对象,而FutureTask就是Runnable对象,所以使用未来任务类将Callbale进行包装。 

public V get() : 同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步

public class ThreadDemo {
    public static void main(String[] args) {
        //创建Callable线程任务对象
        Callable call = new MyCallable();
        //把 Callable 的线程任务对象包装成一个未来任务对象
        FutureTask<String> task = new FutureTask<>(call);
        //创建线程
        Thread t = new Thread(task);
        //执行线程
        t.start();
        try {
            String s = task.get(); // 等待call的结果,获取call方法返回的结果(正常/异常结果)
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }
    }

public class MyCallable implements Callable<String> {
    @Override//重写线程任务类方法,该方法的结果能够通过get()来获得
    public String call() throws Exception {
        return Thread.currentThread().getName() + "->" + "Hello World";
    }
}

简化代码: 

public class FutureTaskThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(()->{
            System.out.println("futureTask");
            return 100;
        });
        new Thread(futureTask,"futureTaskThread").start();
        //获得线程返回的结果
        Integer res = futureTask.get();
        System.out.println("返回的结果是"+res);
    }
}

优缺点同Runnable,只是能够得到线程运行的结果

2 运行原理

Java虚拟机栈:每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程

  • 线程的 CPU 时间片用完(被动)

  • 垃圾回收(被动)

  • 有更高优先级的线程需要运行(被动)

  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法(主动)

程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等  

3 线程相关方法

方法说明
public void start()启动一个新线程;Java虚拟机调用此线程的run方法。(让线程进入就绪状态,运行与否看调度器)每个线程的start方法只能调用一次
public void run()线程启动后调用该方法。(如果传递了runnable参数,则启动后会调用runnable中的run方法否则默认不进行任何操作)
public void setName(String name)给当前线程取名字
public void getName()获取当前线程的名字 线程存在默认名称:子线程是Thread-索引,主线程是main
public static Thread currentThread()获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time)让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争
public static native void yield()提示线程调度器让出当前线程对CPU的使用
public final int getPriority()返回此线程的优先级
public final void setPriority(int priority)更改此线程的优先级,常用1 5 10
public void interrupt()中断这个线程,异常处理机制
public static boolean interrupted()判断当前线程是否被打断,清除打断标记
public boolean isInterrupted()判断当前线程是否被打断,不清除打断标记
public final void join()等待这个线程结束
public final void join(long millis)等待这个线程死亡millis毫秒,0意味着永远等待
public final native boolean isAlive()线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on)将此线程标记为守护线程或用户线程

3.1 run和start

run():包含了要执行这个线程的内容,方法执行结束,线程也随之结束。但是如果直接调用run方法的话不是开启新的线程而是在主线程中顺序执行run方法

start():方法启动新的线程,让该线程处于就绪状态,并且通过新线程间接调用run方法。start线程只能调用一次。 

 面试题:为什么run方法不能抛出异常,只能捕获异常?

 1. 因为run()方法是Runnable接口里面的方法,而Runnable接口在定义run()方法的时候没有抛出任何异常,所以子类在重写run()方法的时候要小于或等于父类(Runnable)的run()方法的异常,所以父类没有抛出异常,子类不能抛出异常

2. 异常不能跨线程传播回 main() 中,因此必须在本地进行处理 

 3.2 sleep和yield

sleep:

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  • sleep() 方法的过程中,线程不会释放对象锁

  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  • 睡眠结束后的线程未必会立刻得到执行,需要等待CPU时间片

  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性,其内部也是Thread的sleep方法,只不过是换算了单位,有着更好的可读性

  • sleep的一个应用是在while(true)循环中加上一个较小的sleep,防止循环空转浪费cpu

yield:

  • 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用

  • 具体的实现依赖于操作系统的任务调度器

  • 会放弃 CPU 资源,锁资源不会释放

3.3 优先级priority

线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

3.4 join()

 public final void join():等待该线程结束 可以添加参数,设置最长等待时间

原理:轮询检查alive状态

synchronized (t1) {
    // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
    while (t1.isAlive()) {
    	t1.wait(0);
    }
}

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

join方法实现线程同步:

  • join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行

    • 需要外部共享变量,不符合面向对象封装的思想

    • 必须等待线程结束,不能配合线程池使用

  • Future 实现(同步):get() 方法阻塞等待执行结果

    • main 线程接收结果

    • get 方法是让调用线程同步等待

public class JoinTest {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
        });
        t1.start();
        t1.join();//不等待线程执行结束,输出的10,注释掉这行输出0
        System.out.println(r);
    }
}

3.5 interrupt 打断 !!!

interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。

interrupt() 并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。

一个线程如果有被中断的需求,那么就需要这样做:

  1. 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  2. 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)

相关方法: 

方法        说明
public void interrupt()打断这个线程,异常处理机制。注意区分打断正常运行的线程还是正在sleep,wait,join阻塞的线程的区别
public static boolean interrupted()判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted()判断当前线程是否被打断,不清除打断标记

其他线程通过 该线程的interrupt()方法对其进行一个打断的提醒。线程通过检查自身是否被中断过来进行相应,线程通过调用方法isInterrupted()来查看是否被打断,也可以调用interrupted()对线程的中断表示进行复位。

首先讲interrupt()方法:

打断正常运行的线程的话,并不会清除中断标识,这时调用isInterrupted()返回true。

对正处于sleep,wait,join阻塞状态的线程进行打断的话,这些方法在抛出InterruptedException方法之前,Java虚拟机会先将线程的中断标识清除,这时调用isInterrupted()返回false,然后再返回中断异常。

interrupt()t打断park并不会清除打断标记,并且还会顺便调用unpark(),如下面代码的执行;

    public static void test03() throws InterruptedException {
        //打断park线程
        Thread t1 = new Thread(()->{
            System.out.println("park......");
            LockSupport.park();
            System.out.println("unpark......");
            boolean flag = Thread.currentThread().isInterrupted();
            System.out.println("打断状态"+ flag);
        });
        t1.start();
        Thread.sleep(500);
        //如果注释掉下面这行,则会只输出”parking......“ 说明打断不会清除park不会清除打断标记,并且还会调用unpark()
        t1.interrupt();
    }
park......
unpark......
打断状态true

再看interrupted()方法:

如果线程处于打断状态,那么调用interrupted()方法以后会返回true,并且清除打断标记,这时如果再调用interrupted()的话会返回false;

最后IsiIterrupted()方法,用来判断打断标记是否为true。

3.6 不推荐的方法

像一些暴力中断线程的方法现在已经不推荐使用,比方说stop(),suspend(),resume()。

4 主线程和守护线程

通过调用Thread.setDaemon(true)来将Thread设置为守护线程,通过Thread.setDaemon(false)来设置用户线程。如果不设置线程属性,那么默认为用户线程。

守护线程:只要非守护线程运行结束,守护线程也会强制结束,不管守护线程代码是否执行完毕。如果没有用户线程,都是守护线程,那么JVM结束。垃圾回收器就是一种守护线程。

用户线程: 主线程结束后用户线程还会继续运行,JVM存活

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值