Java面试_周阳

一、 请谈谈你对volatile的理解

1. volatile是Java虚拟机提供的轻量级额同步机制

保证可见性;不保证原子性;禁止指令重排

2. 谈一谈JMM(Java内存模型)

Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:
(1). 线程解锁前,必须把共享变量的值刷新会主内存
(2). 线程加锁前,必须读取主内存的最新值到自己的工作内存中
(3). 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20210904202644525

2.1 可见性
通过前面的JMM的介绍,我们知道:
各个线程对主内存中共享变量的操作,都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值,但还未写回到主内存中,另一个线程BBB又对主内存中同一个共享变量X进行操作,但此时AAA线程工作内存中共享变量X对线程BBB来说并不可见,这种工作内存和主内存同步延迟现象就造成了可见性问题。

当下面这段代码没有加上volatile时:

package com.jujuxiaer.interview.thread;

import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */

class MyData{
	int number = 0;

	public void addTo60(){
		this.number = 60;
	}
}

public class VolatileDemo {

	public static void main(String[] args) {
		// 资源类
		MyData myData = new MyData();

		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "\t come in");
			// 暂停一会儿
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			myData.addTo60();
			System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
		}, "AAA").start();

		while (myData.number == 0) {
			// main线程会一直在这里卡着循环等待,直到number的值不为0,跳出循环
		}

		System.out.println(Thread.currentThread().getName() + "\t mission is over");
	}
}

image-20210904205937338

上图中一直卡着不动说明,当number共享资源没有加上关键字volatile时,工作流程是这样的,AAA线程将主内存中number=0的值读取到了AAA线程的工作内存中,也即工作内存中number=0,然后花了3秒将number的值改成了60,但是由于没有加关键字volatile,所以并不会把number=60写回到主内存中。 接着main线程走到了while(number == 0)这里,这是main线程从主内存中读取number的值,读取到的number还是0,所以这个while会一直成立,就出现了上图中一直卡着的现象了。

当number加上了关键字volatile:

package com.jujuxiaer.interview.thread;

import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */
class MyData{
	volatile int number = 0;

	public void addTo60(){
		this.number = 60;
	}
}

public class VolatileDemo {

	public static void main(String[] args) {
		// 资源类
		MyData myData = new MyData();

		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "\t come in");
			// 暂停一会儿
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			myData.addTo60();
			System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
		}, "AAA").start();

		while (myData.number == 0) {
			// main线程会一直在这里卡着循环等待,直到number的值不为0,跳出循环
		}

		System.out.println(Thread.currentThread().getName() + "\t mission is over, main thread get number value is: " + myData.number);
	}
}

image-20210904210817544

上面运行截图表明,加上volatile后,main线程在while循环处能够感知到number的值被更改,得到了通知。实际上的过程是这样的,AAA线程从主内存中读取到number=0放到自己的工作内存中,并将number的值改成了60,因为加上了volatile关键字,AAA线程会将更新后的值number=60同步到主内存,也即主内存中的number会变成60,接着main线程在while循环处从主内存中读取到的number就是为60,读取到了main线程的工作内存中。
2.2 原子性

虽然加了关键字volatile,但是不能保证原子性:

package com.jujuxiaer.interview.thread;

class MyData2{
	volatile int number = 0;

	public void addPlusPlus() {
		this.number++;
	}
}

public class VolatileDemo2 {
	public static void main(String[] args) {
		// 资源类
		MyData2 myData = new MyData2();

		for(int i = 0; i < 20; i++) {
		    new Thread(() -> {
					for (int j = 0; j < 1000; j++) {
						myData.addPlusPlus();
					}
		    }, String.valueOf(i)).start();
		}

		// 需要等待上面20个线程 全部计算完毕
		while (Thread.activeCount() > 2) {
			Thread.yield();
		}

		System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
	}
}

上面代码计算结果小于20000,说明么有保证 this.number++ 原子性。因为多个线程同时操作主内存中的number,会出现丢失写值的情况。number++在多线程下是给线程安全的,mber++操作实际上有三步,见下图:

image-20210904230641280

2.3 VolatileDemo代码演示可见性+原子性代码
package com.jujuxiaer.interview.thread;

import java.util.concurrent.atomic.AtomicInteger;

class MyData2{
	volatile int number = 0;

	public void addPlusPlus() {
		this.number++;
	}

	AtomicInteger atomicInteger = new AtomicInteger();

	public void addMyAtomic() {
		this.atomicInteger.getAndIncrement();
	}
}

public class VolatileDemo2 {

	public static void main(String[] args) {
		// 资源类
		MyData2 myData = new MyData2();

		for(int i = 0; i < 20; i++) {
		    new Thread(() -> {
					for (int j = 0; j < 1000; j++) {
						myData.addPlusPlus();
						myData.addMyAtomic();
					}
		    }, String.valueOf(i)).start();
		}

		// 需要等待上面20个线程 全部计算完毕
		while (Thread.activeCount() > 2) {
			Thread.yield();
		}

		System.out.println(Thread.currentThread().getName() + "\t int type, finally number value: " + myData.number);
		System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type, finally number value: " + myData.atomicInteger);
	}
}

上面代码执行结果:myData.number的值小于20000,myData.atomicInteger的值等于20000。原因是AtomicInteger能保证++操作的原子性。

2.4 有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下2中:

image-20210905092032159

