java并发编程 2:java线程基础知识

创建和运行线程

方法一: 直接使用 Thread

public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
              	// run 方法内实现了要执行的任务
                System.out.println("线程执行啦");
            }
        };
        // 设置名称
        thread.setName("线程1");
        // 运行
        thread.start();

        System.out.println("主线程结束");
    }
}

方法二:使用 Runnable配合 Thread

public class CreateThread02 {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程执行啦");
            }
        };

        // 创建线程
        Thread thread = new Thread(runnable, "线程2");
        // 执行
        thread.start();

        System.out.println("主线程结束");
    }
}

Java 8 以后可以使用 lambda 精简代码:

public class CreateThread02 {

    public static void main(String[] args) {
        // 创建线程
        Thread thread = new Thread(() -> System.out.println("线程执行啦"), "线程2");
        // 执行
        thread.start();

        System.out.println("主线程结束");
    }

这种方法把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合。

方法三:FutureTask配合Thread

FutureTask 能够接收 Callable 类型的参数,主要用来处理有返回结果的情况,后面还会详细讲解。

示例:

public class CreateThread03 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建FutureTask对象
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("线程开始啦");
                // 暂停1秒
                Thread.sleep(1000);
                return 100;
            }
        });


        // 创建线程
        Thread thread = new Thread(task, "线程1");
        // 执行
        thread.run();

        // 获取线程的结果,会等到线程结果返回才执行
        System.out.println(task.get());

        System.out.println("主线程结束");
    }
}

也可以使用lambda表达式简化:

public class CreateThread03 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建FutureTask对象
        FutureTask<Integer> task = new FutureTask<>(() -> {
            System.out.println("线程开始啦");
            // 暂停1秒
            Thread.sleep(1000);
            return 100;
        });


        // 创建线程
        new Thread(task, "线程1").start();

        // 获取线程的结果,会等到线程结果返回才执行
        System.out.println(task.get());

        System.out.println("主线程结束");
    }
}

查看进程线程

linux

ps -fe 查看所有进程

ps -fT -p 查看某个进程(PID)的所有线程

kill 杀死进程

top 按大写 H 切换是否显示线程

top -H -p 查看某个进程(PID)的所有线程

Java

jps 命令查看所有 Java 进程

jstack 查看某个 Java 进程(PID)的所有线程状态

jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

线程运行原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

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

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

线程上下文切换(Thread Context Switch)

有时候线程并不是可以一直执行的,有一些原因会导致 cpu 不再执行当前的线程,转而执行另一个线程的代码:

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当发生线程切换时,并不是直接换另一个线程运行那么简单。为了下次再运行相同的线程,就需要上下文切换,也就是需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。

需要知道的是线程上下文切换太频繁发生会影响程序性能。

常见方法了解

方法static功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)设置修改线程名
getPriority()获取线程优先级
setPriority(int)设置线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除 打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
interrupted()static判断当前线程是否被打断会清除 打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield()static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

start与run

调用run

示例:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                log.info("子线程执行啦");
            }
        };
        thread.setName("线程1");
        thread.run();

        log.info("主线程结束");
    }
}

结果如下:

2023-04-16 15:59:16,712 - 0    INFO  [main] up.cys.chapter01.CreateThread01$1:18  - 子线程执行啦
2023-04-16 15:59:16,719 - 7    INFO  [main] up.cys.chapter01.CreateThread01:27  - 主线程结束

根据打印的日志看到"子线程执行啦"虽然打印出来了,但是其执行是在[main]线程中。

调用start

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                log.info("子线程执行啦");
            }
        };
        thread.setName("线程1");
        thread.start();

        log.info("主线程结束");
    }
}

打印如下:

2023-04-16 16:01:53,905 - 0    INFO  [main] up.cys.chapter01.CreateThread01:27  - 主线程结束
2023-04-16 16:01:53,905 - 0    INFO  [线程1] up.cys.chapter01.CreateThread01$1:18  - 子线程执行啦

根据打印的日志看到"子线程执行啦"是在[线程1]中执行的,这才是真的开启开启一个线程了。

结论

  • 直接调用 run 是在主线程中直接执行了 run方法,会把run 方法当成一个main 线程下的普通方法去执行,没有启动新的线程
  • 使用 start 会启动一个线程并使线程进入了就绪状态,当分配到 时间片 后就可以开始运行了

一般开发人员使用start来启动线程。

sleep与yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级

  1. 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  2. 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

join

join会使当前线程等待指定线程运行结束才继续向下运行。

前面我们使用过sleep来让主线程等待一定时间,如下:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class JoinTest01 {
    static int r = 0;

    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.start();

        // 这里的等待2秒时为了等子线运行完程,这样拿到的r值才是被修改后的
        TimeUnit.SECONDS.sleep(2);
        log.info("结果为:{}", r);
        log.info("结束");
    }
}

