Java面试题超详细整理《多线程篇》

Posted 龙源lll

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试题超详细整理《多线程篇》相关的知识,希望对你有一定的参考价值。

为什么要使用并发编程?

  • 提升多核CPU的利用率: 在一个多核CPU的主机上,我们可以创建多个线程,将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率。
  • 方便进行业务拆分: 面对复杂业务模型,可以对业务模块进行拆分,从而提升响应速度。而进行拆分时可以使用多线程技术来完成。

优点:提高了程序的执行效率,一定情况下可以提高程序运行速度。
缺点:可能出现内存泄漏、上下文切换、线程安全、死锁等问题。


并发编程三要素是什么?

  • 原子性:一个操作是不可分割的,要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的值进行修改后,另一个线程能够立刻看到修改的值。(synchronized,volatile)
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行指令重排)

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


并行、并发、串行的区别

  • 串行:串行在时间上不可能发生重叠,前一个任务没有执行结束,下一个任务就不会开始
  • 并行:并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行
  • 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,同一时间点,只有一个任务执行,任务交替执行

什么是线程和进程?两者的区别是什么?

  • 进程:程序的⼀次执⾏过程,是系统运⾏程序的基本单位。进程是动态的,系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
  • 线程:线程是进程划分成的更⼩的运⾏单位,进程中的一个执行任务,⼀个进程在其执⾏的过程中可以产⽣多个线程,在程序里独立执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享进程中的数据。

进程与线程的区别:

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,所以系统在产生⼀个线程,或是在各个线程之间作切换⼯作时,开销要⽐进程小得多,所以线程也被称为轻量级进程
  • 内存分配:进程与进程之间的地址空间和资源是相互独立的,同一进程的线程共享本进程的地址空间和资源,线程之间可能会相互影响。
  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。
  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

对线程安全的理解

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

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

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

如何保证线程安全?

方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用JVM提供的自动锁 synchronized。
方法三:使用JDK提供的手动锁 Lock。


synchronized关键字的作用

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

三种使用方式:

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能

构造方法可以使用 synchronized 关键字修饰么?
构造方法不能使用 synchronized 关键字修饰,构造方法本身就属于线程安全的,不存在同步的构造方法一说。


volatile 关键字的作用

Java 提供了 volatile 关键字是线程同步的轻量级实现,用来保证可见性和有序性(禁止指令重排),volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

  • 对于加了 volatile关键字的成员变量,在对这个变量进行修改时,全直接将CPU高级缓存中的数据送回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
  • 在对 volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性

volatile可以和CAS 结合,来保证原子性。

讲一下 JMM(Java 内存模型)
在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

volatile是怎样实现了?

volatile实现内存可见性原理:
一个问题:本地内存和主内存之间的值不一致,导致内存不可见。
在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。将当前处理器缓存行的数据写回系统内存,这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效,当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。

volatile实现有序性原理:
为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。
内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。

什么是CAS?

CAS即Compare And Swap,比较并替换。是一条CPU并发原语,Java中可以通过CAS操作来保证原子性,它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语提现在Java语言中就是sun.misc.UnSafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作。原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。

Unsafe:CAS的核心类,Java方法无法直接访问内存,需要通过本地方法native来访问,在Unsafe中所有方法都是native方法,用来直接操作内存,执行相应的任务。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。类似于乐观锁。CAS自旋的概率会比较大,从而浪费更多的CPU资源。在这个过程中可能存在ABA问题:
当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次(A->B->A),而经过两次修改后,对象的值又恢复为旧值,这样当前线程无法正确判断这个对象是否修改过。

ABA的问题的解决方式:

  • ABA的解决方法也很简单,就是利用版本号。给变量加上一个版本号,每次变量更新的时候就把版本号加1,这样即使E的值从A—>B—>A,版本号也发生了变化,这样就解决了CAS出现的ABA问题。基于CAS的乐观锁也是这个实现原理。
  • JDK1.5时可以利用AtomicStampedReference类来解决这个问题,AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳,对象值和时间戳都必须满足期望值,写入才会成功

自旋:当多个线程同时操作一个共享变量时,只有一个线程可以对变量进行成功更新,其他线程均会失败,但是失败并不会被挂起,进行再次尝试,也就是自旋。Java中的自旋锁就是利用CAS来实现的。


说说 synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。
  • volatile 关键字只能用于变量,而synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

单例模式

双重校验锁实现对象单例(线程安全)

public class Singleton 

    private volatile static Singleton uniqueInstance;

    private Singleton() 
    

    public  static Singleton getUniqueInstance() 
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) 
            //类对象加锁
            synchronized (Singleton.class) 
                if (uniqueInstance == null) 
                    uniqueInstance = new Singleton();
                
            
        
        return uniqueInstance;
    

uniqueInstance 采用 volatile 关键字修饰的原因: uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  • 为 uniqueInstance 分配内存空间
  • 初始化 uniqueInstance
  • 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