(1). 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
(2). 处理器在进行重排序时必须要考虑指令之间的数据依赖性。
(3). 多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

重排的例子一:

public void mySort() {
    int x = 11; // 语句1
    int y = 12; // 语句2
    x = x + 5;  // 语句3
    y = x * x;  // 语句4
}

指令重排后可能出现的语句顺序如下:
    1234
    2134
    1324
    
问题:指令重排后,语句4会编程第一条吗?
    不会,因为重排序时必须要考虑指令之间的数据依赖性。如果语句4变成了第一条,那变量x都没有在语句4之前定义,所以不会。

重排的例子二:

初始时 int a, b, x, y = 0;

线程1线程2
x = a;y = b;
b = 1;a = 2;
x = 0 y = 0

如果编译器对这段程序做了指令重排优化后,可能会出现下面的程序读取顺序:

线程1线程2
b = 1;a = 2;
x = a;y = b;
x = 2 y = 1

禁止指令重排的小总结:

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
(1). 保证特定操作的执行顺序
(2). 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier,则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障来禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量刷新回主内存。

image-20210905094841861

对volatile变量进行读操作时,会在写操作前加入一条load屏障指令,从主内存中读取共享变量。

image-20210905095155148

线程安全性获得保证:

工作内存和主内存 存在 同步延迟现象导致的可见性问题,可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题,可以通过volatile关键字解决,因为volatile的另一个作用就是禁止指令重排优化。

3. 你在什么地方用过volatile?

3.1 单例模式DCL代码
public class SingletonDemo {

	private static SingletonDemo instance = null;

	public SingletonDemo() {
		System.out.println(Thread.currentThread().getName() + "\t 构造方法SingletonDemo()被执行");
	}

	public static SingletonDemo getInstance() {
		if (instance == null) {
			synchronized (SingletonDemo.class) {
				if (instance == null) {
					instance = new SingletonDemo();
				}
			}
		}
		return instance;
	}

	public static void main(String[] args) {
		for(int i = 0; i < 1000; i++) {
		    new Thread(() -> {
					SingletonDemo.getInstance();
		    }, String.valueOf(i)).start();
		}
	}
}
3.2 单例模式volatile分析
private static volatile SingletonDemo instance = null;

上面的代码中DCL(双端检锁)机制不一定保证线程安全,因为有指令重排的存在,需要给 SingletonDemo实例变量加上volatile关键字,来禁止指令重排。

分析一下DCL的过程,在某一个线程执行到第一次检测,读取到的instance不为null时,有可能instance的引用对应可能还没有完成初始化。
因为 instance = new SingletonDemo(); 可以分为以下散步完成(伪代码):

memory = allocate(); // 1. 分配对象内存空间
instance(memory); // 2. 初始化对象
instance = memory; // 3. 设置instance指向刚分配的内存地址,此时instance不为null

以上的步骤2和步骤3之间不存在数据依赖关系,而且在单线程环境下,无论重排序前还是重排后程序的执行结果并没有改变,所以会出现步骤1、步骤3、步骤2这种顺序的重排序优化的情况。
memory = allocate(); // 1. 分配对象内存空间
instance = memory; // 3. 设置instance指向刚分配的内存地址,此时instance不为null,但是此时还没有完成对象初始化
instance(memory); // 2. 初始化对象

二、CAS你知道吗

1. 比较并交换

public class CASDemo {

	public static void main(String[] args) {
		AtomicInteger atomicInteger = new AtomicInteger(5);
		System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());
		System.out.println(atomicInteger.compareAndSet(5, 2024) + "\t current data: " + atomicInteger.get());
	}
}

输出结果为:
true	 current data: 2019
false	 current data2: 2019

解释:new AtomicInteger(5)后,主内存中的值是5,atomicInteger.compareAndSet(5, 2019)这句期望主内存中是5,如果主内存中是5就将工作内存中的5改成2019,并写会到主内存中,也就主内存的值也改成了2019。atomicInteger.compareAndSet(5, 2024)这句期望主内存中值是5,将这个期望值和实际主内存中的值2019比较,两者不相等,则更新值为2024失败。最终的值还是2019。

2. CAS底层原理?如果知道,谈谈对UnSafe的理解

2.1 atomicInteger.getAndIncrement()
atomicInteger.getAndIncrement()方法的源代码:   
    
/**
* Atomically increments by one the current value.
*
* @return the previous value  内存偏移量地址
*/
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
2.2 Unsafe

image-20210906205755823

(1). Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应计划。

(2). 变量valueOffset,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

(3). 变量value用volatile修饰,保证了多线程之间的内存可见性

2.3 CAS是什么

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某功能的一个子过程,并且原语的执行必须是连续的,再执行过程中不允许被中断,也就是说CAS是一条CPU的原语指令,不会造成所谓的数据不一致问题

image-20210906211848126

2.3.1 unsafe.getAndAddInt()

var1 AtomicInteger对象本身
var2 该对象值的引用地址
var4 需要变动的数量
var5 是通过var1和var2找出的主内存中真实的值。
用该对象当前的值与var5比较,如果相同,更新var5+var4并且返回true;如果不同,继续取值然后再比较,直到更新完成。

