java学习——java并发(1)

本文详细解释了并行与并发的区别,介绍了并发的三大特性,进程与线程的概念及其区别,Java线程的创建方式,以及sleep(),wait(),join()等方法的使用。此外,还涵盖了线程调度、内存管理、线程通信和ThreadLocal的原理与应用。
摘要由CSDN通过智能技术生成

上一篇传送门:点我

java并发相关的一些问题整理,很多部分仅为个人理解,如有问题欢迎讨论指正(待补充)。

并行与并发有什么区别?

并行和并发都是指多个任务同时执行的概念,但是 它们之间有着明显的区别。
并行是指多个任务在同一时刻同时运行,通常会需要使用多个处理器或者多核处理器来实现;而并发指的是多个任务的执行是交替进行的,一个任务执行一段时间后,再去执行另一个任务,它是通过操作系统的协作调度来实现各个任务的切换,从而达到看上去同时进行的效果。
举个例子,假设现在有一个四核的CPU,并行处理就可以让这四个处理器核心同时处理四个不同的任务,每个处理器核心都在独立地工作。而并发处理则是让这四个任务在一个处理器核心上轮流执行,每个任务得到一点时间片来运行,然后就被切换到下一个任务,此时同一时刻只有一个任务在执行。

并发的三大特性

(1)原子性: 指在一个操作中CPU不可以在中途暂停,然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
(2)可见性:当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
(3)有序性:有序性是指程序执行的顺序按照代码的先后顺序执行,不会乱序执行。Java虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。

说说什么是进程与线程 / 说说进程与线程之间的区别

进程和线程是操作系统中概念,用于描述程序运行时的执行实体。
进程是一个程序在执行过程中的一个实例(程序的一次执行)可以并发地执行多个任务,每个进程都有自己独立的地址空间,也就是说它们不能直接共享内存,两个进程之间需要通过进程间通信(IPC)来交换数据
线程进程中的一个执行单元,一个进程里可以包含多个线程,同一个进程中的多个线程可以并发地执行多个任务,并且这些线程共享进程的内存空间
线程相比于进程,线程的创建和销毁开销较小上下文切换开销也较小,因此线程是实现多任务并发的一种更轻量级的方式。

Java线程的创建方式主要有哪几种?

Java中创建线程主要有三种方式。它们分别是继承Thread类、实现Runnable接口、实现Callable接口,但它们的底层实际上都是基于Runnable接口

继承Thread类:

public class MyThread extends Thread{
    public static void main(String[] args){
        MyThread thread = new MyThread();
        thread.start();
    }

    @Override
    public void run(){
        System.out.println("Hello MyThread!");
    }
}

总结: 继承Thread类重写的是run()方法,不是start()方法,但是占用了继承的名额,由于Java中的类是单继承的,这会导致该线程无法继承其他父类。

实现Runnable接口:

public class MyRunnable implements Runnable{
    public static void main(String[] args){
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }

    @Override
    public void run(){
        System.out.println("Hello MyRunnable!");
    }
}

总结: 实现Runnable接口,实现run()方法,使用依然要用到Thread,这种方式相比直接继承Thread类,还可以继承其他类,所以这种方法更常用。

实现Callable接口:

public class MyCallable implements Callable<String> {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }

    @Override
    public String call(){
        return"Hello MyCallable!";
    }
}

总结: 实现Callable接口需要实现call()方法,得使用Thread+FutureTask配合,这种方法可以拿到异步执行任务的结果。

Java线程是调用什么方法来启动,为什么?

Java线程是通过调用线程的start()方法来启动的。当创建了一个线程对象后,该线程并不会自动开始执行,而是处于一种新建状态。为了启动线程并执行线程run()方法中的代码,需要先调用该线程的start()方法,并且对于一个线程对象,start()方法只能被执行一次
调用start()方法实际上是在告诉JVM,我们希望启动一个新的线程来执行该线程对象中的run()方法。start()方法内部会进行一些必要的初始化工作,如分配系统资源,并准备将线程放入就绪状态。一旦线程获得CPU时间片,就会从就绪状态进入运行状态,开始执行run()方法中的代码。
需要注意: run方法仅仅是线程类中的一个普通方法而已。调用run()方法并不会创建一个新的线程,而只是在当前线程中执行了run()方法而已,这就像调用普通的方法一样。

