《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多线程编程核心技术一》--- 快速认识线程的主要内容,如果未能解决你的问题,请参考以下文章

JAVA并发编程揭开篇章,并发编程基本认识,了解多线程意义和使用

java多线程系列

java多线程系列

java多线程系列

java多线程系列

java多线程编程核心技术怎么样