来了来了,继续学习并发知识
文章目录
一.线程的创建和运行
1.方法一:直接使用Thread
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
//创建线程对象
Thread t = new Thread(){
@Override
public void run() {
//要执行的任务
log.debug("running");
}
};
//(可选)为线程起个名字
t.setName("Thread1");
//启动线程
t.start();
log.debug("running");
}
}
执行结果:
14:44:18.713 c.Test1 [main] - running
14:44:18.713 c.Test1 [Thread1] - running
2.方法二:使用Runnable
可以将要执行的代码写入Runnable中:
public void ThreadCreate2(){
//要执行的任务
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("runnning");
}
};
//创建线程
Thread thread = new Thread(runnable);
thread.setName("Thread2");
//运行线程
thread.start();
log.debug("running");
}
扩展:使用java8的lambda简化写法:
点进Runnable的源码中可以看到其加了@FunctionalInterface注释
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
在java8中,如果一个接口中只有一个方法,那么此接口就会被加上@FunctionalInterface注解,并且其可以使用lambda表达式简化书写:
public void ThreadCreate2(){
//要执行的任务
Runnable runnable = () -> {
log.debug("runnning");
};
//创建线程
Thread thread = new Thread(runnable);
thread.setName("Thread2");
//运行线程
thread.start();
log.debug("running");
}
或者:
public void ThreadCreate3(){
Thread thread = new Thread(() -> { log.debug("ruunning");},"thread3");
log.debug("running");
}
3.方法一和方法二创建运行线程的原理
点进Thread类:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
点进init:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
再点进init,省略其他代码,只看this.target = target这句,就是将传进来的Runnable对象赋给当前Thread对象中的target:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
this.target = target;
...
}
同时在这个类中还有一个run方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
如果我们采用第二种方法创建进程并运行,实际执行到后面就是调用Runnble类的run方法进行任务执行;如果采用第一种方法创建进程并运行实际就是重写这个run方法从而不需要Runnble对象就能执行我们重写后的任务代码。
4.小结
- 方法一是把线程和任务合并在了一起,方法而是把线程和任务分开了;
- 用Runnble更容易与线程池等高级API配合;
- 用Runnble让任务类脱离了Thread继承体系,更灵活。
5.方法三:FutureTask配合Thread
对Runnble的一个扩展,获取任务的执行结果
查看源码,FutureTask是实现了RunnableFuture接口,而Runnble接口又继承了Runnble和Future接口,所以FutureTask实际可以跟Runnable做一样的事:
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
FutureTask配合Callable就能在任务执行完后,将结果传递给其他线程。
Callable源码:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
案例:
@Slf4j(topic = "c.Test1")
public class Test3 {
public static void main(String[] args) {
//编写任务代码
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
log.debug("running...");
Thread.sleep(10000);
return 100;
}
});
Thread t1 = new Thread(task,"t1");
Thread t2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
log.debug("{}",task.get());
}
},"t2");
t1.start();
t2.start();
}
}
tast.get()是阻塞等待task返回的结果,所以这段程序的执行情况应该是点击运行程序先创建t1和t2进程,然后线程t1打印日志running,线程t2阻塞等待t1线程中的FutureTask返回值,10秒后线程t2获得t1线程返回的100,然后线程t2日志打印输出100。
执行结果:
14:47:49.096 c.Test1 [t1] - running...
14:47:59.099 c.Test1 [t2] - 100
二.线程运行
1.多个线程同时运行的现象
多线程运行的现象主要就是:交替执行、谁先谁后不受控制
案例:
@Slf4j(topic = "c.Test1")
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(true){
log.debug("t1----running");
}
},"t1");
Thread t2 = new Thread(() -> {
while(true){
log.debug("t2----running");
}
},"t2");
t1.start();
t2.start();
}
}
执行结果:
14:55:52.385 c.Test1 [t2] - t2----running
14:55:52.385 c.Test1 [t2] - t2----running
14:55:52.385 c.Test1 [t2] - t2----running
14:55:52.385 c.Test1 [t2] - t2----running
14:55:52.385 c.Test1 [t2] - t2----running
14:55:52.385 c.Test1 [t1] - t1----running
14:55:52.385 c.Test1 [t1] - t1----running
14:55:52.385 c.Test1 [t1] - t1----running
14:55:52.385 c.Test1 [t1] - t1----running
14:55:52.385 c.Test1 [t1] - t1----running
14:55:52.385 c.Test1 [t1] - t1----running
...
2.查看进程线程的方法
windows
-
使用任务管理器可以查看进程和线程数
-
tasklist 查看所有进程
tasklist | findstr java :查看关键字为java的进程
-
taskkill 杀死进程 taskkill /F /PID 19293
/F为强制杀死 -
jps 查看java进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程
- jps 查看java进程
- kill 4202 杀死进程id为4202的进程
- top 以动态的方式查看进程的信息,而且信息显示更为具体
- jstack 4262 抓取4262进程中的线程信息
3.使用jconsole来管理控制java进程
首先启动一个线程,这里就启动上面那个while(true)的线程例子,循环执行线程不让线程停下来,然后win+R输入jconsole,弹出
选中本地进程中正在运行的那个进程,点击连接,然后选择不安全连接,进入下面界面,可以监控管理此进程:
4.线程运行的原理
Java Virtual Machine Stacks(Java虚拟机栈)
我们都知道JVM中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
- 也就是:一个线程一个栈,一个方法是一个栈中的栈帧
案例:
public class Test5 {
public static void main(String[] args) {
method1(10);
}
public static void method1(int x){
int y = x + 1;
Object o2 = method2();
System.out.println(o2);
}
public static Object method2(){
Object o = new Object();
return o;
}
}
在method1(10)处打断点:
进行调试:
点击调试后先执行主方法,此时Debug栏中的Frames栈帧处显示:
也就是此时主方法加入了栈,并为其分配了栈帧,右侧Variables中显示:
方法执行时的局部变量,方法参数,返回地址都会在栈帧中存储,Variables就能查看。
点击Step into,执行调用method1方法,此时method1方法加入栈,并获得栈帧:
继续Step into,直到执行完method2,method2退出栈,method1重新获得栈帧,执行完毕后退出栈,main方法获得栈帧,执行完后退出栈,整个流程结束。
方法执行完内存就被释放了,方法被调用时会记录一个返回地址,当这个方法被执行完后,就会回到此方法的返回地址那继续执行下面的代码。
以上情况是只有一个线程的情况,如果是在多线程的情况下栈和栈帧的情况又是怎么样的呢?
多线程情况下,就是一个线程一个栈,每个线程的栈内存是相互独立的,但是堆和方法区是共享的。
案例:
在调试的红色断点处右击,选择Suspend为Thread
点击调试:
点击Frame中的下拉框,可以看到存在两个栈内存:main和t1,也就是一个线程一个栈内存。
选中main栈内存,点击Step into,然后切换到t1栈内存,main中点击的Step into并不会影响t1线程的执行。
5.线程上下文切换(Thread Context Switch)
因为以下一些原因导致cpu不在执行当前的线程,转而执行另一个线程的代码:
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用就是记住下一条jvm指令的执行地址,是线程私有的。 - 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch频繁发生会影响性能