《剑指Java面试-Offer直通车》--Java多线程与并发

本文详细讲解了Java中的多线程与并发知识,涵盖了进程与线程的区别,线程的start()和run()方法的区别,Thread与Runnable的关系,线程返回值的处理,线程状态,synchronized和ReentrantLock的区别,Java内存模型JMM的内存可见性,volatile特性和ThreadLocal的使用,以及CAS和线程池的相关内容。文章深入浅出,适合准备Java面试和深入学习多线程的读者。
摘要由CSDN通过智能技术生成

目录

一、进程和线程的区别

进程和线程的由来

进程和线程的区别

Java进程和线程的关系

进程间通信方式

进程调度

死锁

二、线程的start和run方法的区别?

三、Thread和Runnable的关系?

四、如何实现处理线程的返回值?

如何给run()方法传参?

如何实现处理线程的返回值?

五、线程的状态

sleep和wait的区别

notify和notifyall的区别

yield函数

interrupt函数

线程的状态以及状态之间的转换

六、synchronized

底层实现原理

Java对象头

Monitor

锁优化

自旋锁

自适应自旋锁

锁消除

锁粗化

偏向锁

轻量级锁

锁的内存语义

偏向锁、轻量级锁、重量级锁汇总

synchronized和ReentrantLock的区别

七、JMM的内存可见性

Java内存模型JMM

JMM中的主内存(即堆空间)

JMM中的工作内存(即本地内存,或线程栈)

JMM与Java内存区域划分(即Java内存结构)是不同的概念层次

主内存与工作内存的数据存储类型以及操作方式归纳

JMM如何解决可见性问题

指令重排序

happens-before

happens-before的八大原则

happens-before的概念

八、volatile

volatile变量为何立即可见?

volatile变量如何禁止重排序优化?

volatile和synchronized的区别

九、ThreadLocal

十、CAS(乐观锁、悲观锁)

十一、线程池

Executors类创建不同的线程池

Fork/Join框架

工作窃取算法

为什么要使用线程池?

Executor的框架

ThreadPoolExecutor

ThreadPoolExecutor的构造器参数

任务拒绝策略

线程池的状态

工作线程的生命周期 

线程池的大小如何选定?

推荐资料


一、进程和线程的区别

进程:进程百度百科

线程:线程百度百科

计算机组成结构:计算机组成及层次结构

  • 进程和线程的由来

计算机的出现是为了解决复杂的数学计算问题。

1)最初计算机只能接收一些特定的指令,用户输入一个指令,计算机就做一个操作。当用户在思考或输入数据时,计算机就等待,效率低下。

2)为了提升计算机的执行效率,不用等待用户输入,把一系列需要执行的指令预先写下来,形成一个清单,一次性交给计算机。计算机不断读取指令进行相应的操作,批处理操作系统诞生。用户可以将需要执行的多个程序写在磁带上,交由计算机读取并逐个执行这些程序,并将输出结果写到另一个磁带上(这里的磁带相当于现在的磁盘)。

批处理操作系统也遇到问题。当有两个任务A和B,任务A在执行到一半时需要读取大量的数据输入,即所谓的IO操作。此时CPU只能等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。可以让任务A读取数据时任务B去执行,任务A读取完数据后任务B暂停让任务A继续执行。这样有一个问题,原来是一个程序在计算机里运行,内存只有一个程序的运行数据,想要任务A执行IO操作时,任务B抢占CPU执行,内存中要装入多个程序.如何处理?多个程序中的数据如何辨别?当一个程序运行暂停后,如何恢复到之前所执行的状态?进程应运而生。

3)进程对应一个程序,每个进程对应一定的内存地址空间,并且只能使用自己的内存空间。各个进程互不干扰,进程保存了程序每个时刻的运行状态,为进程切换提供了可能。进程暂停时会保存当前进程的状态,比如进程标识、进程使用的资源等。在下一次等到IO设备完成作业输出结果重新切换回来时,根据之前保存的状态恢复,然后继续执行,进程让操作系统的并发成为可能。

