当前位置: 首页 > news >正文

Java面试-并发面试(一)

CountDownLatch和Semaphore的区别和底层原理

  • CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch 的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch的数字减一 ,当数字被减到0后,所有的await线程都将被唤醒,对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒
  • Semahore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用信号量,通过acquire()方法来获取许可,如果没有许可可用则线程组,并通过AQS来排队,可以通过release()方法来释放许可 ,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程依次唤醒,知道没有空闲许可。

ReentrantLock中的tryLock()和lock()方法的区别

  • tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁返回true,如果没有加到则返回false
  • lock()表示阻塞加锁,线程会阻塞知道加到锁,方法也没有返回值。

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,他们的底层实现都会使用AQS来进行排队,他们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程排队,如果有线程排队,则当前线程也进行排队,如果是非公平锁,则不会检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一直没竞争到锁,都会进行排队,当锁释放时,都会唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段

另外,ReentrantLock是可重入锁, 不管是公平锁还是非公平锁都是可重入的。

ReentrantLock默认情况下使用的是非公平锁,性能上相对来说比较高一点儿。

sleep()、wait()、join()、yield()的区别

  1. 锁池
    所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,其他线程需要在这个锁池进行等待,当前面的线程是否同步锁后锁池中的线程去同步竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配
  2. 等待池
    当我们调用wait()方法后,线程会放到等待等待池当中,等待池的线程是不会去竞争同步锁,只有调用了notify()或者notifyAll()后等待池的线程才会开始去竞争所,notify()是随机从等待池中选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。
  • sleep 是Thread类的静态本地方法,wait则是Object类中的本地方法
  • sleep方法不会释放Lock,但是wait方法会释放,而且会加入到等待队列中

sleep就是把cpu的执行资源和执行权释放出去,不在运行此线程,当定时时间结束再取回cpu资源,参数cpi调度,获取到cpu资源后就可以继续运行了,而如果sleep时该线程有锁,那么sleep不会是否放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁,也就是说无法执行程序,如果再睡眠期间其他咸亨调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的

  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  • sleep 不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)
  • sleep一般用于当前线程休眠,或者轮询暂操作,wait则多用于多线程之间的通信
  • sleep会让出CPU执行时间且强制上线文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行

yield()执行后线程直接进入到就绪状态,马上释放了CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进行调度还是会让这个线程取到执行权继续执行

join()执行后线程会进入阻塞状态,例如在线程B中调用线程A的join(),那B线程会进入到阻塞队列,知道线程A结束或中断线程

public static void main(String[] args) throws InterruptedExceotuin{Thread t1 = new Thread(new Runnable(){@Overridepublic void run(){try{Thread.sleep(3000);}catch(InterruptedException e){e.printStackTrace();}System.out.println("22222222");}});t1.start();t1.join();System.out.println("1111");
}
//22222222
//1111

Synchronized的偏向锁,轻量级锁,重量级锁

  1. 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果由第二个线程来竞争,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量解锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  3. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
  4. 自旋锁:自旋锁就是线程在获取所得过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Synchronized 和ReentrantLock区别

  1. synchronized是一个关键字,Reentrantlock是一个类
  2. synchronized会自动枷锁和释放锁,Reentrantlock需要程序员手动加锁与释放锁
  3. synchronized的底层是JVM层面的锁,Reentrantlock是API 层面的锁,
  4. synchronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  5. synchronized锁的是对象,锁信息保存到对象头中,ReenttrantLock通过代码中的int类型的state标识来标识锁的状态
  6. synchronized底层有一个锁升级的过程

ThreadLocal的底层原理

  • ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法中获取缓存的数据
  • ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个TheadLocalMap,Map的Key为ThreadLocal对象,Map的value为需要缓存的值
  • 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完只有,应该要把设置的Key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象。线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,收到清除Entry对象
  • ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

ThrealLocal 内存泄漏原因,如何避免?

什么是内存泄漏

内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早都会被占光

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏

强引用

