【Java面试题】多线程

1、进程与线程

术语定义
线程1、线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;
2、进程要想执行任务,必须得有线程,进程至少要有一条线程;
3、程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。
进程1、进程是指在系统中正在运行的一个应用程序,进程可以看成程序执行的一个实例;
2、每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存。
3、

区别:

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
  • 资源分配:同一进程内的线程共享本进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的。进程是系统资源分配的独立实体.
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
  • 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2、线程的实现方式?

  • 继承Thread类
  • 实现Runnable接口
  • 使用 Callable 和 Future

3、Thread类中的start()和run()方法有什么区别?

  • 通过start()方法来启动一个线程,此时线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止,所以通过start()方法可以达到多线程的目的。
  • 如果直接调用线程类的run()方法,会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,即start()方法呢能够异步的调用run()方法,但是直接调用run()方法确实同步的,无法达到多线程的目的。
  • start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

4、线程状态

  • NEW状态:new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动该线程,那么此时的状态就是新建状态。
  • RUNNABLE状态:线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。
  • RUNNING状态:一旦 CPU 通过轮询或其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。
  • BLOCKED状态:线程正在等待获取锁。
    • 进入BLOCKED状态,比如调用了 sleep 或者 wait 方法。
    • 进行某个阻塞的 IO 操作,比如因网络数据的读写进入BLOCKED状态。
    • 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态。
  • TERMINATED状态:TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。下面几种情况会进入TERMINATED状态:
    • 线程运行正常结束,结束生命周期
    • 线程运行出错意外结束
    • 到JVM Crash导致所有的线程都结束

线程状态转化图
在这里插入图片描述

5、i-- 与 System.out.println() 的异常

示例代码如下:

class MyThread extends Thread {
    private int i = 5;
    
