《Java多线程编程核心技术一》--- 快速认识线程
Posted 小样5411
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java多线程编程核心技术一》--- 快速认识线程相关的知识,希望对你有一定的参考价值。
前言
最近读完了《深入理解Java虚拟机》大部分理论章节,感觉对JVM内部执行豁然开朗,并且发现并发编程和虚拟机工作也密不可分,强推先读一读JVM,或者读我归纳的几篇JVM文章,现在再系统读一读多线程、并发这块的书籍,以前也学过多线程,不过没有系统看书,图书馆选了一本看目录还不错的《Java高并发编程详解:多线程与架构设计》汪文君 著。网上都推荐《实战Java高并发程序设计》葛一鸣著,我也找到了对应pdf版本,先看第一本,如果觉得不全,再看第二本,配合一起看,不过大多内容都大同小异,不多说,一起啃书吧!!!
后续补充:还是看《实战Java高并发程序设计》,第一本后面讲的确实不太好
一、线程的生命周期(重点)
这个图非常重要,面试也常考,记住五大状态转换过程:
NEW(创建状态)
RUNNABLE(可执行或就绪状态)
RUNNING(运行状态)
BLOCKED(阻塞状态)
TERMINATED(终止状态)
面试:一个线程创建到消亡的过程(考你对线程生命周期的理解)
首先,一个线程对象被new后,就创建出一个线程对象,注意此时线程并没有运行或者启动,只是一个Thread对象而已,处于创建状态(NEW),我们平时称的线程准确来说都是在运行态或可执行态的,然后通过调用start方法,线程对象才变为可执行状态(RUNNABLE)的就绪线程,它在等待CPU调度,没有CPU调度它是不会运行的,等到CPU轮转调度时,线程分到CPU的时间片,然后就会真正运行,也就是运行态(RUNNING),要知道CPU是按照时间片轮转的方式调用线程的,这就涉及一些操作系统的知识,每个线程会分到CPU一定长度的时间片,时间片用完,就要让出CPU给其他线程执行,又切换变为RUNNABLE状态,等到再轮转到之前没执行完的线程,然后线程才会继续执行,这里说的也就是图中schedule swap。在RUNNING态的线程可以直接进入TERMINATED状态,生命周期就结束了,比如调用stop()方法,但官方不推荐使用。另外,运行态调用sleep()方法和wait()方法可以使得进入BLOCKED状态,也可能因为IO读写操作进入BLOCKED状态,又或者因为获取不到锁资源,而加入锁的阻塞队列而进入BLOCKED状态。程序中还可以调用其他方法进行唤醒BLOCKED状态的线程,如常见的notify()和notifyAll()方法。下面图捋一下整个创建到消亡过程,主要就是要说明5个状态之间的转换(创建、就绪、运行、阻塞、终止)。
注:其中暂停线程的suspend()和恢复线程的resume()已经弃用,所以不讲。yield()方法是放弃当前CPU资源,但放弃后,一定几率马上又获得CPU时间片,可以理解为线程A让出CPU,然后和其他线程一起等CPU调度。
二、实现多线程的两种方式
下面根据一个案例来讲这两种实现,并进行对比,当然创建多线程不止这两种,还可以用Callable或者线程池,但下面两种是最常见的,其中用Runnable接口最常用。
案例:银行排队叫号窗口4个,用户会被叫去每个窗口办理业务,假设最多一天受理50笔业务,我们来写这个程序
2.1 继承Thread类并重写run方法
public class Main {
public static void main(String[] args) {
//线程1(窗口1)
new ServiceWindow("一号窗口").start();
//线程2(窗口2)
new ServiceWindow("二号窗口").start();
//线程3(窗口3)
new ServiceWindow("三号窗口").start();
//线程4(窗口4)
new ServiceWindow("四号窗口").start();
}
}
//继承Thread
public class ServiceWindow extends Thread{
private String name;//柜台名称
private int count = 1;//叫号
private static final int MAX = 50;//最多50笔业务
ServiceWindow(){
}
ServiceWindow(String name){
this.name = name;
}
//重写run方法,底层启动线程是调用底层的run方法执行的
@Override
public void run(){
while (count < MAX){
System.out.println(name + " -> 当前叫号:" + (count++));
}
}
}
效果
为什么会出现这种重复叫同一个号的情况?因为我们创建了四个叫号线程,count属于成员变量,成员变量是在堆中分配内存,并且多线程共享堆内存,也就是多线程共同访问一个对象中的实例变量,这就出现多线程并发访问的不安全问题,有可能存在覆盖情况,并且count++自增也不是原子操作,然后导致输出的值重复,出现“脏读”,可以看下我的这篇关于JVM内存区域的文章,原子操作可见该文章。
改进1:加static修饰
count加了staic修饰,那么就属于静态变量,静态变量和常量是会加载到方法区的,而方法区是线程共享的,所以count就被四个线程共享,就不会重复叫号,如下,没有重复的
简单补充上面JVM相关知识,JVM内存区域(运行时数据区)分5大块,堆、方法区、虚拟机栈、本地方法栈、程序计数器,其中方法区和堆是线程共享,虚拟机栈、本地方法栈、程序计数器是线程私有的。
但是static虽然能解决,做到了共享资源,不过static修饰的变量生命周期很长,如果有很多这种变量都用static修饰,那么方法区本来就不大,很容易满。所以我们需要再改进
改进2:用Runnable接口实现共享资源
2.2 实现Runnable接口并重写run方法
public class Main {
public static void main(String[] args) {
ServiceWindowRunnable task = new ServiceWindowRunnable();
new Thread(task,"一号窗口").start();//这里给线程实现接口和取名字,Thread.currentThread().getName()可以获取名字
new Thread(task,"二号窗口").start();
new Thread(task,"三号窗口").start();
new Thread(task,"四号窗口").start();
}
}
public class ServiceWindowRunnable implements Runnable{
private int count = 1;//叫号,不用static修饰
private static final int MAX = 50;//最多50笔业务
@Override
public void run() {
while (count < MAX){
System.out.println(Thread.currentThread().getName() + " -> 当前叫号:" + (count++));
}
}
}
四个线程共享Runnable接口的资源,这样就既能不重复叫,也不会像static修饰一样生命周期太长而让方法区满, 实现Runnable接口的ServiceWindowRunnable类会创建在堆内存,堆内存是最大的,并且有垃圾回收机制,所以不用担心满的问题。故以后需要共享都用Runnable实现,这也是相比用继承Thread,Runnable最常用的原因,能用Runnable就不用Thread。
现在实现线程用lambda表达式,简单、快捷,给出一个简单案例给大家参考理解
public class Main{
public static void main(String[] args) {
//重写run方法,lanbda表达式写法()->{...}
//@Overide public void run(){...}英文字全省略,只留()->{},一个表示带的参数,一个表示方法体
new Thread(()->{
for (int i = 0 ; i < 10 ; i++){
System.out.print(i+" ");
}
},"一号窗口").start();
}
}
补充另外两种线程创建方式
1、FutureTask + Callable
public class test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new Callable() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(1);
System.out.println("call方法执行了");
return "call方法返回值";
}
});
futureTask.run();
System.out.println("获取返回值: " + futureTask.get());//get方法用于获取任务完成的返回值
FutureTask futureTask1 = new FutureTask(new Callable() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(1);
System.out.println("call方法执行了1");
return "call方法返回值1";
}
});
futureTask1.run();
System.out.println("获取返回值1: " + futureTask1.get());
}
}
这种方式相比Runnable来说,有两个不同:1、有返回值 2、会抛出异常
同时,futureTask先执行,那么其他的就会阻塞,如此处futureTask1就会阻塞,不管futureTask 中设置sleep多久,futureTask1都要等它执行完才会执行,大家可以自己运行看看。另外get方法可以获取到返回值
2、线程池中的Executors工具类
可以参考我这篇文章:线程池详解
以上是关于《Java多线程编程核心技术一》--- 快速认识线程的主要内容,如果未能解决你的问题,请参考以下文章