例子:
假设线程A和线程B同时执行getAndAddInt操作(分别跑在不同的cpu上)。

  1. AtomicInteger里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B的工作内存中各持有一份值为5的value副本。
  2. 线程A通过getIntVolatile(var1, var2)拿到value的值为5,这时线程A被挂起。
  3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值为5,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B执行完成。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值5和主内存的值6不一致,说明该值已经被其它线程抢先一步修改过了,那线程A本次修改失败,只能重新读取重新来一遍了
  5. 线程A重新获取value值,因为value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
2.3.2 底层汇编

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

image-20210906214713351

2.3.3 简单小总结

CAS:比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较知道主内存和工作内存中的值一致为止。

CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,否则什么都不做,继续循环。

3. CAS的缺点

(1). 循环时间长开销很大;
我们可以看到上图中getAndAddInt方法执行时,有个do while,如果CAS失败,会一直循环尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

(2). 只能保证一个共享变量的原子操作;
当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是,对于多个共享变量执行时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。

(3). 引出来ABA问题;

三、原子类AtomInterger的ABA问题谈谈?原子更新引用知道吗?

1. ABA问题是怎么产生的

CAS算法的一个重要前提是,需要取出内存中某个时刻的数据,并在当下时刻比较并替换,那么在这个时间差会导致数据的变化。
比如线程1和线程2都从内存位置V中取出数据A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成了A,这时线程1进行CAS操作发现内存中任然是A,然后线程1操作成功。

尽管线程1的CAS操作成功,但是不代表这个过程就没有问题。

2. 原子引用
class User {
	String userName;
	int age;

	public User(String userName, int age) {
		this.userName = userName;
		this.age = age;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "User{" +
				"userName='" + userName + '\'' +
				", age=" + age +
				'}';
	}
}

public class AtomicReferenceDemo {
	public static void main(String[] args) {
		User zhangsan = new User("zhangsan", 22);
		User lisi = new User("lisi", 24);
		AtomicReference<User> atomicReference = new AtomicReference<>();
		atomicReference.set(zhangsan);

		System.out.println(atomicReference.compareAndSet(zhangsan, lisi) + "\t " + atomicReference.get().toString());
		System.out.println(atomicReference.compareAndSet(zhangsan, lisi) + "\t " + atomicReference.get().toString());
	}
}

结果输出为:
true	 User{userName='lisi', age=24}
false	 User{userName='lisi', age=24}
3. 时间戳原子引用
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author Jujuxiaer
 */
public class ABADemo {
	static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
	static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

	public static void main(String[] args) {
		System.out.println("==============以下是ABA问题的产生==============");
		new Thread(() -> {
			atomicReference.compareAndSet(100, 101);
			atomicReference.compareAndSet(101, 100);
		}, "t1").start();

		new Thread(() -> {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
		}, "t2").start();

		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("==============以下是ABA问题的解决==============");

		new Thread(() -> {
			int stamp = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 第一次取得的版本号为" + stamp);
			// 暂停1秒,保证上面的t4线程拿到的版本号也是1
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1);
			stamp = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 第二次取得的版本号为" + stamp);
			atomicStampedReference.compareAndSet(101, 100, stamp, stamp + 1);
			System.out.println(Thread.currentThread().getName() + "\t 第三次取得的版本号为" + atomicStampedReference.getStamp());
		}, "t3").start();

		new Thread(() -> {
			int stamp2 = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 第一次取得的版本号为" + stamp2);
			// 暂停3秒,保证上面的t3线程完成一次ABA操作
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			boolean isSuccess = atomicStampedReference.compareAndSet(100, 2019, stamp2, stamp2 + 1);
			stamp2 = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 是否修改成功:" + isSuccess + " 当前最新版本号:" + stamp2);
			System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值:" + atomicStampedReference.getReference());
		}, "t4").start();
	}
}

四、我们知道ArrayList是线程不安全,请编码写一个不安全的案例并给出解决方案

public class ContainerNotSafeDemo {
	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		for (int i = 0; i < 30; i++) {
			new Thread(() -> {
				list.add(UUID.randomUUID().toString().substring(0, 8));
				System.out.println(list);
			}, String.valueOf(i)).start();
		}
	}
}

以上代码会出现 java.util.ConcurrentModificationException 异常
    
导致原因:
    并发争抢修改导致,参考花名册签名情况,一个人正在写入,另一个同学过来争抢,导致数据不一致异常,并发修改异常。
    
解决方案:
    1. new Vector<>();替代
    2. Collections.syschronizedList(new ArrayList<>());替代
	3. new CopyOnWriteArrayList<>();替代
CopyOnWriteArrayList源码:
    
    
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

CopyOnWrite容器,即写时复制容器。往一个容器中添加元素时,不直接往当前容器Obejct[]添加,而是先将当前容器Object[]Copy复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素后,在将原容器的引用指向新的容器 setArray(newElements); 。 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

public class ContainerNotSafeDemo {
	public static void main(String[] args) {
		Set<String> set = new HashSet<>();
		for (int i = 0; i < 50; i++) {
			new Thread(() -> {
				set.add(UUID.randomUUID().toString().substring(0, 8));
				System.out.println(set);
			}, String.valueOf(i)).start();
		}
	}
}
以上代码会出现 java.util.ConcurrentModificationException 异常。
导致原因:
    并发争抢修改导致,参考花名册签名情况,一个人正在写入,另一个同学过来争抢,导致数据不一致异常,并发修改异常。
解决方案:
	1. Set<String> set = Collections.synchronizedSet(new HashSet<>());替代
    2. CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>(new HashSet<>());替代