sleep()和wait()方法对比

sleep()和wait()方法都可以暂停线程的执行,但是它们的目的不一样。sleep()是让当前线程暂停执行,是Thread类静态本地方法。wait()用于线程间的交互,通过调用wait()实现让获得对象锁的进程等待,并且释放当前进程占有的对象锁,线程需要与锁对象进行绑定,随后被锁对象唤醒,所以它是Object类本地方法。sleep()方法执行完成后,线程会自动苏醒。wait()方法调用后,线程不会自动苏醒,需要被唤醒。

Java线程有哪些常用的调度方法?

Java线程常用的调度方法一般有线程等待、线程通知、线程休眠、线程让步、线程中断五种。
线程等待: 当前线程调用Object类的wait() 方法后,会使线程进入等待状态,直到其他线程调用此线程对象的notify()notifyAll() 方法来唤醒它。这个方法通常用在多线程同步中,以确保线程按照特定的顺序访问共享资源。wait方法也可以有超时参数timeout,如果线程调用这个方法后,没有在指定的timeout时间内被唤醒,那么这个方法会因为超时返回

  • 在线程等待中还有一个**join()**方法,它能够让父线程等待子线程结束之后继续执行,即当我们调用某个线程的join()方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行

线程通知: 一个线程可以通过调用Object类的notify()notifyAll() 方法来唤醒在该对象上等待的其他线程。当调用的是notify()方法时,如果此时有多个线程在此对象上等待,则会随机唤醒一个线程,而notifyAll()则是唤醒此对象上等待的所有线程。
线程休眠: 通过调用Thread类下的sleep(long) 方法,使当前线程转到超时等待阻塞状态,long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程则会自动转为就绪状态。
线程让步: 通过调用Thread类下的yield() 方法,暂停当前正在执行的线程对象,把执行的机会让给相同或更高优先级的线程。
线程中断: 通过调用Thread类下的interrupt() 方法,将该线程的中断标志为设为true。中断的结果线程是死亡,还是等待新的任务或是继续运行下一步,就取决于程序本身。同时可以通过调用isInterrupted() 或是interrupted() 方法去判断某个线程是否已被发送过中断请求,其中isInterrupted() 方法不会清除中断标志位,而interrupted() 方法如果发现当前线程被中断,则会清除线程中断标记,并将中断标记设置为false

线程的生命周期?

