二、Java线程

Java程序创建启动的时候,默认就有一个线程,也就是主线程,在运行了。

一、创建和运行线程

1)方法1:直接使用Trhead

//创建线程对象 
Thread t = new Thread() {
    @Override
    public void run() {
        //要执行的任务
    }
};
//设置线程名称   默认是Tread-x  x表示数字从0递增
t.setName("t1");
//启动线程
t.start();

2)方法2:使用Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务 (线程要执行的代码)
Runnable runnable  = new Runnable() {
    @Override
    public void run() {
        // 要执行的任务
    }
};
//创建线程对象   可以使用两个参数的构造,第二个参数表示设置线程名称
Thread t = new Thread( runnable );
//启动线程
t.start();

3)简化

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

有且仅有一个抽象方法的接口,会在接口上方添加注解,这种接口可以使用lambda 精简代码

二、Java线程_第1张图片

lambda语法可以跳转这篇

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/liziing/qm71mg/hqlcf3pgsey339bi

4) 总结

  • 方法1 是把线程和任务合并在一起, 方法2 是把线程和任务分开了
  • 用Runnable 更容易与线程池等高级API 配合
  • 用 Runnable 让任务类脱离了Thread 继承体系,更灵活

5)方法3:FutrueTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数, 用来处理有返回结果的情况

FutureTask task = new FutureTask<>(new Callable() {
    @Override
    public Integer call() throws Exception {
        // 执行的任务
        //返回的值
        return xxx;
    }
});
Thread t = new Thread(task, "t3");
t.start();

//主线程阻塞, 同步等待 task 执行完毕的结果
task.get();

二、原理之线程运行

1)栈 与 栈帧

JVM由堆、栈、方法区所组成,栈内存是给线程用的,每个线程启动,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

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

使用cpu -> 不使用cpu 称为一次线程上下文切换

因为以下一些原因导致 cpu 不在执行当前的线程, 转而执行 另一个线程的代码

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

当Context Switch 发生时,需要有操作系统保存当前的状态,并恢复另一个线程的状态,Java中对应的概念是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。

  • 记录的状态有:程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会印象性能

三、常见方法

1) start 与 run

线程需要通过start方法的方式来自己调用run(),才能够起到异步执行的效果

如果直接通过手动来调用run(),这样是通过主线程来调用的,所以不能够起到异步的效果

public static void main(String[] args) {
	Thread t1 = new Thread("t1");
    @Override
    public void run() {
        log.debug(Thread.currentThread().getName() ...);
        FileReader.read(Constants.MP4_FULL_PATH);
    }
};

t1.run();
log.debud("do other things ...);

二、Java线程_第2张图片

2)sleep 与 yield

1. Sleep()

  • 调用sleep方法之后,当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • 正在睡眠的线程,可以通过其他线程 调用正在睡眠的线程的 interrupt 方法来打断正在睡眠的线程,这时候 sleep 方法被打断了终止了,会抛出 InterruptedException 异常
  • 睡眠结束后的线程未必会立刻得到执行(可能CPU的时间片正在执行其他线程,需要等待时间片到来)
  • 建议用 TimeUnit 的sleep 代替 Thread 的sleep 来获取更好的可读性 (TimeUnit.SECONDS.sleep(1) <=> Thread.sleep(1000))

2. Yield()

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态, 然后调度执行其他线程
  • 但是让出了cpu使用权,这时候如果没有 其实时间片可以执行,那么也是会回到自己身上

3. 总结:

  • sleep 调用是进入等待阻塞状态, cpu时间片不会分配给阻塞状态的线程,需要等到他醒过来进入就绪状态。
  • yield 调用是进入 就绪 状态, cpu 是会将时间片分配给就绪状态的线程, 也就是说如果没有其他线程,这时候yield 也就是没让出去
  • sleep 有时间的概念, 但是yield 是没有的,只是当前这一刻的cpu使用权让出去了,如果没有其他线程,还是会重新分配给当前线程。

4. setPriority()

  • 调用setPriority可以设置线程的优先级, 1-10 ,默认5, 数字越大,优先级越高,CPU分配时间片的概率就大
  • 但是具体还是要看任务调度器具体分配,有可能设置之后,没有效果
  • 因为如果cpu比较忙的时候,设置优先级高的的线程会获得更多的时间片,但是如果cpu比较闲,那么设置就几乎没作用

5. 案例 - 防止 CPU 占用 100 %

sleep实现

在没有利用cpu来计算时,不要让 while(true) 空转浪费cpu,这时可以使用yield 或 sleep 来让出cpu的使用权给其他程序

二、Java线程_第3张图片

3)join

同步应用

场景:

static int r = 0;
public static void main(String[] args) throws InterruptedException {
    test1();
}
private static void test1() throws InterruptedException {
	Thread t1 = new Thread(() -> {
        sleep(1);
        r = 10;
    });
	t1.start();
	// t1.join();  // 解决方法
	log.debug("结果为:{}", r);
}

因为线程1等待了1秒,所以主线程已经输出了 r 的结果,结果为0

解决:

可以让主线程 sleep 1秒来得到 r = 10 的结果,但是这种方式需要我们知道sleep具体的等待时间,所以不好控制

可以让 要等待哪个线程结束就让哪个线程调用 join 方法,这样就会等待 线程执行结束之后才会往下执行

限时同步

join (毫秒数) 可以添加参数,指的是具体等待多少毫秒之后结束,如果线程还没有执行完,也会直接结束

但是如果等待超过了线程的执行时间,那么具体会以 线程执行时间为准,不会真的等待那么长时间,会提前结束

4) interrupt

1. 打断阻塞(sleep、wait、join)的线程

  • 打断等待中的线程之后,会清空打断状态,也就是调用打断的线程调用 isInterruped()方法显示为false
  • 因为打断等待中的线程会抛出 InterruptedException异常,然后将 是否打断的标记置为 false

2. 打断正常线程

  • 正常的线程调用interrupt打断方法之后,线程会被标记成被打断,调用 isInterruped()方法显示为true
  • 但是线程不会因此而终止,只是有一个标记是打断了,然后可以通过这个变量来判断是否需要终止运行

3. 设计模式

3.1. 两阶段终止模式(Two Phase Termination)

3.1.1. 错误思路

二、Java线程_第4张图片

3.1.2. 执行流程

二、Java线程_第5张图片

4. 补充:

调用下面方法,如果判断是true,然后会将true置为false,这是两者区别

5. 打断park()

park() 不是 Thread中的方法,是LockSupport中的方法,线程中调用park方法之后,会让线程停在park位置,

当其他线程调用了park所在的线程的interrupt方法,也就是打断之后,那么就会终止park,继续执行park所在线程中的代码。

但是一旦打断之后,打断标记为true,在park线程中第二次调用park就失效了,因为已经是打断标记是true了,所以这时候可以用到上面的interrupted方法,直接置为false。

才能使得第二次调用有效停止。

5)主线程和守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程,调用线程的setDaemon(true)

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

注意:

  • 垃圾回收器线程就是一种守护线程,如果没有引用指向之后就会执行垃圾回收,如果程序终止了,那么垃圾回收也就终止了。
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完当前请求。

6)五种状态

二、Java线程_第6张图片

二、Java线程_第7张图片

7) 六种状态

二、Java线程_第8张图片

二、Java线程_第9张图片

你可能感兴趣的:(JUC并发编程,java,开发语言,jvm,算法,后端)