public class ContainerNotSafeDemo {
	public static void main(String[] args) {
		Map<String, String> map = new HashMap<>();
		for (int i = 0; i < 50; i++) {
			new Thread(() -> {
				map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
				System.out.println(map);
			}, String.valueOf(i)).start();
		}
	}
}
以上代码会出现 java.util.ConcurrentModificationException 异常。
导致原因:
    并发争抢修改导致,参考花名册签名情况,一个人正在写入,另一个同学过来争抢,导致数据不一致异常,并发修改异常。
解决方案:
1.Map<String, String> map = Collections.synchronizedMap(new HashMap<>());替代
2. Map<String, String> map = new ConcurrentHashMap<>();替代

五、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁

1. 公平锁和非公平锁

1.1 是什么

公平锁:指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发情况下,有可能会造成优先级反转或饥饿现象。

1.2 两者区别

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。

关于两者的区别:

公平锁:就很公平,在并发环境下,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁就比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采取类似公平锁那种方式。

1.3 题外话

对Java的ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。

2. 可重入锁(又名递归锁)

2.1是什么

指的是同一线程外层函数获得锁后,内存递归函数任然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

2.2ReentrantLock/Synchronized就是一个典型的可重入锁
2.3可重入锁的作用是避免死锁
2.4ReentrantLockDemo
class Phone {
	public synchronized void sendSMS() {
		System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS");
		sendEmail();
	}

	public synchronized void sendEmail() {
		System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail");
	}
}
public class ReentrantLockDemo {
	public static void main(String[] args) {
		Phone phone = new Phone();
		new Thread(() -> {
			phone.sendSMS();
		}, "t1").start();

		new Thread(() -> {
			phone.sendSMS();
		}, "t2").start();
	}
}

以上代码执行结果:
t1	 invoked sendSMS
t1	 invoked sendEmail
t2	 invoked sendSMS
t2	 invoked sendEmail
    
解释:
    t1线程在sendSMS()这个外层方法获取锁的时候,t1在进入内层方法sendEmail()这个内层方法会自动获取锁。
package com.jujuxiaer.interview.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Phone implements Runnable {
	Lock lock = new ReentrantLock();

	@Override
	public void run() {
		sendSMS();
	}

	public void sendSMS() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS");
			sendEmail();
		} finally {
			lock.unlock();
		}
	}

	public void sendEmail() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail");
		} finally {
			lock.unlock();
		}
	}
}

public class ReentrantLockDemo {
	public static void main(String[] args) {
		Phone phone = new Phone();
		Thread t1 = new Thread(phone, "t1");
		Thread t2 = new Thread(phone, "t2");
		t1.start();
		t2.start();
	}
}

以上代码执行结果:
t1	 invoked sendSMS
t1	 invoked sendEmail
t2	 invoked sendSMS
t2	 invoked sendEmail
      
解释:
    public void sendSMS() {
		lock.lock();
		try {
            // 线程可以进入任何一个它所拥有的锁,所同步的代码块。  这个所同步的代码块指的就是  sendEmail();
			System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS");
			sendEmail();
		} finally {
			lock.unlock();
		}
	}

注意事项,lock.lock()和lock.unlock()的数量要一一配对,下面这种也可以:

	public void sendEmail() {
		lock.lock();
        lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail");
		} finally {
			lock.unlock();
            lock.unlock();
		}
	}

3. 自旋锁

指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗cpu。

package com.jujuxiaer.interview.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Jujuxiaer
 */
public class SpinLockDemo {
	// 原子引用线程
	AtomicReference<Thread> atomicReference = new AtomicReference<>();

	public void myLock() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + "\t come in");
		while (!atomicReference.compareAndSet(null, thread)) {

		}
	}

	public void myUnLock() {
		Thread thread = Thread.currentThread();
		atomicReference.compareAndSet(thread,null);
		System.out.println(thread.getName() + "\t invoked myUnLock()");
	}

	public static void main(String[] args) {
		SpinLockDemo spinLockDemo = new SpinLockDemo();
		new Thread(() -> {
			spinLockDemo.myLock();
			try {
				TimeUnit.SECONDS.sleep(5);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			spinLockDemo.myUnLock();
		}, "AA").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		new Thread(() -> {
			spinLockDemo.myLock();
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			spinLockDemo.myUnLock();
		}, "BB").start();
	}
}

执行结果:

AA come in
BB come in
AA invoked myUnLock()
BB invoked myUnLock()

4. 独占锁/共享锁

独占锁:指读锁一次只能被一个线程所持有的。对ReentrantLock和Synchronized而言都是独占锁。

共享锁:指该锁可被多个线程所持有。

对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。

package com.jujuxiaer.interview.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */
public class ReadWriteLockDemo {
	public static void main(String[] args) {
		MyCache myCache = new MyCache();
		for(int i = 0; i < 5; i++) {
			int finalI = i;
			new Thread(() -> {
					myCache.put(String.valueOf(finalI), finalI);
		    }, String.valueOf(i)).start();
		}

		for(int i = 0; i < 5; i++) {
			int finalI1 = i;
			new Thread(() -> {
				myCache.get(String.valueOf(finalI1));
			}, String.valueOf(i)).start();
		}
	}

}

class MyCache {
	private volatile Map<String, Object> map = new HashMap<>();

	public void put(String key, Object value) {
		System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		map.put(key, value);
		System.out.println(Thread.currentThread().getName() + "\t 写入完成");
	}