谈谈 synchronized 和 ReentrantLock 的区别

  • synchronized是一个内置的Java关键字,ReentrantLock是一个类,实现了Lock接口

  • synchronized会自动加锁或释放锁,ReentrantLock需要手动加锁和释放锁

  • synchronized 无法判断获取锁的状态,ReentrantLock 可以判断是否获取到了锁

  • synchronized底层是JVM层面的锁,ReentrantLock是API层面的锁

  • synchronized是可重入锁,非公平锁,ReentrantLock是可重入锁,可以选择公平锁和非公平锁

  • synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock锁的线程,通过代码中int类型的state标识来标识锁的状态

  • 相比synchronized,ReentrantLock增加了一些高级功能。(等待可中断、可实现公平锁、可实现选择性通知)

Lock接口
在jdk1.5以后,增加了juc并发包且提供了Lock接口用来实现锁的功能,它除了提供了与synchroinzed关键字类似的同步功能,还提供了比synchronized更灵活api实现。可以把 Lock 看成是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁。

ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

什么是AQS?

AQS的全称是AbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架,像ReentrantLock,Semaphore,FutureTask都是基于AQS实现的。

AQS的工作流程:AQS会维护一个共享资源,当被请求的共享资源空闲,则将请求资源的线程设为有效的工作线程,同时锁定共享资源。如果被请求的资源已经被占用了,AQS就用过队列实现了一套线程阻塞等待以及唤醒时锁分配的机制。

从图中可以看出AQS维护了一个共享资源和一个FIFO的线程等待队列。这个队列是通过CLH队列实现的,该队列是一个双向队列,有Node结点组成,每个Node结点维护一个prev引用和next引用,这两个引用分别指向自己结点的前驱结点和后继结点,同时AQS还维护两个指针Head和Tail,分别指向队列的头部和尾部。

如何使用AQS自定义同步器?

AQS的资源共享方式:
Exclusive:独占,只有一个线程可以执行,例如ReentrantLock
Share:共享,多个线程可同时执行,如Semaphore/CountDownLatch

AQS的底层使用了模板方法模式,自定义同步器只需要两步:
第一,继承AbstractQueuedSynchronizer,第二,重写以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式,尝试获取资源。
  • tryRelease(int):独占方式,尝试释放资源。
  • tryAcquireShared(int):共享方式,尝试获取资源。负数表示失败,0表示成功,但无剩余可用资源,正数表示成功并且有剩余资源
  • tryReleaseShared(int):共享方式,尝试释放资源

独占式的ReentrantLock实现方式:,state初始状态为0,表示未锁定状态。A线程进行lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再调用tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

共享式的CountDownLatch实现方式:任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

CopyOnWriteArrayList

我们都知道将ArrayList作为共享变量,在多线程的情况下是不安全的,解决方法是使用Collections中的SynchronizedList方法,或者我们代码中进行加锁,其实还有另一种线程安全的List,就是CopyOnWriteArrayList。

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。

先对CopyOnWriteArrayList进行一个总体概览,它具有三个特点:

  • 线程安全的,多线程环境下可以直接使用,无需加锁;
  • 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
  • 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。

CountDownLatch、CyclicBarrier、Semaphore的区别

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

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

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

CountDownLatch:计数器,允许一个或多个线程等待直到其他线程中执行的一组操作完成同步辅助。可以用于高并发测试,即计数积累了一定数量的线程后再一起执行。
原理:
countDownLatch.countDown(); // 计数器数量-1
countDownLatch.await(); // 线程会被挂起,等待计数器归零,然后再向下执行,可以设置等待时间

import java.util.concurrent.CountDownLatch; // 计数器 
public class CountDownLatchDemo  
	public static void main(String[] args) throws InterruptedException  
	    
	    // 总数是6,必须要执行任务的时候,再使用! 
		CountDownLatch countDownLatch = new CountDownLatch(6); 
		for (int i = 1; i <=6 ; i++) 
 			new Thread(()-> 
 				System.out.println(Thread.currentThread().getName()+" Go out"); 
 				countDownLatch.countDown(); // 数量-1 
 			,String.valueOf(i)).start(); 
 		
		countDownLatch.await(); // 等待计数器归零,然后再向下执行 
		System.out.println("Close Door"); 
	 

CyclicBarrier: 加法计数器,一组线程全部等待到达共同点。达不到会一直等待
cyclicBarrier.await(); //用来挂起当前线程,直至所有线程都到达brrier状态再同时执行后续任务;

import java.util.concurrent.BrokenBarrierException; 
import java.util.concurrent.CyclicBarrier; 
public class CyclicBarrierDemo  
	public static void main(String[] args)  
		/*** 集齐7颗龙珠召唤神龙 */ 
		CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> 
			System.out.println("召唤神龙成功!"); 
		); 
		for (int i = 1; i <=7 ; i++)  
			final int temp = i; // lambda能操作到 i 吗 
			new Thread(()-> 
				System.out.println(Thread.currentThread().getName()+"收 集"+temp+"个龙珠");
				try 
				cyclicBarrier.await(); // 等待 
				 catch (InterruptedException e)  
					e.printStackTrace(); 
				 catch (BrokenBarrierException e)  
					e.printStackTrace(); 
				 
			).start(); 
		 
	 