强引用:使用最普遍的引用(new)一个对象具有强引用,不会被垃圾回收机器回收,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收这种对象

如果想取消强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样可以使用JVM在合适的时间就会回收该对象

弱引用

弱引用:JVM进行垃圾回首时,无论内存是否充足 ,都会被弱引用关联的对象,在Java中用Java.lang.ref.WeakReference类来表示,可以在缓存中使用弱引用。

ThreadLocal的实现原理,每个Threa维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value 为线程变量的副本

在这里插入图片描述

ThreadLocalMap使用ThreadLocal的弱引用作为Key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样会导致ThreadLocalMap中的Key为null,而value还存在着强引用,只有thread线程推出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在,一条强引用链(红色链条)

key使用强引用

当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocald的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏

Key的弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收,当Key为Null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

因此,ThreadLocal内存泄漏的根源是:由于TheadLocalMap的生命周期跟Thead一样长,如果没有手动删除对应的Key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法:

  • 每次使用完ThreadLocal都调用它的remove() 方法清除数据
  • 将ThreadLocal变量定义成private static ,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,进而清除掉。

Thread、Runable的区别

Thread和Runable的实质是继承关系,没有可比性。无论使用Runable还是Thread,都会new Thread,然后执行run方法,用法上,如果有复杂的线程操作请求,那么就选择继承Thread,如果只是简单的执行一个任务,那就实现Runable。

public class Test{public static void main(String[] args){new MyThread().start();new MyThread().start();}static class MyThread extends Thread{private int ticket = 5;public void run(){while(true){System.out.println("Thread ticket = "+ticket--);if (ticket < 0){break;}}}}
}
public class Test2{public static void main(String[] args){MyThread2 mt = new MyThread2();new Thread(mt).start();new Thread(mt).start();}static class MyThread2 implements Runable{private int ticket = 5;public void run(){while(true){System.out.println("Thread ticket = "+ticket--);if (ticket < 0){break;}}}}
}

原因是:MyThread创建了两个实例,自然会卖出两倍,属于用法错误。

阿里一面:如何查看线程死锁

  • 可以用过jstack命令来查看,jstack命令中会显示发生了死锁的线程
  • 或者两个线程去操作数据库时,数据库发生了死锁,这样可以查询数据库的死锁情况
-- 查询是否有死锁
show OPEN TABLEs where In_use > 0;
-- 查询进程
show processlist;
-- 查看正在锁的事务
select * fromn INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA_INNODB_LOCK_WAITS;

阿里一面:线程之间是如何进行通讯的

  • 线程之间可以通过共享内存或基于网络来进行通信
  • 如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
  • 就像Java中的wait(),notify()就是阻塞和唤醒
  • 通过网络就比较简单了,通过网络将通信数据发送给对象,当然也要考虑到并发问题,处理方式就是加锁等方式

并发、并行、串行的区别

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等待着
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行
  • 并发允许两个任务彼此干扰,统一时间点,只有一个任务运行,交替执行

Java如何开启线程,怎么保证线程安全?

线程和进程的区别:进程是操作系统进行资源分配的 最小单元,线程是操作系统任务分配的最小资源,线程隶属于进程

如何开启线程?

  1. 继承Thread 类,重写run 方法
  2. 实现Runable 接口,实现run 方法
  3. 实现Callable 接口,显示call方法,通过FuterTask创建一个线程,获取到线程执行的返回值,
  4. 通过线程池来开启线程

怎么保证线程安全?核心的思想就是加锁

  1. 使用JVM 提供的锁,也就是Synchronized关键字,
  2. 使用JDK 提供的各种锁LOCK。

Volatile和Synchronized的区别以及DCL(Double Check )单例为何要加Volatile

Volatile和Synchronized的区别:

  • Synchronized关键字用来加锁,Volatile 只是保持变量的线程可见性,通常只适用于一个线程写,多个线程读的场景
  • valatile 关键字只能保证线程可见性,不能保证原子性,不能保证线程的安全性
  • volatile防止指令重排,在DCL中防止高并发情况下,指令重排造成线程安全问题
