八股之并发编程

面试题:

线程有哪些状态?

Java中分成6种状态(原代码里写的):

新建状态New:创建线程
可运行状态Runnable:线程调用start()后进入运行状态,操作系统交给cpu去执行。
终结状态Terminated:代码执行完毕进入终结状态。
阻塞状态Blocked:可运行状态的线程获取锁失败了进入阻塞状态,当持锁线程释放锁后,让其余线程竞争锁,阻塞的线程成功获得锁之后再次进入可运行状态。
等待状态Waiting:可运行状态的线程获得锁但是条件不满足,调用wait()方法释放锁进入等待状态,等条件满足时由其他线程调用notify()方法唤醒等待状态的线程后,线程重新争抢锁,抢到了再进入可运行状态,没抢到就是blocked状态。
超时等待Timed_Waiting:和等待状态不一样的是wait()方法有时间参数,醒来的条件不一样,时间到或者被线程notify()唤醒都会醒来,然后醒着接着抢锁 / 调用sleep()和锁,是否满足条件没关系。
在这里插入图片描述
5种状态(操作系统层面):
在这里插入图片描述
Java中的Runnable包含就绪,运行还有阻塞I/O

ThreadPoolExecutor线程池的核心参数

核心线程:执行完任务依旧需要保存在线程池。
救急线程:执行完任务不需要保存在线程池中。

7个重要参数:
corePoolSize 核心线程数目:最多保留的线程数
maximumPoolSize 最大线程数目:核心线程+救急线程<=最大线程数目

keepAliveTime 生存时间:针对救急线程
unit 时间单位:针对救急线程

workQueue :阻塞队列,缓冲任务——核心线程和workQueue都有上限,如果都满了再来任务就要创建救急线程来做任务了。

threadFactory 线程工厂:起名

handler 拒绝策略:核心线程和workQueue都有上限,而且救急线程也到上限了(二者的和<=maximumPoolSize),又来任务了怎么处理?
1.AbortPolicy移除策略:线程池资源耗尽了,所以抛出异常。
2.CallerRunsPolicy:哪个线程用线程池submit的就自己做一下这个任务。
3.DiscardPolicy:默默直接丢弃,也没有异常,忙不过来了。
4.DiscardOldestPolicy:把任务队列里等待最久的任务丢了,把新的任务放进队列。

在这里插入图片描述

Wait和Sleep的区别

共同点
wait(),wait(时间)和sleep(时间)的都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
不同点
1.方法归属(谁的方法):sleep是线程Thread的方法,wait是Object的成员方法。
2.醒来时间(醒来的方式):
sleep(时间)和wait(时间)都会在等待时间到后醒来;wait()和wait(时间)都可以被notify()唤醒,wait()不唤醒就会一直等待;sleep和wait都可以被打断唤醒。
3.锁的特性不同(是否获取和释放锁):
wait要先获取wait对象的锁,sleep不需要;wait执行后会释放锁,允许其他线程获得对象锁;synchronized代码块中如果执行sleep,不会释放锁。

lock VS synchronized

1.语法:
synchronized是关键字,用c++实现;lock是接口,用java实现;使用synchronized时退出同步代码块锁自动释放,而使用lock需要程序员手动unlock()释放锁;
2.功能:
相同:都是悲观锁,都有互斥,同步,锁重入(重复加锁)功能。
不同:lock可获取等待状态(哪些线程在等待),公平锁(先到先得,构造时参数为true),可打断,可超时(去获取锁),多条件变量(多个等待队列,new Condition());lock接口有多个不同场景实现。

注意:
用了lock()但是没抢到锁在blockedQueue里,抢到锁了但是条件不满足使用条件变量c.await()释放锁后在等待队列c中,条件满足时使用c.signal()或者c.signalAll()回到阻塞队列尾部;
如果用tryLock()无参方法,底层总是用到非公平锁。

