Java并发编程第2讲——线程基础

目录

一、线程简介

1.1 什么是线程

1.2 线程的组成

1.3 线程的特点

1.4 Java的main方法

二、线程的创建与启动

2.1 线程的创建

2.1.1 继承Thread类(无返回值)

2.1.2 实现Runnable接口(无返回值)

2.1.3 实现Callable接口(有返回值、可抛异常)

2.1.4 Executors线程池

2.2 start()和run()的区别

三、线程的生命周期

3.1 线程的状态转换

 3.2 转换示意图

四、线程的常用方法

4.1 休眠(sleep)

4.2 放弃时间片(yield)

4.3 加入(join)

4.4 优先级(priority)

4.5 守护线程(daemon)

4.6 线程打断(interrupt)

五、总结


一、线程简介

1.1 什么是线程

这里第一讲已经提到过了,这里就简要说一下吧。

操作系统在运行一个程序时,就会为其创建一个进程。

进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位,一个进程包含多个线程,多个线程共享这个进程的资源。

1.2 线程的组成

任何一个线程都具有基本的组成部分:

  • CPU时间片:操作系统会为每个线程分配执行时间。

  • 运行数据:

    • 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。

    • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。

  • 线程的逻辑代码

1.3 线程的特点

  • 线程抢占式执行,结果随机。

    • 效率高。

    • 可防止单一线程长时间独占CPU。

  • 在单核CPU中,宏观上同时执行,微观上顺序执行。

1.4 Java的main方法

Java一开始就支持多线程,相较于同一时期的其他语言,这是个明显优势。下面使用JMX来看一下一个普通的main方法包含几个线程:

ps:JMX (Java Management Extensions)是一个为应用程序植入管理功能的框架。

public class JavaMain {
    public static void main(String[] args) {
        //java虚拟机管理线程的接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //两个参数控制分别控制lockedMonitors和lockedSynchronizers的信息获取,这里仅线程和线程的堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        //打印信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName());
        }
    }
}

输出内容如下(内容可能不同,大家可以试一下)。

可以看到,一个main方法的运行就包含了多个线程。

二、线程的创建与启动

下面介绍一下线程的创建方式和启动。

2.1 线程的创建

2.1.1 继承Thread类(无返回值)

  1. 创建自定义线程类MyThread,并继承Thread类。
  2. 重写run()方法,在run方法添加要执行的逻辑。
  3. 创建线程对象,调用start()方法。
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("----自定义线程MyThread------");
    }
    
    public static void main(String[] args) {
       MyThread thread=new MyThread();
       //启动线程
       thread.start();
       System.out.println("=====main线程====");
    }
}

输出结果如下:

2.1.2 实现Runnable接口(无返回值)

  1. 创建自定义线程类MyRunnable,实现Runnable接口,重写run()方法。
  2. 创建线程对象MyRunnable,并把这个对象传递给Thread类对象。
  3. 调用start()启动线程。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("----自定义线程MyRunnable------");
    }
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        System.out.println("=====main线程====");
    }
}

输出结果如下: 

2.1.3 实现Callable接口(有返回值、可抛异常)

  1. 创建自定义MyCallable类,实现Callable接口并重写call()方法。

  2. 创建MyCallable对象,并传入FutureTask类对象,创建Thread类,传入FutureTask对象。

  3. 调用start()方法。

ps:FutureTask类,后续会专门写一篇文章介绍FutureTask和CompletableFuture类。

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("--------自定义Callable线程-----------");
        return "hello world";
    }
    public static void main(String[] args) 
            throws ExecutionException, InterruptedException {
        MyCallable callable=new MyCallable();
        FutureTask<String> task=new FutureTask<>(callable);
        new Thread(task).start();
        System.out.println("---------main线程----------");
        //获取返回值
        String s = task.get();
        System.out.println("s = " + s);
    }
}

输出结果如下: 

2.1.4 Executors线程池

  1. 创建Executors类对象

  2. 创建实现Runnable或Callable接口的任务

  3. 将任务提交给线程池执行

public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("-------线程池------");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService executor=Executors.newFixedThreadPool(5);
        //提交任务给线程池执行
        executor.submit(new MyExecutors());
        //关闭线程池
        executor.shutdown();
        System.out.println("=========main线程========");
    }
}

 输出结果如下:

2.2 start()和run()的区别

简单概括就是:start()方法会启动一个新线程并调用run()方法;而run()方法是直接调用线程的逻辑,并不会启动新线程。

下面用代码举例说明一下:

首先看一下start()方法

public class StartAndRun {
    public static void main(String[] args) {
        //创建一个线程(匿名内部类写法)
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Runnable-----"+i);
                }
            }
        });
        //start()
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("Main----"+i);
        }
    }
}

输出结果如下:

如图,Runnable和Main线程是交错穿插执行的,说明start()开启了一个新线程。

下面再看看run()方法:

public class StartAndRun {
    public static void main(String[] args) {
        //创建一个线程
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Runnable-----"+i);
                }
            }
        });
        thread.run();

        for (int i = 0; i < 5; i++) {
            System.out.println("Main----"+i);
        }
    }
}

输出结果如下:

 从上可以看出,Runnable和Main线程是顺序执行,所以run()方法并不会启动新的线程。

三、线程的生命周期

3.1 线程的状态转换