	public void get(String key) {
		System.out.println(Thread.currentThread().getName() + "\t 正在读取:" + key);
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		Object result = map.get(key);
		System.out.println(Thread.currentThread().getName() + "\t 读取完成: " + result);
	}
}

执行结果:

0 正在写入:0
3 正在写入:3
4 正在写入:4
2 正在写入:2
1 正在写入:1
0 正在读取:0
1 正在读取:1
2 正在读取:2
3 正在读取:3
4 正在读取:4
0 写入完成
0 读取完成: 0
4 读取完成: null
2 读取完成: null
3 读取完成: null
1 读取完成: null
2 写入完成
3 写入完成
1 写入完成
4 写入完成

从结果中可以看出来,写操作中间被读打断了。

加了读写锁,改进后的代码如下:

package com.jujuxiaer.interview.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author Jujuxiaer
 * 多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该可以同时进行。
 * 但是如果有个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。
 * 小总结:
 * 读-读能共存
 * 读-写不能共存
 * 写-写不能共存
 * <p>
 * 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
 */
public class ReadWriteLockDemo {
	public static void main(String[] args) {
		MyCache myCache = new MyCache();
		for(int i = 0; i < 5; i++) {
			int finalI = i;
			new Thread(() -> {
					myCache.put(String.valueOf(finalI), finalI);
		    }, String.valueOf(i)).start();
		}

		for(int i = 0; i < 5; i++) {
			int finalI1 = i;
			new Thread(() -> {
				myCache.get(String.valueOf(finalI1));
			}, String.valueOf(i)).start();
		}
	}

}

class MyCache {
	private volatile Map<String, Object> map = new HashMap<>();
	private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

	public void put(String key, Object value) {
		ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
		writeLock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			map.put(key, value);
			System.out.println(Thread.currentThread().getName() + "\t 写入完成");
		}catch (Exception e){
		    e.printStackTrace();
		}finally {
			writeLock.unlock();
		}
	}

	public void get(String key) {
		ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
		readLock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t 正在读取");
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			Object result = map.get(key);
			System.out.println(Thread.currentThread().getName() + "\t 读取完成: " + result);
		}catch (Exception e){
		    e.printStackTrace();
		}finally {
			readLock.unlock();
		}
	}

	public void clearCacheMap() {

	}
}

执行结果如下:

1 正在写入:1
1 写入完成
0 正在写入:0
0 写入完成
2 正在写入:2
2 写入完成
3 正在写入:3
3 写入完成
4 正在写入:4
4 写入完成
0 正在读取
1 正在读取
2 正在读取
3 正在读取
4 正在读取
0 读取完成: 0
1 读取完成: 1
2 读取完成: 2
4 读取完成: 4
3 读取完成: 3

可以观察到,写操作之间没有被打断,读操作可以并发

六、CountDownLatch/CyclicBarrier/Semaphore使用过吗

1. CountDownLatch

public class CountDownLatchDemo {
	public static void main(String[] args) {
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "\t 上晚自习,离开教室");
			}, String.valueOf(i)).start();
		}

		System.out.println(Thread.currentThread().getName() + " 班长最后关门走人");
	}
}

执行结果如下:

1 上晚自习,离开教室
5 上晚自习,离开教室
4 上晚自习,离开教室
main 班长最后关门走人
3 上晚自习,离开教室
2 上晚自习,离开教室
6 上晚自习,离开教室

从执行结果可以观察到,并不能保证班长等所有的同学走完才关门。

改进做法的代码如下:

import java.util.concurrent.CountDownLatch;

/**
 * @author Jujuxiaer
 */
public class CountDownLatchDemo {
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(6);
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "\t 上晚自习,离开教室");
				countDownLatch.countDown();
			}, String.valueOf(i)).start();
		}

		countDownLatch.await();
		System.out.println(Thread.currentThread().getName() + " 班长最后关门走人");
	}
}

执行结果如下:

1 上晚自习,离开教室
6 上晚自习,离开教室
4 上晚自习,离开教室
5 上晚自习,离开教室
3 上晚自习,离开教室
2 上晚自习,离开教室
main 班长最后关门走人

从执行结果可以观察到,能保证班长等所有的同学走完才关门。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo2 {
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(6);
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "国,被灭");
				countDownLatch.countDown();
			}, CountryEnum.forEachCountryEnum(i).getRetMessage()).start();
		}

		countDownLatch.await();
		System.out.println(Thread.currentThread().getName() + " 秦国统一天下");
	}
}

public enum CountryEnum {
	ONE(1, "齐"),
	TWO(2, "楚"),
	THREE(3, "燕"),
	FOUR(4, "韩"),
	FIVE(5, "赵"),
	SIX(6, "魏");

	private Integer retCode;
	private String retMessage;

	CountryEnum(Integer retCode, String retMessage) {
		this.retCode = retCode;
		this.retMessage = retMessage;
	}

	public Integer getRetCode() {
		return retCode;
	}

	public void setRetCode(Integer retCode) {
		this.retCode = retCode;
	}

	public String getRetMessage() {
		return retMessage;
	}

	public void setRetMessage(String retMessage) {
		this.retMessage = retMessage;
	}

	public static CountryEnum forEachCountryEnum(int index) {
		CountryEnum[] values = CountryEnum.values();
		for (CountryEnum value : values) {
			if (index == value.getRetCode()) {
				return value;
			}
		}
		return null;
	}
}

