目录
1,锁的分类?
2,锁的四种状态和升级过程?
3,线程池的7个参数?
4,线程池的设计里体现了什么设计模式?
5,线程池的线程数怎么设置比较好?
6,说说ThredLocal?
7,CAS是什么?
8,CAS的ABA问题怎么解决?
9,除了CAS、原子类、synchronized、Lock之外还有什么线程安全的方式?
10,说说AQS的实现原理?
11,为什么说AQS的底层是CAS+volatile
12,JUC包中同步组件主要实现了AQS的哪些主要方法?
13,说说volatile的可见性和禁止指令重排序是怎么实现的?
14,对象头具体包括什么?
15,synchronized和ReentrantLock的底层实现和重入锁的底层原理?
16,什么叫阻塞队列的有界和无界?
17,PriorityQueue底层是什么?初始容量是多少?扩容方式?
18,你知道跳表吗?什么场景会用到?
19,LinkedTransferQueue和SynchronousQueue有什么区别?
20,实现一个容器,提供两个方法:add,size;写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。
锁的分类一般是依据锁的特性、锁的设计、锁的状态等进行分类的:
锁的四种状态,一般是指synchronized锁的四种状态:无锁、偏向锁、轻量级锁和重量级锁。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
享元模式:在软件开发过程中,如果我们需要重复使用某个对象的时候,先把对象创建好,在需要使用该对象的时候直接获取该对象,不需要重复创建,让对象实现共享。
分CPU密集型和IO密集型
但具体需要配置多少个线程,需要进行实际测试进行调整。
ThreadLocal是为了解决多线程中相同变量的访问冲突问题,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
CAS全称是Compare and Swap,即比较并交换的意思,其原理是通过cpu的原子指令来实现原子操作。将获取存储在内存地址的原值和指定的内存地址进行比较,只有当他们相等时,交换指定的预期值和内存中的值,若不相等,则重新获取存储在内存地址的原值,这整个过程是原子操作。
CAS操作是在更新值的时候检查下原值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,后来变成了B,然后又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
这个问题看业务场景,如果是对于一个基本数据类型,CAS操作的中发生了ABA操作,但是业务没有影响,那么这个问题就不算问题;但是如果是需要感知数据的每一次改变,那么就需要解决ABA问题。
解决方式:给变量加上版本号,那么每次变量更新的时候把版本号加一,那么A->B->A 就会变成A1->B2->A3.
jdk提供了AtomicStampedReference类来解决ABA问题。
使用final关键字,final修饰的字段在所有线程中是属于不可变(基本类型值不可变,引用类型是引用地址不可变)。还有一点,在对象完全初始化之后,线程才能看到对该对象的引用,这样就可以保证看到该对象的final字段的正确初始化值。
使用自旋CAS+队列+LockSupport.park()来实现。
用 volatile 修饰的整数类型的 state 状态,用于表示同步状态,提供 getState 和 setState, compareAndSetState来操作同步状态;
提供了一个 FIFO 等待队列,实现线程间的竞争和等待,这是 AQS 的核心;其中, 链表头Head和链表尾Tail也有volatile修饰。
AQS 内部提供了各种基于 CAS 原子操作方法,如 compareAndSetState 方法,并且提供了锁操作的acquire和release方法。
表示锁状态的变量state,以及FIFO队列的头,尾,节点的状态都是volatile修饰的
在设置state,队列的头,尾,状态的时候都有用到CAS技术
具体参考:https://blog.csdn.net/baidu_32689899/article/details/106746253
tryAcquire, tryRelease:在获取锁/释放锁的方法中调用
tryAcquireShared, tryReleaseShared:在获取共享锁/释放共享锁的方法中调用
isHeldExclusively:查询当前线程是否持有此锁。
volatile关键字会多一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障:它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他CPU中对应的缓存行无效。
三部分: Mark Word、指向类的指针、数组长度(只有数组对象才有)
Mark Word记录了对象和锁有关的信息;
从可重入锁的实现原理说起:每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态,如果锁状态是0,代表该锁没有被占用,如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法。
synchronized的线程id和状态是记录在锁对象的对象头中的,在同一个对象中的synchronized方法,锁对象都是this或者class文件,如果在子类中调用了父类的方法,因为调用者是子类对象,所以锁对象也是子类对象或者子类的Class文件。
对于ReentrantLock来说,它使用state来标示锁的状态,获取锁后+1,记录获取锁的线程,当该线程再次需要获取锁的时候,会判断当前锁被哪个线程持有,如果是当前自己这个线程,则会把state再次+1。
有界和无界指的是队列的容量,有界队列当队列容量达到指定容量时put 操作会阻塞,无界队列put操作永远都不会阻塞,队列的容量限制来源于系统资源的限制。
常见的有界队列有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等
常见的无界队列有:ConcurrentLinkedQueue、PriorityBlockingQueue、DelayedQueue、LinkedTransferQueue等
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
翻译:优先级队列表示为一个平衡的二进制堆:队列[n]的两个子队列为队列[2*n+1]和
队列[2*(n+1)]。
如果comparator为null,则优先级队列按比较器或元素的自然顺序排序:对于堆中的每个节点n和n的每个后代d, n <= d。
*/
transient Object[] queue; // non-private to simplify nested class access
底层是个数组,一个实现了排序的数组。
看源码得知,如果不指定容量,则使用默认的容量大小
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
源码中默认的容量大小为11,所以初始容量为11。
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
容量在小于64的时候,以原容量乘2再加2的方式扩容。
容量在大于等于64的时候,则在原容量的大小上增加50%。
跳表底层基于链表结构,在链表的基础上加了多层索引结构。ConcurrentSkipListMap就是使用跳表来实现的一个可排序的支持并发的Map集合。跳表是可以实现二分查找的有序链表,对于需要排序并且要求高性能查找元素的场景就可以使用跳表结构。
用wait和notify实现:线程2启动后就等待,线程1启动后添加了5个元素后唤醒线程2然后让自己等待。此时线程2被唤醒后给出提示然后唤醒线程1,线程2执行结束。
使用LockSupport实现:与wait和notify实现相同。
使用ReentrantLock实现:与wait和notify实现相同。
使用CountDownLatch实现:创建一个CountDownLatch(1)对象,让线程2等待(await()),线程1添加满5个元素的时候调用countDown()让线程2得以执行并发出提示。如果还需要限制线程2在给出提示之前线程1需要等待的话,可以使用两个CountDownLatch对象来实现。
使用while来实现:看代码
public class NonLockTest {
private static List list = new ArrayList<>();
private static volatile int state = 0;
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 10; i++) {
while (state==1){
// 空转等待
}
list.add("xxx");
int size = list.size();
System.out.println("线程1添加了一个元素,当前元素个数:"+size);
if(size==5){
state++;
}
}
},"Thread-1").start();
new Thread(()->{
while (state==0){
// 空转等待
}
System.out.println("容器中已添加了5个元素--by Thread-2");
state--;
},"Thread-2").start();
}
}