public class SingleDemo1{
//    public static SingleDemo1  singleDemo1 = new SingleDemo1();public static SingleDemo1  singleDemo1;private SingleDemo1(){}//public static SingleDemo1 getInstance(){//   if (singleDemo1 == null){//     singleDemo1 = new SingleDemo1();//}// return singleDemo1;//}// public static synchronized SingleDemo1 getInstance(){//   if (singleDemo1 == null){//     singleDemo1 = new SingleDemo1();//}//return singleDemo1;
//    }
//    public static  SingleDemo1 getInstance(){//        if (singleDemo1 == null){
//				synchronized(SingleDemo1){//                   singleDemo1 = new SingleDemo1();//              }//     }//    return singleDemo1;//}public static  SingleDemo1 getInstance(){if (singleDemo1 == null){synchronized(SingleDemo1){if(singleDemo1 == null){singleDemo1 = new SingleDemo1();}}}return singleDemo1;}
}

JAVA 线程锁机制是怎么样?偏向锁、轻量级锁、重量级锁有什么区别,锁机制是如何升级的?

  1. Java的锁就是在对象中的MarkWord中记录一个锁状态,偏向锁 、无锁、轻量级锁,重量级锁对应不同的锁状态
  2. Java的锁机制就是根据资源竞争的不同程度不断升级的过程

在这里插入图片描述

谈谈你对AQS的理解 ,AQS 如何实现可重入锁

AQS 是一个JAVA 线程同步的框架,是JDK中很多锁工具的核心实现框架。

在AQS 中维护了一个信号量state和一个线程组成的双向链表队列,其中,这个线程队列,就是用来给线程排队的,而state就像一个红绿灯,用来控制线程排队或者放行,在不同的场景下,有不同的意义。

在可重入锁这个场景下,state就用来表示加锁的次数,0 标识无锁,每次加锁,state 就加1,释放state就减一

如何保证多个线程之间的执行顺序

DountDownLatch,CylicBarrier,Senaphore.

如何对一个字符串快速排序

Fork/Join 框架

并发的三大特性

  • 原子性、可见性、有序性
原子性:

原子性是指在一个操作中CPU不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好不转账,从账户A 向账户B转1000元,那么必然包括两个操作,从账户A减去1000元,往账户B加上1000元。两个操作必须完成

private long count = 0;
public void calc(){count++;
}
  1. 将count 从驻村督导工作内存中的副本中
  2. +1的运算
  3. 酒酿结果写入工作内存
  4. 将工作内存的值刷回贮存(什么时候刷入由操作系统决定,不确定的)

那程序中原子性指的是最小操作单元,比如自增操作,它本身其实并不是原子性操作,分了三步的,包括读取变量的原始值,进行加1操作,写入工作内存。所以再多线程中,有可能一个线程还没有自增完,可能才执行到第二步,另一个线程就已经读取了值,导致结果错误,那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

关键字:Synchronized

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量,其他线程能够立即看到修改的值。

若两个线程在不同的CPU,那么线程1改变了i的值还没有刷新到主存,线程2又使用了i,那么这个i的值还是之前的,线程1对变量的修改线程2没有看到这就是可见性的问题

boolean stop  = false;
while(true){doSomeThing();
}
// 线程2
stop = true;

如果线程2改变了stop的值,线程1一定会停止吗?不一定,当线程2更改了stop变量的值,但是还没有来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2 对stop的更改,因此还会一直循环下去。

关键字:volatile,synchronized,final

有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将它们重排序,实际上,对于有些代码进行重排之后,虽然对变量的值没有造成影响,但是有可能会出现线程安全问题。

int a = 0;
boolean flag = false;public void write(){a = 2;flag =false;
}
public void multiply(){if (flag){int ret = a*a;}
}

write方法里的1和2 做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显延迟了一步。

关键字:volatile,synchronized

volatile 本身就包含了禁止指令重排的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其及逆行lock操作”这条规则明确。

说说你对线程安全的理解