    @Override
    public void run() {
    	System.out.println("i=" + (i--) + " threadName=" + Thread.currentThread().getName());
    }
}
//测试i--与println()联合使用时可能出现异常
public class Test {
    public static void main(String[] args) {
        // 创建一个MyThread对象,并将该对象分别加载到五个线程中并分别给线程命名
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        Thread t2 = new Thread(myThread);
        Thread t3 = new Thread(myThread);
        Thread t4 = new Thread(myThread);
        Thread t5 = new Thread(myThread);

        // 启动五个线程,myThread中的 i=5 会由5个线程共享,5个线程分别执行 run()
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

运行结果:

i=5 threadName=Thread-1
i=2 threadName=Thread-5
i=5 threadName=Thread-2
i=4 threadName=Thread-3
i=3 threadName=Thread-4

原因: println()方法与i--联合使用时“有可能”出现异常,虽然println()方法在内部是同步的,但i--的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率。

println() 的源码:

public void println(String x) {
	synchronized (this) {
		print(x);
		newLine();
	}
}

解决方法:println()i--联合使用时可能会出现非线程安全问题,解决方法仍是使用同步方法,即在需要同步的方法(比如上文中的run方法)前加上关键字synchronized

6、如何知道代码段被哪个线程调用?

System.out.println(Thread.currentThread().getName());

7、线程活动状态

public class XKThread extends Thread {
	@Override
	public void run() {
		System.out.println("run run run is " + this.isAlive() );
	}

	public static void main(String[] args) {
		XKThread xk = new XKThread();
		System.out.println("begin ——— " + xk.isAlive());
		xk.start();
		System.out.println("end ————— " + xk.isAlive());
	}
}

8、sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前的正在执行的线程休眠(暂停执行)。

9、如何优雅地设置睡眠时间?

jdk1.5后,引入了一个枚举 TimelJnit,对sleep方法提供了很好的封装。

比如要表达2小时22分55秒899毫秒。

Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(2);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

可以看到表达的含义更清晰,更优雅。

10、停止线程

run()方法执行完成,自然终止。

stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。

大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记,此方法不会去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

11、interrupted和islnterrupted的区别

interrupted:判断当前线程是否已经中断,会清除状态。
islnterrupted:判断线程是否已经中断,不会清除状态。

12、yield

放弃当前CPU资源,将它让给其他的任务占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

测试代码:(cpu独占时间片)

public class XKThread extends Thread {
	@Override
	public void run() {
		long beginTime = System.currentTimeMillis();
		int count = 0;
		for (int i = 0; i < 50000000; i++) {
			count = count + (i + 1);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("⽤用时 = " + (endTime - beginTime) + " 毫秒! ");
	}

	public static void main(String[] args) {
		XKThread xkThread = new XKThread();
		xkThread.start();
	}
}

输出结果:

用时 = 20 毫秒!

加入yield再来测试。(CPU让给其他资源导致速度变慢)

public class XKThread extends Thread {
	@Override
	public void run() {
		long beginTime = System.currentTimeMillis();
		int count = 0;
		for (int i = 0; i < 50000000; i++) {
			Thread.yield();//加入yield
			count = count + (i + 1);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("⽤用时 = " + (endTime - beginTime) + " 毫秒! ");
	}

	public static void main(String[] args) {
		XKThread xkThread = new XKThread();
		xkThread.start();
	}
}

结果:

用时 = 38424 毫秒!

13、线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu优先执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。

Java的优先级分为1~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:java.lang.lllegalArgumentException。

14、优先级继承特性

线程的优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。

15、谁跑的更快?

设置优先级高低两个线程,累加数字,看谁跑的快,上代码。

public class Run extends Thread{
	public static void main(String[] args) {
		try {
			ThreadLow low = new ThreadLow();
			low.setPriority(2);
			low.start();
			ThreadHigh high = new ThreadHigh();
			high.setPriority(8);
			high.start();
			Thread.sleep(2000);
			low.stop();
			high.stop();
			System.out.println("low = " + low.getCount());
			System.out.println("high = " + high.getCount());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

class ThreadHigh extends Thread {
	private int count = 0;
	public int getCount() {
		return count;
	}

	@Override
	public void run() {
		while (true) {
			count++;
		}
	}
}

class ThreadLow extends Thread {
	private int count = 0;
	public int getCount() {
		return count;
	}
	
	@Override
	public void run() {
		while (true) {
			count++;
		}
	}
}

结果:

low = 1193854568
high = 1204372373

16、线程种类

Java线程有两种,一种是用户线程,一种是守护线程(Daemon)。

17、守护线程

17.1 守护线程的特点

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。

17.2 Java中典型的守护线程

GC(垃圾回收器)

17.3 如何设置守护线程

Thread.setDaemon(true)

PS:Daemon属性需要在启动线程之前设置,不能再启动后设置。

17.4 Java虚拟机退出时Daemon线程中的finally块一定会执行?

Java虚拟机退出时,Daemon线程中的finally块并不一定会执行。

代码示例:

public class XKDaemon {
	public static void main(String[] args) {
		Thread thread = new Thread(new DaemonRunner(),"xkDaemonRunner");
		thread.setDaemon(true);
		thread.start();
	}
	
	static class DaemonRunner implements Runnable {
		@Override
		public void run() {
			try {
				SleepUtils.sleep(10);
			} finally {
				System.out.println("Java⼩小咖秀 daemonThread finally run …");
			}
		}
	}
}

结果:


没有任何的输出,说明没有执行finally。

18、设置线程上下文类加载器

获取线程上下文类加载器

public ClassLoader getContextClassLoader()

设置线程类加载器(可以打破Java类加载器的父类委托机制)

public void setContextClassLoader(ClassLoader cl)

19、join

join是指把指定的线程加入到当前线程,比如join某个线程a,会让当前线程b进入等待,直到a的生命周期结束,此期间b线程是处于blocked状态。

20、synchronized

20.1 什么是synchronized?

synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对象的所有读写都将通过同步的方式来进行。

20.2、synchronized包括哪两个jvm重要的指令?

monitor entermonitor exit

20.3 synchronized关键字用法?

可以用于对代码块或方法的修饰

20.4 synchronized锁的是什么?

被修饰情况被锁对象
普通同步方法当前实例对象
静态同步方法当前类的Class对象
同步方法块synchronized括号里配置的对象

21、Java对象的组成

Java对象在内存中的保存格式
内容长度(32/64位JVM)说明
对象头Mark Word32/64bit存储对象的hashCode或者锁信息等。当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
指向类的指针32/64bit即指向当前对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。
数组长度32/32bit只有数组对象保存了这部分数据
实例数据实例数据部分是对象真正存储的有效信息,也就是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。 这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
对齐填充字节因为JVM要求Java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。

synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽存储对象头,如果对象是非数组类型,用2字宽存储对象头。

Tips:32位虚拟机中一个字宽等于4字节。

32位JVM的Mark Word默认存储结构:

锁状态25bit4bit1bit是否偏向锁2bit锁标志位
无锁状态对象的hashCode对象分代年龄001

21.1 Mark Word的状态变化

Mark Word存储的数据会随着锁标志位的变化而变化。在32位JVM中长度是这么存的:

锁状态25bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
无锁对象的HashCode对象分代年龄001
偏向锁线程IDEpoch对象分代年龄101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC标记11

64位JVM下,Mark Word是64位大小的。

锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁标志位
无锁unusedhashCode001
偏向锁ThreadID(54bit) Epoch(2bit)101

JVM一般是这样使用锁和Mark Word的:

  1. 无锁状态时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
  2. 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程ID,表示进入偏向锁状态
  3. 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向锁状态,Mark Word中记录的线程ID就是线程A自己的ID,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
  4. 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程ID改为线程B的ID,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
  5. 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
  7. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

22、Java锁的种类

22.1 锁的升降级规则

Java SE1.6为了提高锁的性能。引入了偏向锁和轻量级锁。

Java SE1.6中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

22.2 偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行cas操作来加锁和解锁,只需测试一下对象头Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1(表示当前是偏向锁),如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向锁只限当前线程。
在这里插入图片描述

关闭偏向锁延迟

java6 和 7 中默认启用偏向锁延迟,但是会在程序启动几秒后才激活,如果需要关闭延迟,使用-XX:BiasedLockingStartupDelay=0。

如何关闭偏向锁

JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
Tips:如果你可以确定程序的所有锁通常情况处于竞态,则可以选择关闭。

22.3 轻量级锁

线程在执行同步块,JVM会在当前线程的栈帧中创建用于储存锁记录的空间。并将对象头中的Mark
Word复制到锁记录中。然后线程尝试使用 cas 将对象头中的MarkWord替换为自己锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁的解锁

轻量级锁解锁时,会使原子操作cas将displaced Mark Word替换回对象头,如果成功则表示没有竞争发生,如果失败,表示存在竞争,此时锁就会膨胀为重量级锁。
在这里插入图片描述

22.4 锁的优缺点对比

优点缺点适用场景
偏向锁加锁和解锁都不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间、同步块执行速度非常块
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间慢追求吞吐量、同步块执行时间较长

23、什么是原子操作

不可被中断的一个或者一系列操作。

23.1 Java如何实现原子操作

Java中通过锁和循环 CAS 的方式来实现原子操作,JVM的CAS操作利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

23.2 CAS实现原子操作的3大问题

  • ABA问题
  • 循环时间长消耗资源大
  • 只能保证一个共享变量的原子操作
什么是ABA问题

问题:因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新。如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检测时会发现值没有发生变化,其实是变过的。

本质:ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成cas多次执行的问题。

解决:添加版本号,每次更新的时候追加版本号,A-B-A一>1A-2B-3A。

从JDK1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。

CAS循环时间长占用资源大问题

如果 JVM 能支持处理器提供的pause指令,那么效率会有一定的提升。

第一、它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,有些处理器延迟时间是0。

第二、它可以避免在退出循环的时候因为内存顺序冲突而引起的CPU流水线被清空,从而提高CPU执行效率。

CAS只能保证一个共享变量原子操作

1、对多个共享变量操作时,可以用锁。
2、可以把多个共享变量合并成一个共享变量来操作。比如 x=1,k=a,合并成xk=1a,然后用cas操作xk。

Tips:java1.5开始,JDK提供了AtormcReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象来进行cas操作。

24、voIatiIe关键字

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性“。

Java语言规范第3版对 volatil 定义如下,Java允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为 volatil,Java线程内存模型所有线程看到这个变量的值是一致的。

25、wait

方法wait()的作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入“预执行队列”中,并在wait()所在的代码处停止执行,直到接到通知或中断为止。

在调用wait之前,线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁。

26、notify

notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来唤醒一个线程,如果有多个线程等待,则随机挑选出其中一个处于wait状态的线程,对其发出通知notify,并让它等待获取该对象的对象锁。

27、notify/notifyAII

notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

28、等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。

28.1 等待/通知经典范式

等待wait:

synchronized(obj) {
	while(条件不不满⾜) {
		obj.wait();
	}
	执行对应逻辑
}

通知notify:

synchronized(obj) {
	改变条件
	obj.notifyAll();
}

29、ThreadLocal

主要解决每一个线程想绑定自己的值,存放线程的私有数据。

29.1 ThreadLocal使用

获取当前的线程的值通过get(),默认值为空,设置set(T)方式来设置值。

public class XKThreadLocal {
	
	public static ThreadLocal threadLocal = new ThreadLocal();

	public static void main(String[] args) {
		if (threadLocal.get() == null) {
			System.out.println("未设置过值");
			threadLocal.set("Java⼩小咖秀");
		}
		System.out.println(threadLocal.get());
	}
}

输出:

未设置过值
Java⼩小咖秀

29.2 解决get()返回null问题

通过继承重写initialValue()方法即可。

代码实现:

public class ThreadLocalExt extends ThreadLocal{
	
	static ThreadLocalExt threadLocalExt = new ThreadLocalExt();
	
	@Override
	protected Object initialValue() {
		return "Java小咖秀";
	}

	public static void main(String[] args) {
		System.out.println(threadLocalExt.get());
	}
}

输出:

Java小咖秀

30、Lock接口

锁可以防止多个线程同时共享资源。Java5前,程序是靠synchronized实现锁功能。Java5之后,并发包新增Lock接口来实现锁功能。

30.1 Lock接口提供synchronized不具备的主要特性

特性描述
尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁与synchronized不同,获取到锁的线程能够响应中断。当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁在指定的截止时间之前获取锁。如果截止时间到了仍旧无法获取锁,则返回。

30.2 重入锁ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

30.3 重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

  1. 锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。
  2. 所得锁最终释放。线程重复n次是获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

30.4 ReentrantLockl默认锁?

默认非公平锁。

代码为证:

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	} else if (current == getExclusiveOwnerThread()) {
			int nextc = c + acquires;
			if (nextc < 0) // overflow
				throw new Error("Maximum lock count exceeded");
			setState(nextc);
			return true;
	}
	return false;
}

31、公平锁和非公平锁的区别

公平性与否是针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

32、读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻塞。读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。

Java中提供读写锁的实现类是ReentrantReadWriteLock

33、LockSupport工具

定义了一组公共静态方法,提供了最基本的线程阻塞和唤醒功能。

方法名称描述
void park()阻塞当前线程,如果用 unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回
void parkNanos(long nanos)阻塞当前线程,最长不超过 nanos 纳秒,返回条件在 park()的基础上增加了超时返回
void parkUntil(long deadline)阻当前线程.直到deadline时间(从1970年开始到deadline时间的毫秒数)
void unpark(Thread thread)唤醒处于阻塞状态的线程thread

34、Condition接口

提供了类似Object监视器方法,与Lock配合使用实现等待/通知模式。

34.1 Condition使用

代码示例:

public class XKCondition {
	Lock lock = new ReentrantLock();
	Condition cd = lock.newCondition();

	public void await() throws InterruptedException {
		lock.lock();
		try {
			cd.await();//相当于Object 方法中的wait()
		} finally {
			lock.unlock();
		}
	}

	public void signal() {
		lock.lock();
		try {
			cd.signal(); //相当于Object 方法中的notify()
		} finally {
			lock.unlock();
		}
	}
}

35、Java并发容器

并发容器描述
ConcurrentHashMap并发安全版HashMap,Java7中采用分段锁技术来提高并发效率,默认分16段。Java8放弃了分段锁,采用CAS,同时当哈希冲突时,当链表的长度到8时,会转化成红黑树。
ConcurrentLinkedQueue基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用cas算法来实现。
ConcurrentLinkedDeque
ConcurrentSkipListMap
ConcurrentSkipListSet
CopyOnWriteArrayList
CopyOnWriteArraySet
----------------------------------------------------------------------------------------------------------
ArrayBlockingQueue一个由数据支持的有界阻塞队列,此队列按照FIFO原则对元素进行排序。队列头部在队列中存在的时间最长,队列尾部存在时间最短。
LinkedBlockingQueue
LinkedBlockingDeque双向队列
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列,但它不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
-------------------------------------------------------------------------------------------------
SynchronousQueue
LinkedTransferQueue
DelayQueue是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现 Delayed 接口和 Comparable 接口,在创建元素时可以指定多久才能从队列中获取当前元素。

36、阻塞队列

36.1 什么是阻塞队列?

队列比较好理解,数据结构中我们都接触过,先进先出的一种数据结构,那什么是阻塞队列呢?从名字可以看出阻塞队列其实也就是队列的一种特殊情况。它具有如下特点:

  1. 当阻塞队列满了,往队列添加元素的操作将会被阻塞。
  2. 当阻塞队列为空时,从队列中获取元素的操作将会被阻塞。

36.2 阻塞队列常用的应用场景?

常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列正好是生产者存放、消费者来获取的容器。

36.3 Java里的阻塞的队列

队列描述
ArrayBlockingQueue数组结构组成的 、有界阻塞队列
LinkedBlockingQueue链表结构组成的 、有界阻塞队列
PriorityBlockingQueue支持优先级排序 、无界阻塞队列
DelayQueue优先级队列实现 、无界阻塞队列
SynchronousQueue不存储元素 、阻塞队列
LinkedTransferQueue链表结构组成、无界阻塞队列
LinkedBlockingDeque链表结构组成、双向阻塞队列

37、Fork/Join

Java7 提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务的结果后得到大任务结果的框架。

37.1 工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

37.2 工作窃取算法的优缺点

  • 优点:充分利用线程进行并行计算,减少了线程间的竞争。
  • 缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

38、Atomic包

Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁地进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。

原子更新对象
原子操作更新基本类型
AtomicBoolean布尔类型
Atomiclnteger整型类型
AtomicLong长整型类型
原子操作更新数组
AtomicIntegerArray整型数组里的元素
AtomicLongArray长整型数组里的元素
AtomicReferenceArray引用类型数组里的元素
原子操作更新引用类型
AtomicReference引用类型
AtomicReferenceFieldUpdater引用类型里的字段
AtomicMarkabIeReference原子更新带有标记位的引用类型,标记位用boolean类型表示,构造方法为`AtomicMarkableReference(V initialRef, boolean initialMark)`
原子操作更新字段类
AtomiceIntegerFieldUpdater整型字段的更新器
AtomiceLongFieldUpdater长整型字段的更新器
AtomiceStampedFieldUpdate带有版本号的引用类型 `

39、JDK并发包中提供了哪几个比较常见的处理并发的工具类?

提供并发控制手段:CountDownLatch、CyclicBarrier、Semaphore

线程间数据交换:Exchanger

39.1 CountDownLatch

countDownLatch这个类 使一个线程等待其他线程各自执行完毕后再执行。 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

CountDownLatch的构造函数接受一个int类型的参数作为计数器,你想等待n个线程完成,就传入n。

三个重要的方法:

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

Tips:

  • 计数器必须大于等于0,当为0时,await()就不会阻塞当前线程。
  • 不提供重新初始化或修改内部计数器的值的功能。

39.2 CyclicBarrier

可循环使用的屏障。它的作用就是会让所有线程都等待完成后才会继续下一步行动。 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。

CyclicBarrier默认构造器是CyclicBarrier(int parities),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。

39.3 CountDownLatch与CyclicBarrier区别

计数器等待
CountDownLatch计数器只能使用一次一个线程或多个等待另外n个线程完成之后才能执行
CyclicBarrier计数器可以重置(通过reset()方法)n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

39.4 Semaphore

用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。

应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。

39.5 Exchanger

Exchanger是一个用于线程间协作的工具类,它提供一个同步点,在这个同步点上,两个线程可以交换彼此的数据。比如第一个线程执行exchange()方法,它会一直等待第二个线程也执行exchange(),当两个线程都到同步点,就可以交换数据了。

一般来说为了避免一直等待的情况,可以使用exchange(V x,long timeout, TimeUnit unit),设置最大等待时间。

Exchanger可以用于遗传算法。

40、线程池

40.1 为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处:

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
  • 提高响应速度: 当任务到达时,任务不需要等到线程创建就可以立即执行。
  • 提供线程可以管理性: 可以通过设置合理分配、调优、监控。

40.2 线程池工作流程

在这里插入图片描述
假设线程池的最大容量为5个,其中核心线程池容量为3个,非核心线程池容量为2个。工作队列的容量大小为2个。

一开始,线程池里的线程个数为0,这时候任务1来了,线程池管理者就会创建1个线程,用来执行任务1,接着任务2、3也来了,线程池管理者就会再创建两个线程,用来执行任务2和3。此时,线程池里一共3个线程,也就是核心线程池满了。

这时候,又来一个任务4,但是核心线程池已经满了,管理者就会让它到工作队列进行排队,任务5来了,同样也是在工作队列里等待。此时,等待队列已满。

这时候任务1执行完了,释放了一个线程1,就可以让任务4出队被处理。然后,任务6和7来了。此时,三个核心线程都在工作,工作队列还有一个空位。任务6去了工作队列排队,任务7没有位置坐。因为线程池最大可以放5个线程,只有核心线程池3个满了,还有两个位置可以放非核心线程,所以管理者会创建一个非核心线程,然后任务5出队,任务7入队。

直到线程池全满(5个),工作队列也满,新来的任务才会无法被处理。优先使用线程池中的核心线程池。

40.3 创建线程池参数有哪些作用?

public ThreadPoolExecutor(   int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

  • corePoolSize:核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建
  • maximumPoolSize:线程池最大数,允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。
  • keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多,并且每个任务执行时间较短,可以调大时间,提高线程利用率。
  • unit:线程保持活动时间单位,天(DAYS)、小时(HOURS)、分钟(MINUTES、毫秒MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)
  • workQueue:任务队列,保存等待执行的任务的阻塞队列。一般来说可以选择如下阻塞队列:
    • ArrayBlockingQueue:基于数组的有界阻塞队列。
    • LinkedBlockingQueue:基于链表的阻塞队列。
    • SynchronizedQueue:一个不存储元素的阻塞队列
    • PriorityBlockingQueue:一个具有优先级的阻塞队列。
  • threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • handler:饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处理新的任务。默认策略是AbortPolicy。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:调用者所在的线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,直接丢掉。
    • 当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。

40.4 向线程池提交任务

可以使用execute()submit()两种方式提交任务。

  • execute():无返回值,所以无法判断任务是否被执行成功。
  • submit():用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程知道任务完成。get(long timeout, TimeUnit unit)可以设置超市时间。

40.5 关闭线程池

可以通过shutdown()shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。

shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。

shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,isTerminaed就会返回true。

一般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。

40.6 线程池如何合理设置

配置线程池可以从以下几个方面考虑。

  • 任务是CPU密集型、IO密集型或者混合型
    • CPU密集型可以配置容量较小的线程,比如 n + 1个线程。
    • IO密集型可以配置较多的线程,如 2n个线程。
    • 混合型可以拆成IO密集型任务和CPU密集型任务。
  • 任务优先级,高中低。
  • 任务时间执行长短。
  • 任务依赖性:是否依赖其他系统资源。

可以通过Runtime.getRuntime().availableProcessors()来获取cpu个数。建议使用有界队列,增加系统的预警能力和稳定性。

41、Executor

从JDK5开始,把工作单元和执行机制分开。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

41.1 Executor框架的主要成员

  • ThreadPoolExecutor:可以通过工厂类Executors来创建。有3种类型:
    • SingleThreadExecutor
    • FixedThreadPool
    • CachedThreadPool
  • ScheduledThreadPoolExecutor :可以通过工厂类Executors来创建。有2种类型:
    • ScheduledThreadPoolExecutor
    • SingleThreadScheduledExecutor
  • Future接口:Future和实现Future接口的FutureTask类来表示异步计算的结果。
  • Runnable和Callable:它们的接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。Runnable不能返回结果,Callable可以返回结果。

41.2 FixedThreadPool

查看源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());}

1、特点

  • FixedThreadPool是可重用固定线程数的线程池。
  • FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。
  • FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。

2、使用无界队列的影响

  • 当线程池中的线程数达到corePoolSize后,新任务将在无界阻塞队列中等待,因此线程池中的线程数不会超过corePoolSize;
  • 使用无界队列时maximumPoolSize将是一个无效参数;
  • 使用无界队列时keepAliveTime将是一个无效参数;
  • 使用无界队列时运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution()方法)。
  • 使用无界队列可能会堆积越来越多的任务,最终导致OOM;

3、 处理流程

  • (1)如果当前运行的线程数 < corePoolSize,则创建新线程来执行任务;
  • (2)如果当前运行的线程数 = corePoolSize,则将任务加入workQueue;
  • (3)线程执行完【1】中的任务后,循环从workQueue获取任务来执行;

4、适用场景

FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

41.3 SingleThreadExecutor

查看源码:

public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService
		(new ThreadPoolExecutor(1, 1,
								0L, TimeUnit.MILLISECONDS,
								new LinkedBlockingQueue<Runnable>()));
}

1、特点

  • SingleThreadExecutor是使用单个worker线程的Executor。
  • SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1(其他参数与FixedThreadPool相同)。
  • SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

2、适用场景

SingleThreadExecutor适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景。

3、工作流程

执行流程以及造成的影响同FixedThreadPool。

41.4 CachedThreadPool

查看源码:

public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
									60L, TimeUnit.SECONDS,
									new SynchronousQueue<Runnable>());

1、特点

  • CachedThreadPool是一个会根据需要创建新线程的线程池;
  • CachedThreadPool的corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的;
  • CachedThreadPool的keepAliveTime被设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,线程空闲时间超过60秒后将会被终止。
  • CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

2、工作流程

  • (1)首先执行SynchronousQueue.offer()。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(),主线程把任务交给空闲线程执行,execute()方法执行完成;
  • (2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
  • (3)在【2】中新创建的线程将任务执行完后,会执行SynchronousQueue.poll()。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒。如果60秒内主线程提交了一个新任务,那么这个空闲线程将执行主线程提交的新任务;否则这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

3、 适用场景
CachedThreadPool是无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值