虽然并发从宏观上看有多个任务在执行,但是事实上,对于单核CPU的机器来说,任一个具体的时刻,只有一个任务在占用CPU资源。单核情况下让用户看起来像同一时刻并发执行多个任务是CPU分配给单一任务的时间片很短,任务切换的频次高,造成所有任务都并发执行的假象。

4)一个进程在一段时间内只做一件事情,如果一个进程有多个子任务,只能逐个的执行这些子任务,往往子任务之间不存在顺序上的依赖,可以并发执行。子任务共享进程的内存资源,属于同一个进程的子任务切换不需要切换页目录以使用新的地址空间,为子任务间更快速切换提供了可能,人们便发明了线程,让一个线程去执行一个子任务,一个进程就包含多个线程,每个线程负责独立的子任务,达到提升实时性的效果,线程让进程的内部并发成为可能。

  • 进程和线程的区别

进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程是资源分配的基本单位,所有与该进程相关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或者正在使用它们。


进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。进程拥有一个完整的虚拟内存地址空间,当进程发生调度的时候,不同的进程拥有不同的虚拟地址空间,而同一进程内不同线程共享同一地址空间;与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。

线程只由堆栈、寄存器、程序计数器和线程计数表TCB组成。

总结:

1)线程不能看做独立应用,而进程可看做独立应用。(操作系统并没有将多个线程看作多个独立的应用来实现进程的调度和管理以及资源分配)

2)进程有独立的地址空间,互不影响;线程只是一个进程中的不同执行路径。(一个进程奔溃后,在保护模式下,不会对其他进程产生影响;如果某个线程挂掉,它所在的进程也会挂掉)

3)线程有自己的堆栈和局部变量,但线程没有独立的地址空间,多进程的程序比多线程程序健壮。

4)进程的切换比线程的切换开销大。(如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程,每个独立的线程有个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存于某一个应用程序当中,由应用程序提供对多个线程的执行控制)

  • Java进程和线程的关系

1)Java对操作系统提供的功能进行封装,包括进程和线程。

2)运行一个Java程序会产生一个Java进程,每个Java进程包含至少一个线程。

3)每个进程对应一个JVM实例,每个JVM实例唯一对应一个堆,多个线程共享JVM里面的堆,每一个线程都有自己私有的栈。

4)Java采用单线程编程模型,程序会自动创建主线程。(自己的程序中如果没有主动创建线程的话,程序会自动创建一个线程,这个线程就是主线程。因此在编程的时候,将耗时的操作放入子线程中进行,以避免阻塞主线程,影响用户体验)

5)主线程可以创建子线程,原则上要后于子线程完成执行。(Java程序启动时,主线程立刻运行。主线程是产生其他子线程的线程,通常是最后完成执行,因为它需要执行各种关闭动作)

//打印当前线程的名字
public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
}

运行结果为main

Java采用单线程编程模型,虽然只有一个线程执行任务,JVM中并不是只有一个线程,JVM实例在创建的时候,同时会创建许多其他的线程(比如垃圾收集器线程),所以JVM是多线程的。

进程间通信方式

管道、系统IPC(包括消息队列、信号、共享存储)、套接字Socket

参考:进程间通信方式总结

进程调度

参考:操作系统中的进程调度策略有哪几种

死锁

参考:什么是死锁,产生死锁的原因及必要条件

 

参考:Java程序员校招面试——操作系统

 

二、线程的start和run方法的区别?

调用start()方法会创建一个新的子线程并启动;run()方法只是Thread的一个普通方法的调用(还是在主线程里执行)。

eg:

public class MyThread {
    public static void attack(){
        System.out.println("attack");
        System.out.println("current thread is:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t=new Thread(){
            public void run(){
                attack();
            }
        };
        System.out.println("current main thread is:"+Thread.currentThread().getName());
        t.run();
    }
}

运行结果为:

