java并发编程(2) java线程1

java并发编程(2) java线程1,第1张

java并发编程(2) java线程1

文章目录

前言1、创建多线程的三种方法

1. Thread创建2. Runnable创建3. 对比4. FutureTask 配合 Thread 2、线程运行-现象3、查看进程和线程4、原理

4.1 栈帧与栈4.2 多线程和栈帧 5、线程上下文切换

1. 原因2. java并发编程 6、Thread常见方法7、start 与 run


前言

这一系列基于黑马的视频:java并发编程,目前还没有看完,整体下来这是我看过的最好的并发编程的视频。


1、创建多线程的三种方法 1. Thread创建

直接使用 Thread创建线程,重写run方法,再调用start

@Slf4j
public class Test1 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                log.debug("running");
            }
        };

        thread.start();

        log.debug("running");
    }
}

2. Runnable创建

把线程和任务分开用 Runnable 更容易与线程池等高级 API 配合用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

@Slf4j
public class Test2 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.debug("running");
            }
        };

        Runnable runnable1 = ()->{log.debug("running");};
		//lambda表达式
        Thread thread = new Thread(runnable, "t1");
        Thread thread1 = new Thread(runnable1, "t2");
        thread.start();
        thread1.start();
        //DEBUG [t1] (Test2.java:18) - running
    }
}

3. 对比

方法1是把线程和任务结合在了一起,没有进行分离,这种方法弊端就是不利于分离,耦合度高。方法2中分离了任务和线程,这样做的好处就是可以让runnable接口配合线程池等高级Api来进行 *** 作。同时脱离了Thread体系,更加灵活。

4. FutureTask 配合 Thread

这种方式最大的特点就是Callable接口有返回值

@Slf4j
public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask也实现了runnable接口
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Integer call() throws Exception {
                log.debug("running");
                Thread.sleep(3000);
                return 100;
                //DEBUG [FutureTask线程] (Test3.java:23) - running
                //DEBUG [main] (Test3.java:32) - 结果是100
            }
        });
        //创建线程开始运行
        Thread t = new Thread(task, "FutureTask线程");
        //开始运行
        t.start();
        //获取结果,当主线程运行到get的时候就会一直等待线程返回才会往下执行,有点join内味了
        log.debug("结果是{}", task.get());
    }
}



2、线程运行-现象



3、查看进程和线程



4、原理 4.1 栈帧与栈

Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。每个线程都有自己的一个独立的栈帧,也维护着自己独立的栈帧。

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

public class Testframes {
    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;
    }
}

内存模型:
过程:每当我们调用到某一个方法的时候就会在当前线程中创建一个栈帧,里面存储了局部变量,返回地址,锁记录, *** 作数栈信息,然后会分别执行方法中的指令,创建变量等等,最后返回,返回地址就是在哪调用了。返回地址之后栈帧就会释放掉。

4.2 多线程和栈帧
public class Testframes {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                method1(20);
            }
        };
        thread.setName("t1");
        thread.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;
    }
}

每个线程都有自己的栈帧。



5、线程上下文切换 1. 原因

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

线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)垃圾回收有更高优先级的线程需要运行线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

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

状态包括程序计数器,虚拟机栈中每个栈帧的信息Context Switch频繁发生影响性能

2. java并发编程

1、在《java并发编程的艺术》中更详细提到了上下文切换。里面有谈到一些重要的点:

    即使是单核CPU也可以指向多线程的代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间。CPU通过分配时间片算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是在切换前会保留原来任务的状态,以便下一次恢复,这样一次保存到再加载的过程就是一次上下文切换。这种功能就类似于书签,当我们不想读一本书的时候就可以在书上面放一个书签,下次可以继续从书签的位置开始读。


2、问题:在了解上下文切换是什么之后,不难想象一个问题,多线程一定快吗?
在书中有一段代码,演示了亿级别的加减法运算(这里就不放出来了)。在结果中可以发现的是单并发执行累加 *** 作不超过百万次的时候,速度会比串行执行累加 *** 作要慢。这是因为有上下文切换带来的时间影响,在次数低的时候其实不一定必串行的要快。



3、测试上下文切换时长
书中也给出了上下文切换的示例,结果是上下文每1秒切换1000多次



4、如何减少上下文切换

无锁并发编程:使用一些方法来避免锁,比如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。CAS算法:不需要加锁使用最少线程:有些时候任务少,但是创建了很多线程来处理也是不好的。这就不得不提一下线程池的设计了。协程:在单线程里面实现多任务的调度,并在单线程里面维持多个任务间的切换。适当使用线程个数:
1、使用JDK 自带的工具 VisualVM 来查看线程等待时间和线程工作时间
2、使用公式:线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)



6、Thread常见方法




7、start 与 run
@Slf4j
public class TestRunAndStart {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running");
            }
        };
        //查看状态信息
        System.out.println(t1.getState());//NEW


        //run方法使用了主线程来执行,没有使用另外的线程
        t1.run();//DEBUG [main] (TestRunAndStart.java:18) - running
        t1.start();//DEBUG [t1] (TestRunAndStart.java:18) - running

        //查看状态信息
        System.out.println(t1.getState());//RUNNABLE
    }
}

多次调用start会报错

总结:

直接调用 run() 是在主线程中执行了 run(),没有启动新的线程使用 start() 是启动新的线程,通过新的线程间接执行 run()方法 中的代码






如有错误,欢迎指出

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5708158.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存