从零开始的并发世界生活-第二天

本文详细介绍了Java中线程的创建和运行,包括直接使用Thread、实现Runnable接口以及使用FutureTask的方法。同时,文章讨论了线程运行的原理,线程上下文切换的影响,以及如何在Windows和Linux系统中查看线程信息。此外,还展示了使用jconsole管理Java进程的例子。
摘要由CSDN通过智能技术生成

来了来了,继续学习并发知识

一.线程的创建和运行

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频繁发生会影响性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值