current main thread is:main
attack
current thread is:main

public class MyThread {
    public static void attack(){
        System.out.println("attack");
        System.out.println("current thread is:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t=new Thread(){
            public void run(){
                attack();
            }
        };
        System.out.println("current main thread is:"+Thread.currentThread().getName());
        t.start();
    }
}

运行结果:

current main thread is:main
attack
current thread is:Thread-0

调用run的时候会引用主线程来执行方法,start的时候会用非main的线程执行方法。

 

三、Thread和Runnable的关系?

Thread是一个类,Runnable是一个接口,Thread类实现了Runnable接口。

eg:

Thread实现多线程

public class ThreadDemo extends Thread{
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("Thread start "+this.name+",i"+i);
        }
    }
}
public class ThreadDemoTest {
    public static void main(String[] args) {
        ThreadDemo t1=new ThreadDemo("Thread1");
        ThreadDemo t2=new ThreadDemo("Thread2");
        ThreadDemo t3=new ThreadDemo("Thread3");
        t1.start();
        t2.start();
        t3.start();
    }
}

 运行结果:

Thread start Thread3,i0
Thread start Thread1,i0
Thread start Thread2,i0
Thread start Thread1,i1
Thread start Thread3,i1
Thread start Thread1,i2
Thread start Thread1,i3
Thread start Thread1,i4
Thread start Thread1,i5
Thread start Thread1,i6
Thread start Thread1,i7
Thread start Thread1,i8
Thread start Thread1,i9
Thread start Thread3,i2
Thread start Thread3,i3
Thread start Thread3,i4
Thread start Thread3,i5
Thread start Thread3,i6
Thread start Thread3,i7
Thread start Thread3,i8
Thread start Thread3,i9
Thread start Thread2,i1
Thread start Thread2,i2
Thread start Thread2,i3
Thread start Thread2,i4
Thread start Thread2,i5
Thread start Thread2,i6
Thread start Thread2,i7
Thread start Thread2,i8
Thread start Thread2,i9

Runnable实现多线程

public class RunnableDemo implements Runnable {
    private String name;
    public RunnableDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("Thread start"+this.name+",i"+i);
        }
    }
}
public class RunnableDemoTest {
    public static void main(String[] args) {
        RunnableDemo r1=new RunnableDemo("Runnable1");
        RunnableDemo r2=new RunnableDemo("Runnable2");
        RunnableDemo r3=new RunnableDemo("Runnable3");
        Thread t1=new Thread(r1);
        Thread t2=new Thread(r2);
        Thread t3=new Thread(r3);
        //Runnable没有start方法,先创建线程,把Runnable子类的实例传进去
        t1.start();
        t2.start();
        t3.start();
    }
}

 运行结果:

Thread start Runnable1,i0
Thread start Runnable3,i0
Thread start Runnable2,i0
Thread start Runnable2,i1
Thread start Runnable2,i2
Thread start Runnable2,i3
Thread start Runnable2,i4
Thread start Runnable2,i5
Thread start Runnable2,i6
Thread start Runnable2,i7
Thread start Runnable2,i8
Thread start Runnable2,i9
Thread start Runnable3,i1
Thread start Runnable3,i2
Thread start Runnable3,i3
Thread start Runnable3,i4
Thread start Runnable3,i5
Thread start Runnable3,i6
Thread start Runnable3,i7
Thread start Runnable3,i8
Thread start Runnable3,i9
Thread start Runnable1,i1
Thread start Runnable1,i2
Thread start Runnable1,i3
Thread start Runnable1,i4
Thread start Runnable1,i5
Thread start Runnable1,i6
Thread start Runnable1,i7
Thread start Runnable1,i8
Thread start Runnable1,i9

Thread是实现了Runnable接口的类,通过start给Runnable的run方法赋值上多线程特性。因为Java类的单一继承原则,为了提升系统可扩展性,推荐通过使业务类实现Runnable接口将业务逻辑封装在run方法里。