3.性能:
没有竞争时,synchronized做了优化,如偏向锁,轻量级锁,性能不错;
竞争激烈时,Lock会提供更好的性能。

volatile能否保证线程安全?

volatile可以保证可见行和有序性,但是不能保证原子性。

线程安全的三个方面:
1.可见性:一个线程修改共享变量,另一个线程可以看到最新的结果。
2.有序性:一个线程内代码按编写顺序执行(不会指令重排)
3.原子性:一个线程内多行代码以一个整体运行,期间不能有其余线程代码插队。

可见性问题:
由于共享变量最新值获取失败导致某线程中代码陷入死循环,一般都认为是因为每个线程有自己的共享线程副本,更新后拿的是副本的值而不是内存的值所导致的可见性问题——然而不是共享变量副本所造成的,因为更新之后再开线程能够取到内存最新值。

原因是因为JIT,java即时编译,会陷入死循环的代码很明显运行非常频繁,JIT会自动将热点代码优化,比如stop一直读的都是false,他就直接把代码while(!stop)改成while(!false)来优化效率,但是这就会导致即使stop更新该线程也拿不到更新的true来结束循环。

只要不用JIT,如果只用解释器 || 将死循环的代码执行次数减少,JIT认为不是那么频繁没必要优化 就可以看到效果了。
但是禁用JIT优化或者缩短时间都会降低性能,只为一个变量读取最新值没有必要,所以根本解决办法是使用volatile修饰变量,这样JIT就会放过代码,不对它进行优化

有序性:
因为编译时优化可能导致指令重排,所以会存在代码执行顺序和实际写代码的顺序不一样。

volatille可以禁止指令重排序。
volatile使用内存屏障避免指令重排,对volatile的变量进行写操作时,如下所示,要写你先写,我写就别干扰我。对volatile的变量进行读操作时,在volatile修饰的变量下面有读屏障,volatile修饰的变量先进行读操作。——被volatile修饰的变量:先读后写
注意屏障是单向的。
在这里插入图片描述
在这里插入图片描述

原子性问题:一些看起来是原子操作的代码实际在字节码中不是原子操作。

t2代码插队:volatile是在读取的时候拿到最新值,以下这种情况已经读取赋值过了就察觉不到值已经更新了。
在这里插入图片描述
可以用synchronized或者加锁保证原子性。

悲观锁和乐观锁:

悲观锁:线程只有占有锁后才能操作共享变量。每次只有一个线程占锁成功,没抢到锁的线程都要停下等待(可运行状态->阻塞状态)
线程从运行到阻塞再到唤醒——牵涉到线程的上下文切换,频繁发生影响性能。
悲观锁代表:synchronized和Lock锁,实际中synchronized和Lock都做了优化,会多尝试几次来减少阻塞的机会。

乐观锁:不需要加锁,每次只有一个线程能成功修改共享变量,其他失败的线程也不停止,不断重试直到成功。
线程一直运行所以不用上下文切换,但是需要多核cpu线程支持且线程数不能超过cpu的核树。
乐观锁代表:AtomicInteger,使用cas(比较并交换)保证原子性。其底层是Unsafe。

举例加深理解:

	static final Unsafe u = Unsafe.getUnsafe();
	static final long BALANCE = u.objectFieldOffset(Account.class,"balance");
	static class Account{
		volatile int balance = 10;
	}
	public static void main(String args[]){
		Account a = new Account();
		int o = a.balance;	//旧值
		int n = o+5;	//新值
		//如果旧值o和BALANCE不一样证明有线程改过了,那么就会修改失败。
		System.out.println(Unsafe.compareAndSetInt(account,BALANCE,o,n));
		System.out.println(account.balance);
	}

可以优化成以下代码:

	public static void main(String args[]){
		Account a = new Account();
		while(true){
			int o = a.balance;	//旧值
			int n = o+5;	//新值
			//修改成功就退出,没改成功就继续循环进行更改
			if(Unsafe.compareAndSetInt(account,BALANCE,o,n)){
				break;
			}
		}
	}