不是线程安全的,应该是内存安全,堆是共享内存,可以被所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就可以说是线程安全的

是进程和线程共有的空间,分为全局堆和局部堆,没有额外的分配空间,局部堆就是用户分配的空间,堆在操作系统中堆进程进行初始化的时候分配,运行过程中也可以像系统要额外的堆,但是用完了还要还给操作协同,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建,堆锁存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例 以及数组都在这里分配内存

是每个线程独有的,保存其运行状态和局部自动变量的,栈在线程开始的时候初始化,每个线程的栈相互独立,因此栈是线程安全的。操作系统在切换线程的时候,会自动给切换栈,栈空间不需要再高级语言里面显示分配和释放。

目前主流的操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个线程只能访问分配给自己的内存空间,而不能访问别的进程,这是由操作系统保障的。

每个进程的内存空间中都会有一块特殊的公共区域, 通常成为堆(内存),进程内所有线程都可以访问到该区域,这就是造成问题的潜在原因。

京东二面:并发编程三要素

  • 原子性:不可分割的操作,多个步骤要保证同时成功或者同时失败
  • 可见性:程序执行的顺序和代码的顺序保持一致
  • 可用性: 一个线程堆共享变量的修改,另一个线程能立马看到

Java 死锁如何避免

造成死锁的原因:

  • 一个资源每次只能被一个线程访问
  • 一个线程再阻塞某个资源是,不释放已占有资源
  • 一个线程已经获得的资源,在未使用完之前,不能被强制进行剥夺
  • 若干线程形成头尾相接的循环等待资源关系

以上就是造成死锁必要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可,而其中3个条件是作为要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系

在开发过程中:

  • 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  • 要注意加锁的时限,可以针对锁设置一个超时时间
  • 要注意死锁检查,这是一种预防机制,腰包在第一时间发现死锁并及逆行解决。

京东一面:如果你提交任务时,线程池队列已满,这时会发生什么?

  • 如果使用的是无界队列,那么可以继续提交任务是没有关系的
  • 如果使用的是有界队列,提交任务时,如果队列满了,如果核心线程数没有达到上线,那么则增加线程,如果线程数已达到了最大值,则使用拒绝策略进行拒绝。
http://www.dtcms.com/a/415414.html

相关文章:

  • 比特之绘:位图的二进制诗学
  • 【K8s-Day 32】StatefulSet 深度解析:为你的数据库和有状态应用保驾护航
  • 优质的营销网站建设广告公司取名
  • Webpack5 第四节
  • 设计网站公司力荐亿企邦松江新城投资建设发展有限公司网站
  • 家用电器:从解放双手到智能生活的变革者
  • 上海网站建设大概多少钱WordPress允许用户修改评论
  • 如何给网站做seo优化用网站做宣传的方案
  • 使用神经网络预测天气
  • 青海省建设工程信息网站最近十大新闻
  • 建设银行网站显示404企业活动网站创意案例
  • XAMPP下载安装教程(附下载链接,图文并茂)
  • 杭州网站建设浙江搜搜网站收录
  • Redis-分布式锁-redission原理
  • 博州住房和城乡建设局网站wordpress搭建官网
  • 做报废厂房网站怎么做网站架设的结构
  • 海口h5建站西地那非片能延时多久有副作用吗
  • 从知乎403拦截到成功采集:网页抓取实战
  • wordpress建立购物网站seo综合查询
  • 极简 Go 语言教程:从 Java 开发者视角 3 小时入门实战
  • 济宁网站建设第一品牌深圳十大品牌策划公司
  • 破解大语言模型的无失真水印
  • Android开发-Fragment
  • 等额本息年利率反推方法
  • 电商网站建设需要开原网站开发
  • 网站推广服务合同中国建筑集团有限公司电话
  • 全国金融许可证失控情况数据(邮政储蓄网点 / 财务公司等)2007.7-2025.7
  • 基于STM32与influxDB的电力监控系统-5
  • 太原做app网站建设推广普通话奋进新征程宣传语
  • 【Day 65】Linux-ELK