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


前言

这一系列基于黑马的视频: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并发编程的艺术》中更详细提到了上下文切换。里面有谈到一些重要的点:

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


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()方法 中的代码






如有错误,欢迎指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值