Semaphore:一组计数信号量
semaphore.acquire(); //获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release(); //释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!

import java.util.concurrent.Semaphore; 
import java.util.concurrent.TimeUnit; 
public class SemaphoreDemo  
	public static void main(String[] args)  
		// 线程数量:停车位! 限流! 抢车位!6车---3个停车位置
		Semaphore semaphore = new Semaphore(3); 
		for (int i = 1; i <=6 ; i++)  
			new Thread(()-> // acquire() 得到 
				try 
					semaphore.acquire(); 
					System.out.println(Thread.currentThread().getName()+"抢到车 位"); 			
					TimeUnit.SECONDS.sleep(2); 
					System.out.println(Thread.currentThread().getName()+"离开车 位"); 
				 catch (InterruptedException e)  
					e.printStackTrace(); 
				 finally  
					semaphore.release(); // release() 释放 
				 
			,String.valueOf(i)).start(); 
		 
	 

ReadWriteLock读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁
Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现ReentrantReadWriteLock。


锁分类

是否锁同步资源:乐观锁、悲观锁

乐观锁:一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁:一种悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是synchronized;

线程是否阻塞:自旋锁

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

线程自旋是需要消耗 CPU 的,说白了就是让 CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优点:
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

线程竞争时是否排队:公平锁、非公平锁

  • 公平锁:锁的分配机制是公平的,加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

  • 非公平锁:加锁时不考虑排队等待问题,JVM 按随机、就近原则分配锁的机制则称为不公平锁,非公平锁实际执行的效率要远远超出公平锁(5-10倍),除非程序有特殊需要,否则最常用非公平锁的分配机制。

线程中的多个流程是否可以获取同一把锁:可重入锁(递归锁)

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

多线程之间能不能共享同一把锁:共享锁和排他锁

  • 共享锁:共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
  • 排他锁:每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

锁状态:无锁、偏向锁、轻量级锁、重量级锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。

  • 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能

  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

  • 重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。


死锁

两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去!

如何查看线程死锁:通过jstack命令进行查看,jstack中会显示发生死锁的线程

数据库中查看死锁:
查看是否有表锁:show OPEN TABLES where In_use > 0;
查询进程:show processlist;
查看正在锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
查看等待锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS_WAITS;


什么是守护线程?

守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。(GC垃圾回收线程就是一个经典的守护线程)

应用场景:为其他线程提供服务支持、需要正常且立刻关闭某个线程时

守护线程不能用于访问固有资源,比如读写操作或计算逻辑,因为它在任何时候甚至在一个操作的中间发生中断。


创建线程有哪几种方式?

继承 Thread 类,重写run方法:

public class MyThread extends Thread  
	@Override 
	public void run()  
		System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); 
	

实现 Runnable 接口,实现run方法:

public class MyRunnable implements Runnable  
	@Override 
	public void run()  
		System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); 
	

实现 Callable 接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值:

public class MyCallable implements Callable<Integer>  
	@Override 
	public Integer call()  
		System.out.println(Thread.currentThread().getName() + " call()方法执行 中...");
		return 1; 
	

使用 Executors 工具类创建线程池,并开启线程。


Thread、Runable和Callable 三者区别?

  • Thread 是一个抽象类,只能被继承,而 Runable、Callable 是接口,需要实现接口中的方法。继承 Thread 重写run()方法,实现Runable接口需要实现run()方法,而Callable是需要实现call()方法。
  • Thread 和 Runable 没有返回值,Callable 有返回值,返回值可以被 Future 拿到。
  • 实现 Runable 接口的类不能直接调用start()方法,需要 new 一个 Thread 并发该实现类放入 Thread,再通过新建的 Thread 实例来调用start()方法。实现 Callable 接口的类需要借助 FutureTask (将该实现类放入其中),再将 FutureTask 实例放入 Thread,再通过新建的 Thread 实例来调用start()方法。获取返回值只需要借助 FutureTask 实例调用get()方法即可!

什么是 FutureTask?
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。


线程的 run()和 start()有什么区别?

通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。

方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

run() 可以重复调用,而 start()只能调用一次。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法?

如果直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。


线程的状态

线程通常有5种状态:新建、就绪、运行、阻塞和死亡状态

  • 新建(new):新创建了一个线程对象。
  • 就绪(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  • 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
  • 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
    阻塞的情况分三种:
    ① 等待阻塞(wait->等待对列):运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;
    ② 同步阻塞(lock->锁池):线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    ③ 其他阻塞(sleep/join): 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。


由上图可以看出:线程创建之后它将处于 new(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程这时候处于 ready(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice)后就处于 running(运⾏) 状态。


线程基本方法