【Java面试】并发
创始人
2024-03-24 11:39:12
0

文章目录

  • 线程有那些状态?
  • 一个程序来显示状态切换过程
    • 正常执行流程
    • 阻塞执行流程
    • 等待执行流程
  • 说说线程池的核心参数
  • wait和sleep的区别
  • Lock和synchronized的区别
  • Lock中Condition的使用
  • 说说Java中的悲观锁与乐观锁
    • 乐观锁
    • 悲观锁
  • Hashtable和ConcurrentHashMap的区别?
  • 【Java面试】谈一谈你对ThreadLocal的理解

线程有那些状态?

操作系统层面将线程状态分为5种,分别是:新建,就绪,运行,阻塞,死亡
但是在Java的Thread类中,线程的状态枚举如下:
在这里插入图片描述
也就是Java将线程分为了六种状态:创建,可运行,阻塞,等待,限时等待和终结
在这里插入图片描述
新建状态指的是我们使用new方法新建出来一个线程对象的时候,此时Java还没有将其与OS关联起来,那么这个线程就不会被分配CPU去执行代码,只有调用了start方法之后才会与真正的线程关联起来,此时就从新建状态变为了可运行(就绪)状态,等待分配CPU去执行任务,如果CPU分配给了当前线程,当前线程就会执行对应的任务,任务执行完毕之后,当前线程死亡(终结),注意是死亡,此时这个任务对应的线程以及资源都会被释放。
而并不是所有的任务都会被执行,因此会有部分任务进行阻塞,例如多线程访问同一个锁资源的时候,如果当前线程没有得到锁,那么当前线程就需要进行阻塞,此时CPU被分配给得到锁的线程去执行任务,任务执行完毕之后,锁释放,线程再次去争夺锁,得到锁的话就可以从阻塞状态变为可运行状态,此时只需要等待CPU分配资源就可以运行了。
而当前持锁线程可能执行任务后发现有些条件不满足,那么这个线程不能总是占有锁,因此可以选择调用wait方法去释放锁,然后让自己进入等待状态去等待条件的满足,此时其他线程就可以去竞争锁从而执行他们的任务,等到条件满足,就可以由另一个线程调用notify方法去提醒这个线程再次竞争锁去完成任务。
最后一种是等待的一种情况,叫做现时等待,也就是这次等待可以设定时间。
方法是使用wait方法并且设定等待的时间,再次之前如果时间到了或者另一个线程调用了notify方法,那么这个线程就会被唤醒。
还有一种是sleep方法,这种方法和wait不一样,sleep是不会释放当前线程的锁的,其他线程都得陪着这个线程等,这个线程等待完毕之后在继续执行接下来的任务,调用了sleep方法相对于是告诉CPU你现在先不用执行我的任务了,你可以先去干其他的,相当于让出CPU资源。

一个程序来显示状态切换过程

正常执行流程

首先来两个断点,一个断点打在主线程上,另一个断点打在分支上。
并且设定触发断点的条件为Thread,这样子我们进入debug状态之后就可以以线程作为操控条件了。
在这里插入图片描述
在这里插入图片描述
主线程断点设置在了最后一句,所以此时前面的语句都已经执行完毕了,但是由于还有一个断点打在了分支线程里面,所以如果我不让他执行,他就得等待
在这里插入图片描述
现在切换到分支线程执行语句
在这里插入图片描述
在这里插入图片描述
此时分支线程的任务已经执行完毕了,那么只有主线程还有任务了。
让主线程执行断点处的语句
在这里插入图片描述
可以发现在没有阻塞的情况下,线程状态为NEW->RUNNABLE->TERMINATED
这几个状态都是Thread类中定义的枚举类型。

阻塞执行流程

要让线程阻塞,就让他获取锁失败就行了
在这里插入图片描述
首先先创建线程,然后开启线程,但是不让线程执行任务
在这里插入图片描述
开启任务之后,先让分支线程执行一下,然后让他触碰到获取锁的位置但是先不执行,然后让主线程去执行获取锁的方法,然后再切换到分支线程去获取锁,此时分支线程获取锁失败,那么再让主线程打印分支线程的状态,此时的状态就是阻塞了。
之后主线程释放锁,分支线程继续执行任务,分支线程变为RUNNABLE状态,然后执行完成任务后死亡。
在这里插入图片描述