但是这样有个坏处就是,我们不知道线程需要等多久。这时候就需要使用join:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class JoinTest01 {
    static int r = 0;

    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.start();
        // 使用join让主线程等待子线程结束
        t1.join();

        log.info("结果为:{}", r);
        log.info("结束");
    }
}

注意:用 join,需加在 t1.start() 之后。

join还可以带一个参数,表示只等待多久,这里就不在演示。

interrupt

此方法用来打断线程。但是打断不同状态的线程效果是不一样的。

1.打断sleep,wait,join的线程

这几个方法都会让线程进入阻塞状态。打断 sleep 的线程, 会清空打断状态,以 sleep 为例。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
        log.info(" 打断状态: {}", t1.isInterrupted());
    }
}

输出:

java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:339)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at up.cys.chapter01.InterruptTest.lambda$test1$0(InterruptTest.java:21)
	at java.base/java.lang.Thread.run(Thread.java:834)
2023-04-16 17:06:30,827 - 0    INFO  [main] up.cys.chapter01.InterruptTest:29  -  打断状态: false

2.打断正常运行的线程

打断正常运行的线程, 不会清空打断状态:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class InterruptTest02 {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        Thread t2 = new Thread(()->{
            while(true) {
                // 获取打断标记
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                // 如果标记为true,停止
                if(interrupted) {
                    log.info(" 打断状态: {}", interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        // 主线程打断子线程:注意并不会真正的打断,自线程还是会正常运行,需要自线程获取标记自己决定是否中断
        log.info("打断");
        t2.interrupt();
    }
}

输出如下:

2023-04-16 17:20:48,575 - 0    INFO  [main] up.cys.chapter01.InterruptTest02:34  - 打断
2023-04-16 17:20:48,588 - 13   INFO  [t2] up.cys.chapter01.InterruptTest02:26  -  打断状态: true

不推荐使用的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:

stop() 、suspend()、resume()

主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守

护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DaemonTest {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.info("开始运行...");
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("运行结束...");
        }, "daemon");
        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1);
        log.info("运行结束...");
    }
}

输出如下:

2023-04-17 00:09:48,359 - 0    INFO  [daemon] up.cys.chapter01.DaemonTest:19  - 开始运行...
2023-04-17 00:09:48,359 - 0    INFO  [main] up.cys.chapter01.DaemonTest:31  - 运行结束...

垃圾回收器线程就是一种守护线程;

Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

线程状态

操作系统中的线程状态

这是从 操作系统 层面来描述的。

在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态;等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】;与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

java中的线程状态

这是从 Java API 层面来描述的,根据 Thread.State 枚举,分为六种状态

在这里插入图片描述

  • NEW :线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE :当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING :都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED :当线程代码运行结束
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java并发编程是指在Java程序使用多线程实现并发执行的编程技术。它能有效利用多核处理器的优势,提升程序的性能和响应能力。以下是Java并发编程基础知识: 1. 线程与进程:Java线程是程序执行的最小单位,线程共享进程的资源,包括内存空间和文件等。多线程可以同时执行不同的任务,相比单线程能更高效地利用系统资源。 2. 线程创建Java创建线程有两种方式,一种是继承Thread类,实现run()方法;另一种是实现Runnable接口,重写run()方法。通过调用start()方法启动线程。 3. 线程同步:多个线程在访问共享资源时可能会产生竞争条件,可能会导致数据不一致或者出现死锁等问题。通过使用同步机制来保证线程安全,例如使用synchronized关键字实现对共享资源的互斥访问。 4. 线程通信:线程之间可以通过共享变量来进行通信。使用wait()、notify()和notifyAll()方法实现线程的等待和唤醒。 5. 线程池:线程池是一种管理线程的机制,可以有效控制线程的数量和复用线程资源,避免频繁创建销毁线程的开销。 6. 并发容器:Java提供了一些线程安全的数据结构,如ConcurrentHashMap和ConcurrentLinkedQueue等,用于在多线程环境下安全地操作数据。 7. 原子操作:Java提供了一些原子操作类,如AtomicInteger和AtomicLong等,它们能够保证对共享数据的操作是原子的,不会发生数据不一致的情况。 8. 同步工具类:Java提供了一些同步工具类,如CountDownLatch和CyclicBarrier等,用于控制线程的执行顺序和线程之间的同步。 以上是Java并发编程基础知识,掌握了这些知识可以更好地利用多线程来提高程序的性能和并发能力。同时也需要注意并发编程可能带来的线程安全问题,合理使用同步机制和并发容器等工具类来保证程序的正确运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ethan-running

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

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

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

打赏作者

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

抵扣说明:

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

余额充值