线程知识点和面试题 | Java提升营

线程知识点和面试题

sleep()方法

在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

wait()方法

在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。

当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出 IllegalMonitorStateException异常。

唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。

waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

yield()方法

暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所>以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

yield()只能使同优先级或更高优先级的线程有执行的机会。

join()方法

等待该线程终止。等待调用join方法的线程结束,再继续执行之后的代码。

Java中sleep方法的几个注意点:


  • hread.sleep()方法用来暂停线程的执行,将CPU放给线程调度器。
  • Thread.sleep()方法是一个静态方法,它暂停的是当前执行的线程。
  • Java有两种sleep方法,一个只有一个毫秒参数,另一个有毫秒和纳秒两个参数。
  • 与wait方法不同,sleep方法不会释放锁
  • 如果其他的线程中断了一个休眠的线程,sleep方法会抛出Interrupted Exception。
  • 休眠的线程在唤醒之后不保证能获取到CPU,它会先进入就绪态,与其他线程竞争CPU。
  • 有一个易错的地方,当调用t.sleep()的时候,会暂停线程t。这是不对的,因为Thread.sleep是一个静态方法,它会使当前线程而不是线程t进入休眠状态。

    yield和sleep的区别


  • yield和sleep的主要是,yield方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。执行了yield方法的线程什么时候会继续运行由线程调度器来决定,不同的厂商可能有不同的行为。

  • yield方法不保证当前的线程会暂停或者停止,但是可以保证当前线程在调用yield方法时会放弃CPU。

volatile和synchronized的区别


  • volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

  • synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。volatile仅能实现变量的修改可见性,不能保证原子性,仅能实现对原始变量(如boolen、short、int、long等)操作的原子性,但需要特别注意,volatile不能保证复合操作的原子性,即使只是i++;而synchronized则可以保证变量的修改可见性和原子性

  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

对于volatile关键字,当且仅当满足以下所有条件时可使用:


  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    该变量没有包含在具有其他变量的不变式中。
  • static是类的属性,存储在类的那块内存,每个线程操作的时候会读取这个内存块,甚至会加载到寄存器或高速缓存中,这样自然不会保证其他线程对该值的可见性;而volatile表示每次读操作直接到内存,如果多个线程都遵循这样的约定,就会读取到最新的状态.

    PS:synchronized代码块会对变量进行写入操作

    如何控制某个方法允许并发访问线程的个数

    Semaphore(信号量)

产生死锁的四个必要条件:


  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?


  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测。

Java中活锁和死锁有什么区别?


  • 活锁:互相释放资源给对方,结果谁都没有用到这资源。
  • 死锁:互相抢着资源,谁都没有抢到。

面试题


1、多个线程同时读写,读线程的数量远远大于写线程,你认为应该如何解决并发的问题?你会选择加什么样的锁?

ReadWriteLock读写锁


2、JAVA的AQS是否了解,它是干嘛的?

AbstractQueuedSynchronizer(AQS)为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁定和相关同步器(信号量、事件,等等)提供一个框架。要明白AQS在功能上有独占锁和共享锁两种功能。


3、除了synchronized关键字之外,你是怎么来保障线程安全的?

lock、标志位


4、Java中synchronized 和 ReentrantLock 有什么不同?

  • 这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
  • Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

5、什么时候需要加volatile关键字?它能保证线程安全吗?

  • volatile是不能保证线程安全的,它只是保证了数据的可见性,不会再缓存,每个线程都是从主存中读到的数据,而不是从缓存中读取的数据,附上代码如下,当synchronized去掉的时候,每个线程的结果是乱的,加上的时候结果才是正确的。
  • 并发编程的3个概念:原子性、可见性、有序性
  • 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
  • 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

6、Java线程池实现原理

其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。


7、线程池内的线程如果全部忙,提交1个新的任务,会发生什么?队列全部塞满了之后,还是忙,再提交会发生什么?

  • 一个任务通过execute(Runnable)方法被添加到线程池,任务就是一个Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。当一个任务通过execute(Runnable)方法想添加到线程池时:
  • 如果此时线程池中数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务放入缓冲队列
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue 满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程 corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务
  • 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
  1. corePoolSize: 线程池维护线程的最少数量
  2. maxnumPoolSize: 线程池维护线程的最大数量
  3. keepAliveTime: 线程池维护线程所允许的空闲时间
  4. unit: 线程池维护线程所允许的空闲时间的单位
  5. workQueue: 线程池所使用的缓冲队列
  6. handler: 线程池对拒绝任务的处理策略

8、synchronized关键字锁住的是什么东西?在字节码中是怎么表示的?在内存中的对象上表现为什么?

synchronized锁住的是括号里的对象,不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。

在java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。

对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。

而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象


9、wait/notify/notifyAll方法法需不需要被包含在synchronized块中?这是为什么?

  • Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作。
  • 从语法角度来说就是Obj.wait(),Obj.notify必须synchronized(Obj){…}语句块内。
  • 从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行
  • 如果实例方法含有如下的语句时:wait();则其意义同:this.wait();

10、ExecutorService你一般是怎么用的?是每个service放一个还是一个项目里放一个?有什么好处?

  • Java线程池ExecutorService
  • 如果有一套相同逻辑的多个任务的情况下,应用一个线程池是个好选择。
  • 如果项目中有多套不同的这种任务,那每套任务应该一个线程池不是很正常的吗。

11、线程池的几种区别

  • newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:

  1. 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  2. 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  3. 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
  • newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

  • newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

  • newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。


12、java.util.concurrent包下CountDownLatchCyclicBarrierSemaphore使用场景

  • CountDownLatch

CountDownLatch类利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

  • CyclicBarrier

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。

  • Semaphore

Semaphore翻译成字面意思为信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

区别

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  1. CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  2. CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  3. 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。


13、什么是线程局部变量ThreadLocal

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。


14、ThreadLoal的作用是什么?

简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

给老奴加个鸡腿吧 🍨.