前言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()); } }
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; } }
内存模型:
过程:每当我们调用到某一个方法的时候就会在当前线程中创建一个栈帧,里面存储了局部变量,返回地址,锁记录, *** 作数栈信息,然后会分别执行方法中的指令,创建变量等等,最后返回,返回地址就是在哪调用了。返回地址之后栈帧就会释放掉。
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; } }
每个线程都有自己的栈帧。
原因:因为以下一些原因导致 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+平均等待时间/平均工作时间)
@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条)