在线程的生命周期中,通常有五种状态:创建、就绪、运行、阻塞和死亡
(1)创建状态(New):新创建了一个线程对象。
(2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
(3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
(5)死亡状态(Dead):线程执行完了或者因异常退出了run方法, 该线程结束生命周期。
阻塞的情况又分为三种:
(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是Object类的方法。
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

Java线程有多少种状态?

Java线程的状态分为初始状态、运行状态(可以分为就绪和运行中)、阻塞状态、等待状态、超时等待和终止状态六种状态。
初始状态 (NEW):线程被构建,但是还没有调用 start 方法;
运行状态 (RUNNABLE):该状态代表线程已经准备运行或者正在运行。一般在调用 start() 方法后,该线程就处于该状态 ;
阻塞状态 (BLOCKED):这种状态一般发生在多个线程进行工作时,假如有两个线程:线程1和线程2。当通过先启动线程1后启动线程2时候。此时的线程1处于运行状态,而线程2处于阻塞状态;
等待状态 (WAITING):线程在调用其它线程的join()方法、调用wait()方法等操作时,会使该线程会进入等待状态。进入这个状态后的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待状态 (TIMED_WAITING):超时等待状态用在于线程使用 sleep() 方法时,线程就处于超时等待状态。直到 sleep() 方法执行完毕,线程才会被唤醒。
终止状态 (TERMINATED):当线程执行完毕。这个线程就死亡了,此时就是终止状态。
在这里插入图片描述
图片来源:https://blog.csdn.net/qq_33996921/article/details/106357703

这里和线程生命周期的答案不一样,我理解是:要是问线程的生命周期,就回答 上一个问题的答案,问线程的状态就回答这一题的答案。

什么是线程上下文切换?

线程上下文切换指的是在多线程运行时,操作系统从当前正在执行的线程中保存它的上下文信息(包括当前线程的寄存器、程序指针、栈指针等状态信息),并将另一个等待执行的线程的上下文信息恢复到该线程中,从而实现线程直接的切换。

线程有哪些通信方式?

线程间通信是指在多线程编程中,各个线程之间共享信息或者协同完成某一任务的过程。常用的线程间通信方式一般有以下几种:
1.共享变量:共享变量是指多个线程都可以访问和修改的变量。它们通常是在主线程中创建的。多个线程对同一个共享变量进行读写操作的时候,可能会导致数据错误或程序异常。需要使用同步机制如synchronized、Lock等来保证线程安全;
2.管道通信:管道是一种基于文件描述符的通信机制,形成一个单向通信的数据流管道。它通常用于只有两个进程或线程之间的通信。其中一个进程将数据写入到管道(管道的输出端口),另一个进程从管道的输入端口读取数据;
3.信号量:信号量(semaphore)是一种计数器,用于控制多个线程对资源的访问。当一个线程需要访问资源时,它需要申请获取信号量,如果信号量的计数器大于0,则可以访问资源,否则该线程就会等待。当线程结束访问资源后,需要释放信号量,并将计数器加1;
4.条件变量: 条件变量是一种通知机制,用于在多个线程之间传递状态信息和控制信息。当某个线程需要等待某个条件变量发生改变时,它可以调用wait() 方法挂起,并且释放所占用的锁。当某个线程满足条件后,可以调用notify() 或者signal() 方法来通知等待该条件变量的线程继续执行。

ThreadLocal是什么?

ThreadLocal也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
在这里插入图片描述

ThreadLocal是如何实现的?

ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。
ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
在这里插入图片描述

ThreadLocal内存泄漏是怎么回事?

如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。(这是由于强引用没办法回收)
解决办法是在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法手动清除Entry对象。

ThreadLocalMap的结构

ThreadLocalMap虽然被称为Map,但是其实它是没有实现Map接口的,不过结构还是和HashMap类似,主要关注两个要素:元素数组散列方法
在这里插入图片描述
元素数组一个table数组,存储Entry类型的元素,Entry是以ThreadLocal的弱引用作为key,Object作为value的结构。

private Entry[] table;

散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当干取余)

补充:每创建一个ThreadLocal对象,key值就会新增0x61c88647,这个值是斐波那契数(也叫黄金分割数),可以使hash分布更加平均

ThreadLocalMap是如何解决hash冲突的

ThreadLocalMap内部使用开放地址法来解决Hash冲突的问题。具体来说,当Hash发生冲突时,ThreadLocalMap会将当前插入的元素从冲突位置开始依次往后遍历,直到找到一个空闲的位置。这样即便出现了Hash冲突,也不会影响已经插入的元素,而只是会新的插入操作。
查找的时候,会先根据ThreadLocal对象的hash值找到对应的位置,然后比较该位置Entry对象中的key是否和get方法中的key一致,不一致就依次往后查找。

谈谈ThreadLocal的扩容机制

ThreadLocalMap的扩容机制和HashMap类似,也是在元素数量到达阈值(默认阈值为数组总长度的2/3)时进行扩容。具体来说,在set()方法中,如果当前元素数量已经达到了阈值,就会调用rehash()方法,rehash()会先去清理过期的Entry,然后还要根据条件判断size >= threshold - threshold/4(判断容量是否大于最大容量的3/4)来决定是否需要扩容。
发现**需要扩容时调用resize()**方法,resize()方法首先会将数组长度翻倍,然后创建一个新的数组newTab,接着遍历旧数组oldTab中的所有元素,并利用散列方法重新计算位置,开发地地址法解决哈希冲突,然后放到新的newTab中,遍历完成之后,oldTab中所有的Entry数据都已经放入到newTab中了,然后tabke引用指针指向新数组newTab。

ThreadLocal是怎么实现父子线程通信的?

使用Thread类中的InheritableThreadLocal变量来进行值的传递,当父线程的InheritableThreadLocal不为空时,就会将这个值传到当前子线程的InheritableThreadLocal。

下一篇传送门:点我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值