等待执行流程

大致流程也是差不多的,先让分支线程获取到锁,然后此时分支线程调用wait方法把锁让出去,那么主线程就能得到锁,然后查看此时分支线程的状态,主线程把分支线程唤醒之后,分支线程的状态从WAITTING变为了BLOCKING,因为此时分支线程要继续执行任务的话就需要获得锁。
得到锁之后继续重新执行任务,此时主线程发现分支线程的状态就是RUNNABLE了,然后分支线程执行完毕任务,死亡。
在这里插入图片描述

说说线程池的核心参数

线程池的执行流程也就是主线程(业务线程)提交任务到线程池之后,任务的处理流程。
下面来看execute方法
在这里插入图片描述
这个方法首先判断要执行的任务是否是空,如果是,返回一个空指针异常。否则,判断当前的工作线程数,如果工作线程数小于核心线程数,那么直接创建一个工作线程执行任务,如果不是,那么判断当前工作队列是否不满,如果是,那么将这个任务放入到工作队列中,如果不是,判断当前是否还有可用的非核心线程(判断maximumPoolSize-corePoolSize>0)即工作线程数是否大于最大线程数,如果不是,那么创建一个非核心线程来执行当前任务,如果是,那么就执行拒绝策略。流程图如下:
在这里插入图片描述

wait和sleep的区别

  • 共同点:
    wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
  • 方法归属不同
    sleep(long)是 Thread的静态方法
    而wait(),wait(long)都是Object的成员方法,每个对象都有,因此每一个对象都可以作为锁来调用这个方法
  • 醒来时机不同
    执行 sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
    wait(long)和 wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
    它们都可以被打断唤醒
  • 锁特性不同
    wait方法的调用必须先获取wait对象的锁,而sleep 则无此限制
    wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃,但你们还可以用),而sleep如果在 synchronized代码块中执行,并不会释放对象锁(我放弃,你们也用不了)

Lock和synchronized的区别

  • 语法层面
    synchronized是关键字,源码在jvm 中,用c++语言实现
    Lock是接口,源码由jdk 提供,用java语言实现
    使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  • 功能层面
    二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock
  • 性能层面
    在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
    在竞争激烈时,Lock的实现通常会提供更好的性能

Lock中Condition的使用

说说Java中的悲观锁与乐观锁

1.悲观锁的代表是synchronized和Lock锁

  • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
  • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
  • 实际上,线程在获取synchronized和Lock锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会

2.乐观锁的代表是Atomiclnteger,使用cas来保证原子性

  • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
  • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
  • 它需要多核cpu支持,且线程数不应超过cpu核数

乐观锁

乐观锁的典型操作就是CAS(compare and set)
这里按照乐观锁的要求,不加锁,每次失败的时候都重试,直到成功
当然,如果想要进行原子操作,需要保证共享变量的可见性,所以一般需要要求变量是volatile类型的。

public class SyncVsCas {static final Unsafe U = Unsafe.getUnsafe();//得到偏移位置static final long BALANCE = U.objectFieldOffset(Account.class,"balance");static class Account{volatile int balance=10;}public static void main(String[] args) {Account account = new Account();while (true) {int o = account.balance;int n = o+5;//第一个参数为:要修改的变量是那个对象的 第二个是偏移量//第三个参数为旧值  第四个参数为新值//这个方法会把o值和我们共享变量的值进行比对 如果一样那么就修改if(U.compareAndSetInt(account, BALANCE, o, n)){//原子性的break;}}System.out.println(account.balance);}
}

悲观锁

悲观锁就是很典型的使用synchronized

  public static void sync(Account account){Thread t1 = new Thread(()->{synchronized (account){int o = account.balance;int n = o-5;account.balance=n;}},"t1");Thread t2 = new Thread(()->{synchronized (account){int o = account.balance;int n = o+5;account.balance=n;}},"t2");t1.start();t2.start();System.out.println(account.balance);}

Hashtable和ConcurrentHashMap的区别?

【Java面试】谈一谈你对ThreadLocal的理解

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...