Java中线程的状态分为6种:

  1. 初始(New):新创建了一个线程,还没调用start()方法

  2. 运行(Runnable):Java线程中将就绪(Ready)运行中(Running)统称为“运行”。就绪(Ready):线程对象创建后,其他线程(比如Main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配CPU使用权。运行中(Running):就绪(Ready)的线程获得了CPU时间片,开始执行代码

  3. 阻塞(Blocking):表示线程阻塞于锁(后续会讲到),线程无法继续执行。在该状态下,可以通过满足条件回到就绪状态,然后再次竞争执行

  4. 等待(Waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(Time_Waiting):该状态不同于Waiting,可以在制定的时间后自行返回。

  6. 终止(Terminated):表示该线程已经执行完毕。(终止是最终状态,无法再转换为其他状态)。

 3.2 转换示意图

四、线程的常用方法

4.1 休眠(sleep)

  • public static native void sleep(long millis) throws InterruptedException;

当前线程休眠millis毫秒后,继续执行。

public class Sleep {
    public static void main(String[] args) {
        SleepThread sleepThread=new SleepThread();
        sleepThread.start();
    }
    static class SleepThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"...."+i);
                //休眠
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 输出结果如下:

4.2 放弃时间片(yield)

  • public static native void yield();

当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。

public class Yield {
    public static void main(String[] args) {
        YieldThread yieldThread = new YieldThread();
        YieldThread yieldThread1 = new YieldThread();
        yieldThread.start();
        yieldThread1.start();
    }
    static class YieldThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "...." + i);
                //临时放弃cpu,让给优先级和它相同或比它优先级高的线程
                Thread.yield();
            }
        }
    }
}

输出结果如下:

由输出结果可以看出,启动两个线程执行上述代码,所得到的结果更接近于交替执行。

4.3 加入(join)

  • public final void join() throws InterruptedException

允许其他线程加入到当前线程中。当某个线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才继续执行。

public class Join {
    public static void main(String[] args) {
        JoinThread joinThread=new JoinThread();
        joinThread.start();
        //加入线程
        try {
            //阻塞主线程
            joinThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:-----"+i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class JoinThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"...."+i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果如下:

4.4 优先级(priority)

  • pubic final void setPriority(int newPriority)

改变该线程的优先级,线程优先级为1-10,默认为5,10为最高优先级,1为最低优先级,优先级越高,表示获取CPU机会越多。

ps:线程是抢占式执行的,并不会严格遵守优先级的设置。

public class Priority {
    public static void main(String[] args) {
        PriorityThread priority=new PriorityThread("线程1");
        PriorityThread priority1=new PriorityThread("线程2");
        PriorityThread priority2=new PriorityThread("线程3");
        priority.setPriority(1);
        priority1.setPriority(10);
        priority.start();
        priority1.start();
        priority2.start();
    }
    static class PriorityThread extends Thread{
        public PriorityThread(String name){
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName()+"-----"+i);
            }
        }
    }
}

输出的内容太多了,这里就不贴图片了。循环次数少没效果,我把循环次数加到了1w甚至10w,执行了很多次才有结果:线程1最后执行完的几率最大,因为它的优先级是1,属于最低优先级。所以,别妄想着通过设置线程优先级来控制程序的执行顺序了😂。

4.5 守护线程(daemon)

  • public final void setDaemon(boolean on)

如果参数为true,则标记该线程为守护线程。

在JAVA中线程有两类:用户线程(前台线程)守护线程(后台线程)。

守护可以理解为守护用户线程。如果程序中所有用户线程都执行完毕了,守护线程会自动结束。

例如:垃圾回收线程属于守护线程。

下面创建一个线程循环40次,并设置为守护线程,主线程循环30次。

public class Deamon {
    public static void main(String[] args) {
        DaemonThread daemonThread=new DaemonThread();
        //设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();
        for (int i = 0; i < 30; i++) {
            System.out.println("主线程========="+i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class DaemonThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 40; i++) {
                System.out.println(Thread.currentThread().getName()+"----"+i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果如下:

我们可以看到,虽然子线程循环40次,但执行到30就结束了,原因就是子线程是守护线程,随着主线程的结束而结束。

4.6 线程打断(interrupt)

  • public void interrupt()

当线程处于阻塞状态(例如调用了sleep()、wait()、join()等方法)时,如果线程接收到中断的信号,会抛出一个异常,开发中可以捕获该异常并选择继续执行或者终止。

ps:中断只是发出一个信号,并不会停止线程执行。

public class Interrupt {
    public static void main(String[] args) throws Exception{
        InterruptThread interruptThread=new InterruptThread();
        interruptThread.start();
        System.out.println("20秒内输入任意字符结束子线程。。");
        System.in.read();
        interruptThread.interrupt();
        System.out.println("主线程结束。。。。");
    }
    static class InterruptThread extends Thread{
        @Override
        public void run() {
            System.out.println("子线程开始执行。。。");
            try {
                //先睡20秒
                Thread.sleep(20000);
                System.out.println("子线程自然醒了。。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("子线程被打醒了。。。");
            }
            System.out.println("子线程结束了。。。");
        }
    }
}

输出结果如下:

五、总结

本篇文章首先介绍了线程的基本概念、组成和特点,又使用JMX看了一下main方法的启动包含了几个线程。接着介绍了4种创建线程的方式,所以又介绍了run()和start()的区别(因为面试中有被问到过)。第三部分介绍了线程的6个状态以及它们之间的转换,并画了示意图。第四部分介绍了线程的sleep()、yield()、join()、setPriority()、setDaemon()等方法。

怎么感觉面试的时候或多或少都被面试官问到过,死去的记忆又开始攻击我了。😅

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橡 皮 人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值