java多线程1

3.1创建线程以及运行线程

创建线程

方法一:直接使用线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test01")
public class Test01 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run(){
log.debug("running");
}
};
t.setName("t1");
t.start();
t.setName("t2");
log.debug("running");
}
}

方法二:使用runnable配合Thread

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

Thread代表线程

runnable代表可运行的任务(线程需要执行的代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test02")
public class Test02 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
Thread t = new Thread(r, "t2");
t.start();
}
}

上述的代码可以使用Lambda表达式进行简化

Thread与Runnable之间的关系

分析一下Thread与Runnable之间的关系

runnable对象作为参数传给了Thread的构造方法,其中又调用了init方法,然后又被传给了一个重载的Init()方法,最后发现this.target = target;实际上就是将Runnable对象赋值给了Thread中的一个成员变量

方法二实际上走的还是人家Thread 的run方法,如果你有target(runnable)对象,采用runnable对象的run方法。不论是方法一还是方法二,将来线程运行时候,走的都是线程对象中的run方法。

  • 方法一就是将线程和任务合并在一块了,方法二是将线程和任务分开了。
  • 用Runnable更更容易与线程池等高级API配合使用
  • 用Runnable让任务脱离了Thread继承体系,更灵活。实际上这两就是组合的关系。

方法三:FutureTask配合Thread

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

FutureTask是runnable的一个扩展,可以用来获取任务的执行结果

1
public class FutureTask<V> implements RunnableFuture<V> 

他是一个实现类实现了RunnableFuture的一个接口

1
public interface RunnableFuture<V> extends Runnable, Future<V> 

它间接实现了runnable接口,它可以作为一个任务对象,它比Runnable多了一个future接口

1
public interface Future<V> 

future用来返回任务的执行结果

1
V get() throws InterruptedException, ExecutionException;

就是这个get方法。

但是Runnable接口返回值是一个void,所以它并不能方便地在两个线程直接把一个结果传给另外一个线程。

Callable和runnable接口非常像,但是它是有返回值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

@Slf4j(topic = "cTest03")
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
Thread.sleep(10000);
return 100;
}
});

Thread t1 = new Thread(task, "t1");
t1.start();
//等待task返回一个结果(阻塞),主线程阻塞读取返回值
log.debug("{}", task.get());
}
}

3.2观察多个线程同时执行

主要是理解

  • 交替执行
  • 谁先谁后,不由得我们控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class practice1 {
public static void main(String[] args) {
new Thread(() -> {
while (true){
log.debug("running");
}
}, "t1").start();
new Thread(() -> {
while (true){
log.debug("running");
}
},"t2").start();
}
}

3.3 查看进程的方法

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist
  • taskkill

Linux查看进程和查看线程的信息

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

  • kill 杀死进程

  • top 按下大写H是否切换线程
  • top -H -p 查看某个进程(PID)的所有线程(看哪一个进程的线程)

Java

  • jps命令查看所有的Java进程(同时也适用于Linux系统)
  • jstack 查看Java进程(PID)的所有线程(那一时刻的)情况(相当于快照)

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

3.4 原理之线程运行

3.4.1 栈与栈帧

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

  • 每个栈由多个栈帧组成,对应着每次方法调用时候所占用的内存
  • 每个线程只能有一个活动栈帧,对应正在执行的那一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test04 {
//主方法的调用对应着主线程,(主线程启动之后)就会给它分配一块栈内存
public static void main(String[] args) {
method1(10);
}
private static void method1(int x){
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2(){
Object n = new Object();
return n;
}
}

再创建一个线程,在method1(20);method1(10);建立断点(以线程的方式添加断点),这段程序的主要就是要明白线程的栈内存是相互独立的,每个线程有自己独立的栈内存(里面有多个栈帧),它们之间互不干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test04 {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run(){
method1(20);
}
};
t1.setName("t1");
t1.start();
method1(10);
}
private static void method1(int x){
int y = x + 1;
Object m = method2();
System.out.println(m);
}

private static Object method2(){
Object n = new Object();
return n;
}
}

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

什么时候会发生线程上下文切换?当任务调度器将这些时间片都分配给每个线程运行的时候,每个线程的时间片用完的时候,用完了,就得把CPU的使用权交给其他线程,这时候当前线程就会发生线程上下文切换。也就是从使用CPU到了不使用CPU,这就称之为一次线程上下文切换。

发生的原因:

  • 线程的CPU时间片用光
  • 垃圾回收(它会暂停当前所有的工作线程)
  • 有更高的优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

1、2、3都是被动的4是主动的。

当(Context Switch)发生的时候,需要由操作系统保存当前线程的状态(和Tmux的作用好像),并且恢复另一个线程的状态,Java中对应的概念就是程序计数器(Progmer Counter Register) ,它的作用是记住下一条JVM指令的地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每一个栈帧的信息,如局部变量、操作栈数、返回地址等。
  • Context Switch频繁发生会影响性能

java多线程1
http://example.com/2024/04/10/JUC-03-1/
作者
nianjx
发布于
2024年4月10日
许可协议