HashTable vs ConcurrentHashMap

相同:都是线程安全的Map集合。
不同:
HashTable并发度低,整个HashTable一把锁,同一时刻只能有一个线程操作它。
ConcurrentHashMap:在1.8之前,数据结构为segment+数组+链表,每个segment一把锁,多个线程访问不同segment,不会发生冲突。
1.8之后,底层数据结构为:数组+链表/红黑树,将数组的链表/树的每个头节点作为锁,多个线程访问的头节点不同,不会冲突。因此数组容量决定其并发度。

加深ConcurrenHashMap的理解:
1.7的ConcurrentHashMap的索引计算:
二次哈希值最后m位代表是存放在segment的数组中的哪个位置,根据segment的数量是2的x次幂选择二次哈希值的高x位,计算其值得到该放在哪个segment中。

如何扩容?
segment数量=并发度,此后segment数量固定不会再变动。
segment的数组大小 = capacity/clever = 容量/并发度,扩容是扩大了segment中数组的容量,每次容量变成原来的2倍。

segment[0]已经有数组了,作用是为后面的segment提供原型,后面segment的小数组会和segment[0]的大小和扩容因子都copy走,也是设计模式中的原型模式的一种实现。

1.8的ConcurrentHashMap和1.7的不同:
底层数据结构:1.7是segment+数组+链表,1.8是数组+链表/红黑树。
初始化时机:1.7 是饿汉式,开始就有了;1.8是懒汉式,直到put第一个元素创建底层数据结构。
扩容时机:1.7是超过扩容因子*容量就扩容,1.8是到了就扩容。——都是扩容2倍。

1.8的ConcurrentHashMap中,capacity不再是数组长度,而是假设要放进去的元素数量。
factor扩容因子只在初始化数组时有效,后面还是按照0.75的扩容因子计算是否扩容。

扩容时出现其他线程想要get或者put怎么办?
get看链表头,如果是forwardingNode,证明数据已经迁移走了,应该去新数组找数据,如果不是证明还没有处理,直接就数组继续get就可以。
如果要找的元素在迁徙的头节点后面,链表迁移的时候重新创建节点,不然可能会出现指向改变的情况,也做了优化:如果迁移之后顺序没变就不用动,变了才重新创建对象。
put的三种情况:
在这里插入图片描述

对ThreadLocal的理解?

1.实现线程间资源对象的线程隔离,每个线程使用各自的资源对象,避免争抢造成的线程安全问题。(局部变量也可以实现线程隔离,但是不能跨越方法)。
2.实现了线程内资源共享

ThreadLocal的原理:
每个线程有自己的ThreadLocalMap的成员变量,用来存储资源对象。
1.调用set():ThreadLocal作为key,资源对象做value,放入当前的ThreadLocalMap集合中。
2.调用get():ThreadLocal作为key,在当前线程中查找关联的资源值。
3.调用remove():ThreadLocal做key,移除当前线程关联的资源值。

冲突会使用开放寻址法,找下一个空闲的位置。

ThreadLocalMap释放时机:不放可能出现内存泄露问题。
1.ThreadLocalMap 的key是弱引用,因为Thread可能需要长时间运行,如果key不使用了的话,内存不足的时候虚拟机的垃圾回收机制可以释放其占用的内存。
2.ThreadLocalMap的value是强引用,会根据key = null ?来释放值的内存。时机有:
    2.1  get()获取key发现key = null,会把值也置为null。
在这里插入图片描述
    2.2 set key时,启发式扫描,会清理附近的null key。元素个数多,扫描的范围就大一些。多发现null key就多扫描几次。
在这里插入图片描述
在这里插入图片描述
    2.3 remove()时:一般使用ThreadLocal都是作为静态变量,因此GC无法回收。静态变量一直对ThreadLocal保持强引用,因此自己手动清理。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值