参考:Java创建线程的方式Runnable接口和Callable接口的区别

 

四、如何实现处理线程的返回值?

  • 如何给run()方法传参?

和线程相关的业务逻辑需要放入到run()方法里面,但是run方法是没有参数的,并且也没有返回值的。给run()方法传参实现方式主要有三种:

1)构造函数传参

2)成员变量传参,通过set方法进行传参

3)回调函数传参

  • 如何实现处理线程的返回值?

有的程序的执行是依赖于子任务的返回值进行的,当子任务交给子线程去完成的时候,是需要获取到它们的返回值。获取子线程返回值的方式主要有三种:

1)主线程等待法,即让主线程循环等待,直到目标子线程返回值为止。主线程等待实现简单,缺点是需要自己实现循环等待的逻辑,但是如果等待的变量一多,代码就会显得异常的臃肿;而且需要循环多久是不确定的,无法做到精准的控制。

public class CycleWait implements Runnable{
    private String value;
    public void run(){
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value="we have data now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw=new CycleWait();
        Thread t=new Thread(cw);
        t.start();
        while(cw.value==null){
            Thread.currentThread().sleep(100);
        }
        System.out.println("value:"+cw.value);
    }
}

运行结果:value:we have data now (主线程没有休眠则运行结果为value:null)

2)使用Thread类的join()阻塞当前线程以等待子线程处理完毕。join方法可以阻塞调用此方法的线程即这里可以阻塞主线程,直到join方法所在的线程执行完毕为止。此方法比主线程等待法做到更精准的控制,实现起来简单,缺点是粒度不够细。

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw=new CycleWait();
        Thread t=new Thread(cw);
        t.start();
//        while(cw.value==null){
//            Thread.currentThread().sleep(100);
//        }
        t.join();
        System.out.println("value:"+cw.value);
    }

3)通过Callable接口实现,通过FutureTask或者线程池获取。JDK5之后新增了Callable接口,执行了Callable任务之后,可以获取一个Future的对象,在该对象上调用get方法就可以获取到Callable任务返回的对象。关于通过Callable接口实现的方式获取线程的返回值有两种方式来实现,第一种是通过FutureTask类获取,第二种是通过线程池获取。

Future Task

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception{
        String value="test";
        System.out.println("Ready to work");
        Thread.currentThread().sleep(5000);
        System.out.println("task done.");
        return value;
    }
}
public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //重点关注构造函数、isDone()、get()、有参get()方法
        FutureTask<String> task=new FutureTask<String>(new MyCallable());
        //FutureTask实现RunnableFuture接口,RunnableFuture接口继承Runnable接口
        new Thread(task).start();
        if(!task.isDone()){
            System.out.println("task has not finished,please wait!");
        }
        System.out.println("task return:"+task.get());
    }
}

运行结果:

task has not finished,please wait!
Ready to work

(间隔5秒)
task done.
task return:test

线程池

MyCallable类同上

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool= Executors.newCachedThreadPool();
        Future<String> future=newCachedThreadPool.submit(new MyCallable());
        if(!future.isDone()){
            System.out.println("task has not finished,please wait!");
        }
        try {
            System.out.println("task return:"+future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            newCachedThreadPool.shutdown();
        }
    }
}

运行结果:

task has not finished,please wait!
Ready to work

(间隔5秒)
task done.
task return:test

 

五、线程的状态

Thread源码枚举类型State中,包括六种状态New、Runnable、Waiting、Timed Waiting、 Blocked、Terminated。

1)新建New状态,创建后尚未启动的线程的状态,即新创建了一个线程,但是还没有调用start方法。

2)运行Runnable状态,包括操作系统线程状态中的Running和Ready。处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。比如线程对象创建后,调用了该对象的start方法之后,这个时候线程处于Runnable状态。由于该状态分为两个子状态Running和Ready,处于Running的线程位于可运行线程之中,等待被线程调度选中,获取CPU的使用权;处于

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值