执行结果如下:

齐国,被灭
魏国,被灭
燕国,被灭
楚国,被灭
韩国,被灭
赵国,被灭
main 秦国统一天下

2. CyclicBarrier

CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author Jujuxiaer
 */
public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
			System.out.println("=====召唤神龙======");
		});

		for (int i = 1; i <= 7; i++) {
			int finalI = i;
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + " 收集到第" + finalI + "颗龙珠");
				try {
					cyclicBarrier.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					e.printStackTrace();
				}
			}, String.valueOf(i)).start();
		}
	}
}

执行结果如下:

1 收集到第1颗龙珠
7 收集到第7颗龙珠
5 收集到第5颗龙珠
4 收集到第4颗龙珠
3 收集到第3颗龙珠
2 收集到第2颗龙珠
6 收集到第6颗龙珠
=召唤神龙==

3. Semaphore

信号量主要勇于两个目的,一个是用于多个共享资源的互斥使用个,另一个用于并发线程数的控制。

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */
public class SemaphoreDemo {
	public static void main(String[] args) {

		// 模拟3个车位
		Semaphore semaphore = new Semaphore(3);
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				try {
					semaphore.acquire();
					System.out.println(Thread.currentThread().getName() +"\t抢到车位");
					// 停车3秒
					try {
						TimeUnit.SECONDS.sleep(3);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() +"\t停车3秒后离开车位");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					semaphore.release();
				}
			}, String.valueOf(i)).start();
		}
	}
}

执行结果如下:

1 抢到车位
2 抢到车位
3 抢到车位
2 停车3秒后离开车位
3 停车3秒后离开车位
4 抢到车位
5 抢到车位
1 停车3秒后离开车位
6 抢到车位
4 停车3秒后离开车位
6 停车3秒后离开车位
5 停车3秒后离开车位

七、阻塞队列知道吗?

1. 队列+阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起到的作用如下图所示:

image-20210911151908695

线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
  • 当阻塞队列是满时,从队列中添加元素的操作将会被阻塞。
  • 视图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他线程往空的对队列中插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程将会被阻塞,直到其他的线程从队列中移除一个或者多个元素或者完全清空队列后,才能新增。

2.为什么用?有什么好处?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

为什么需要BlockingQueue?好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。

在cuncurrent包发布以前,在多线程环境下,每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给程序员带来不小的复杂度。

3. BlockQueue的核心方法

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()不可用不可用

抛出异常:

  • 当阻塞队列满时,再往队列中add插入元素会抛IllegalStateException: Queue full
  • 当阻塞队列空时,再往队列中remo移除元素会抛NoSuchElementException

特殊值:

  • 插入方法,成功true,失败false。
  • 移除方法,成功返回出队列的元素,队列里没有就返回null

一直阻塞:

  • 当阻塞队列满时,生产者线程就绪往队列里put元素,队列会一直阻塞生产线程直到put数据或者响应中断退出。
  • 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

超时退出:

  • 当阻塞队列满时,队列会阻塞生产者线程一定时间,超时后,生产者线程会退出。

4. 架构梳理+种类分析

4.1 种类分析
  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */
public class SynchronousQueueDemo {
	public static void main(String[] args) {
		BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
		new Thread(() -> {
			try {
				System.out.println(Thread.currentThread().getName() + "\t put 1");
				blockingQueue.put("1");

				System.out.println(Thread.currentThread().getName() + "\t put 2");
				blockingQueue.put("2");

				System.out.println(Thread.currentThread().getName() + "\t put 3");
				blockingQueue.put("3");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "AAA").start();

		new Thread(() -> {
			try {
				try {
					TimeUnit.SECONDS.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "\t take " + blockingQueue.take());

				try {
					TimeUnit.SECONDS.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "\t take " + blockingQueue.take());

				try {
					TimeUnit.SECONDS.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "\t take " + blockingQueue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "BBB").start();
	}
}

执行结果如下:

AAA put 1
BBB take 1
AAA put 2
BBB take 2
AAA put 3
BBB take 3

5. 用在哪里

5.1 生产者消费者模式

传统的写法:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Jujuxiaer
 * @date 2021-09-11 16:35
 * 题目:一个初始值为0的变量,两个线程交替对其操作,一个加1,一个减1,来5轮
 * 1. 线程   操作方法   资源类
 * 2. 判断 --> 干活 --> 通知
 * 3. 防止虚假唤醒,用while,而不是if
 */
public class ProdConsumerTraditionDemo {
	public static void main(String[] args) {
		ShareData shareData = new ShareData();

		new Thread(() -> {
			for (int i = 1; i <= 5; i++) {
				try {
					shareData.increment();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "AAA").start();

		new Thread(() -> {
			for (int i = 1; i <= 5; i++) {
				try {
					shareData.decrement();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "BBB").start();
	}
}

/**
 * 资源类
 */
class ShareData {
	private int number = 0;
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	public void increment() throws InterruptedException {
		lock.lock();
		try {
			// 1. 判断
			while (number != 0) {
				// 等待,不能生产
				condition.await();
			}
			// 2. 干活
			number++;
			System.out.println(Thread.currentThread().getName() + "\t " + number);
			// 3. 通知唤醒
			condition.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void decrement() throws InterruptedException {
		lock.lock();
		try {
			// 1. 判断
			while (number == 0) {
				// 等待,不能生产
				condition.await();
			}
			// 2. 干活
			number--;
			System.out.println(Thread.currentThread().getName() + "\t " + number);
			// 3. 通知唤醒
			condition.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

上述代码执行结果:

AAA 1
BBB 0
AAA 1
BBB 0
AAA 1
BBB 0
AAA 1
BBB 0
AAA 1
BBB 0

5.1.1 **synchronized和Lock有什么区别?**用新的lock有什么好处?举例说明

1.原始构成

synchronized是关键字,属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调wait/notify方法)

2.使用方法

synchronized不需要用户手动去释放锁,当synchronized代码执行完成后,系统会自动让线程释放对锁的占用。ReentrantLock则需要用户手动释放锁,若没有主动释放锁,就有可能导致死锁。需要lock()和unlock()方法配合try/finally语句块来完成。

3.等待是否可中断

synchronized不可中断,除非抛出异常或者正常运行完成。ReentrantLock可中断,第一种中断方式:设置超时时间tryLock(long timeout, TimeUnit unit);第二种方式:lockInterruptibly()放代码块中,调用interrupt()方法可中断

4.加锁是否公平

synchronized非公平锁;ReentrantLock两者都可以,默认为公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁。

5.锁绑定多个条件Condition

synchronized没有;ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

题目:

多线程之间按顺序调用。实现 A --> B --> C三个县城启动,要求如下:

A打印5次,B打印10次,C打印15次。紧接着A打印5次,B打印10次,C打印15次 … 来10轮。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Jujuxiaer
 */
public class SyncAndReentrantLockDemo {
	public static void main(String[] args) {
		ShareResource shareResource = new ShareResource();
		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				shareResource.print5();
			}
		}, "A").start();

		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				shareResource.print10();
			}
		}, "B").start();

		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				shareResource.print15();
			}
		}, "C").start();
	}
}

class ShareResource {
	/**
	 * 标志位,number为1表示A线程;number为2表示B线程;number为3表示C线程;
	 */
	private int number = 1;
	private Lock lock = new ReentrantLock();
	private Condition conditionA = lock.newCondition();
	private Condition conditionB = lock.newCondition();
	private Condition conditionC = lock.newCondition();

	// 1. 判断
	public void print5() {
		lock.lock();
		try {
			while (number != 1) {
				conditionA.await();
			}
			// 2. 干活
			for (int i = 1; i <= 5; i++) {
				System.out.println(Thread.currentThread().getName()+"\t " + i);
			}
			// 3. 通知
			number = 2;
			conditionB.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	// 1. 判断
	public void print10() {
		lock.lock();
		try {
			while (number != 2) {
				conditionB.await();
			}
			// 2. 干活
			for (int i = 1; i <= 10; i++) {
				System.out.println(Thread.currentThread().getName()+"\t " + i);
			}
			// 3. 通知
			number = 3;
			conditionC.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	// 1. 判断
	public void print15() {
		lock.lock();
		try {
			while (number != 3) {
				conditionC.await();
			}
			// 2. 干活
			for (int i = 1; i <= 15; i++) {
				System.out.println(Thread.currentThread().getName()+"\t " + i);
			}
			// 3. 通知
			number = 1;
			conditionA.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

}
5.2 线程池
5.3 消息中间件
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Jujuxiaer
 */
public class ProdConsumerBlockingQueueDemo {
	public static void main(String[] args) {
		MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));

		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
			try {
				myResource.myProducer();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "Producer").start();

		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
			try {
				myResource.myConsumer();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "Consumer").start();

		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("5秒时间到,大老板main线程叫停,活动结束");
		myResource.stop();
	}
}

class MyResource {
	// 默认开启,进行生产 + 消费
	private volatile boolean flag = true;
	private AtomicInteger atomicInteger = new AtomicInteger();

	private BlockingQueue<Object> blockingQueue;

	public MyResource(BlockingQueue<Object> blockingQueue) {
		this.blockingQueue = blockingQueue;
		System.out.println(blockingQueue.getClass().getName());
	}

	public void myProducer() throws InterruptedException {
		String data;
		boolean isOffered;
		while (flag) {
			data = String.valueOf(atomicInteger.incrementAndGet());
			isOffered = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
			if (isOffered) {
				System.out.println(Thread.currentThread().getName() + "\t 插入队列数据:" + data + "成功");
			} else {
				System.out.println(Thread.currentThread().getName() + "\t 插入队列数据:" + data + "失败");
			}
			TimeUnit.SECONDS.sleep(1);
		}
		System.out.println();
		System.out.println(Thread.currentThread().getName() + "\t 此时flag==false了,表示大老板叫停了,生产动作结束。");
	}

	public void myConsumer() throws InterruptedException {
		String result;
		while (flag) {
			result = (String) blockingQueue.poll(2L, TimeUnit.SECONDS);
			if (result == null || "".equalsIgnoreCase(result)) {
				flag = false;
				System.out.println(Thread.currentThread().getName() + "\t 超过2秒没有取到数据,退出消费");
				return;
			}
			System.out.println(Thread.currentThread().getName() + "\t 消费队列成功,消费到的数据:" + result);
		}
	}

	public void stop() {
		this.flag = false;
	}
}

上述执行结果如下:

java.util.concurrent.ArrayBlockingQueue
Producer 生产线程启动
Consumer 生产线程启动
Producer 插入队列数据:1成功
Consumer 消费队列成功,消费到的数据:1
Consumer 消费队列成功,消费到的数据:2
Producer 插入队列数据:2成功
Consumer 消费队列成功,消费到的数据:3
Producer 插入队列数据:3成功
Producer 插入队列数据:4成功
Consumer 消费队列成功,消费到的数据:4
Producer 插入队列数据:5成功
Consumer 消费队列成功,消费到的数据:5
5秒时间到,大老板main线程叫停,活动结束

Producer 此时flag==false了,表示大老板叫停了,生产动作结束。
Consumer 超过2秒没有取到数据,退出消费

八、线程池用过吗?ThreadPoolExecutor谈谈你的理解?

1. 为什么用线程池,优势

线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。它的主要特点为:线程复用;控制最大并发数;管理线程

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配、调优和监控。

2. 线程池如何使用

2.1 架构说明

Java中线程池是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor这几个类。

image-20210911182800543
2.2 编码实现

了解即可的内容:

  • Executors.newScheduledThreadPool()

  • Executors.newWorkStealingPool(int),Java8新增的,使用目前机器上可用的处理器作为它的使用级别。

重点内容:

  • Executors.newFixedThreadPool(int)

    实行长期的任务,性能好很多

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

    主要特点如下:

    1. 采集一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    2. newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它LinkedBlockingQueue

  • Executors.newSingleThreadExecutor()

    一个任务一个任务执行的场景

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

    主要特点如下:

    1. 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
    2. newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue
  • Executors.newCacheThreadPool()

    使用于:执行很多短期异步的小程序或者负载较轻的服务器

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

    主要特点如下:

    1. 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    2. newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * @author Jujuxiaer
     */
    public class MyThreadPoolDemo {
    	public static void main(String[] args) {
    		ExecutorService threadPool = Executors.newFixedThreadPool(5);
    
    		try {
    			// 模拟10个用户来办理业务,每个用户就是一个来自外部 请求线程
    			for (int i = 0; i < 10; i++) {
    				threadPool.execute(() -> {
    					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
    				});
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			threadPool.shutdown();
    		}
    	}
    }
    

3. 线程池的几个重要参数介绍

7大参数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. corePoolSize:线程池中常驻核心线程数
    • 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为当日当值线程
    • 当线程池中的线程数量达到了corePoolSize后,就会把到达的任务放到缓存队列当中
  2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间。当前线程池线程数量超过corePollSize时,当空闲时间达到keepAliveTime值时,多余空闲线程就会被销毁,直到只剩下corePoolSize个线程为止。
    • 默认情况下,只有当前线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
  7. handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来

参考银行网点办理业务窗口例子理解7个参数:

image-20210911211857984

4. 说说线程池的底层工作原理

image-20210911212401523

线程池的主要处理流程图如下:

image-20210911213729337

  1. 在创建了线程池后,等待提交过来的任务请求

  2. 当调用过来executor()方法添加一个请求任务时,线程池会做如下判断:

    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程来运行这个任务
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
    • 如果这时候队列满了,且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程来立刻运行这个任务
    • 如果这时候队列满了,且正在运行的线程数量大于等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  3. 当一个线程完成任务是,它会从队列中取下一个任务来执行

  4. 当一个线程无事可做超过了一定的时间(keepAliveTime)时,线程池会判断:

    • 如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。所以线程池的所有任务完成后,其中的线程数量最终会收缩到corePoolSize的大小

九、线程池用过吗?生产上你如何设置合理参数

1. 线程池的拒绝策略你谈谈

1.1 是什么

等待队列已经满了,再也塞不下新任务了,并且线程池中的线程数量也达到了maximumPoolSize线程数,无法再继续为新任务服务了。这时候就需要拒绝策略机制合理处理这个问题。

1.2 JDK内置的拒绝策略(下面内置拒绝策略君实现了RejectedExecutionHandler接口)
  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
  2. CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会丢弃任务也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
  4. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

2. 单一的/固定数的/可变的三种创建线程池的方法,你在工作中使用哪种最多?超级大坑

答案是一个都不用。

生产上只能使用自定义的。JDK中Executors已经给你提供好了,为什么不用?

image-20210911220811718 image-20210911220856963

3. 你在工作中是如何使用线程池的,是否自定义过线程池使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author Jujuxiaer
 */
public class MyThreadPoolDemo2 {
	public static void main(String[] args) {
		ExecutorService threadPool = new ThreadPoolExecutor(2, 5,1L, TimeUnit.SECONDS,
				new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

		try {
			// 模拟10个用户来办理业务,每个用户就是一个来自外部 请求线程
			for (int i = 0; i < 9; i++) {
				threadPool.execute(() -> {
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			threadPool.shutdown();
		}
	}
}

4. 合理配置线程池你是如何考虑的

4.1 CPU密集型

获取CPU的核数,可以通过Runtime.getRuntime().availableProcessors()获取

CPU密集指的是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集型任务配置竟可能少的线程数量:

一般公式:CPU核心+1个线程的线程池

4.2 IO密集型

由于IO密集型任务线程并不是一直在执行任务,则应该配置尽可能多的线程,CPU核心数*2

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务导致浪费大量的CPU运算能力,浪费在等待上。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式: CPU核心 / (1 - 阻塞系数),阻塞系数在0.8 ~ 0.9 之间

比如8核CPU:8 / (1 - 0.9) = 80 个线程数

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值