并发编程七-并发相关的一些问题

目录

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给出提示并结束。


1,锁的分类?

锁的分类一般是依据锁的特性、锁的设计、锁的状态等进行分类的:

  • 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁多个线程会争抢锁,谁抢到就给谁,所以能会造成优先级反转或者饥饿现象;synchronized 就是非公平锁,ReentrantLock 通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁;
  • 独享锁/共享锁:独享锁是指该锁一次只能被一个线程持有,共享锁指该锁可以被多个线程持有;synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁;
  • 乐观锁/悲观锁:乐观锁认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,使用“数据版本机制”和“CAS操作”来实现。悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。
  • 偏向锁/轻量级锁/重量级锁:这种分类是按照锁状态来归纳的,并且是针对 synchronized 的,java 1.6 为了减少获取锁和释放锁带来的性能问题而引入的一种状态,其状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后无法降为偏向锁,这种升级无法降级的策略目的就是为了提高获得锁和释放锁的效率。
  • 互斥锁/读写锁:其实就是独享锁、共享锁的具体说法;互斥锁实质就是 ReentrantLock,读写锁实质就是 ReadWriteLock。
  • 可重入锁:指在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁,synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁。
  • 分段锁:实质是一种锁的设计策略,不是具体的锁,ConcurrentHashMap 在jdk1.8之前并发的实现就是通过分段锁的形式来实现高效并发操作;
  • 自旋锁:其实是相对于互斥锁的概念,互斥锁线程会进入 WAITING 状态和 RUNNABLE 状态的切换,涉及上下文切换、cpu 抢占等开销,自旋锁的线程一直是 RUNNABLE 状态的,一直在那循环检测锁标志位,机制不重复,但是自旋锁加锁全程消耗 cpu,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长。
  • 可中断锁:synchronized 是不可中断的,Lock 是可中断的,这里的可中断建立在阻塞等待中断,运行中是无法中断的。

 

2,锁的四种状态和升级过程?

锁的四种状态,一般是指synchronized锁的四种状态:无锁、偏向锁、轻量级锁和重量级锁。

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
  • 偏向锁:当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
  • 轻量级锁:当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
  • 重量级锁:当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

 

3,线程池的7个参数?

  • 参数一:corePoolSize:线程池核心线程大小,线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
  • 参数二:maximumPoolSize 线程池最大线程数量,一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
  • 参数三:keepAliveTime 空闲线程存活时间,一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
  • 参数四:unit 空间线程存活时间单位,keepAliveTime的计量单位。
  • 参数五:workQueue 任务队列,用于保存等待执行的任务的阻塞队列。新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。 

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。 

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

  • 参数六:threadFactory 线程工厂,创建新线程时使用的工厂。
  • 参数七:handler 拒绝策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

AbortPolicy:直接抛出异常。 

CallerRunsPolicy:只用调用者所在线程来运行任务。 

DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 

DiscardPolicy:不处理,丢弃掉。 

 

 

4,线程池的设计里体现了什么设计模式?

享元模式:在软件开发过程中,如果我们需要重复使用某个对象的时候,先把对象创建好,在需要使用该对象的时候直接获取该对象,不需要重复创建,让对象实现共享。

 

5,线程池的线程数怎么设置比较好?

分CPU密集型和IO密集型

  • CPU密集型的意思就是该任务需要大量运算,而没有阻塞,CPU一直全速运行,一般来说配置(CPU核数+1)个线程的线程池比较合适。
  • IO密集型,即该任务需要大量的IO,会有大量的阻塞,导致浪费大量的CPU运算能力浪费在等待上,所以可以尽量多一些线程来执行IO密集型的任务,一般有两种配置方式。第一种:CPU核数 * 2+1 个线程;第二种:CPU核数 / (1 – 阻塞系数)

但具体需要配置多少个线程,需要进行实际测试进行调整。

 

6,说说ThredLocal?

ThreadLocal是为了解决多线程中相同变量的访问冲突问题,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。

 

7,CAS是什么?

CAS全称是Compare and Swap,即比较并交换的意思,其原理是通过cpu的原子指令来实现原子操作。将获取存储在内存地址的原值和指定的内存地址进行比较,只有当他们相等时,交换指定的预期值和内存中的值,若不相等,则重新获取存储在内存地址的原值,这整个过程是原子操作。

 

8,CAS的ABA问题怎么解决?

CAS操作是在更新值的时候检查下原值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,后来变成了B,然后又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

这个问题看业务场景,如果是对于一个基本数据类型,CAS操作的中发生了ABA操作,但是业务没有影响,那么这个问题就不算问题;但是如果是需要感知数据的每一次改变,那么就需要解决ABA问题。

解决方式:给变量加上版本号,那么每次变量更新的时候把版本号加一,那么A->B->A 就会变成A1->B2->A3.

jdk提供了AtomicStampedReference类来解决ABA问题。
 

9,除了CAS、原子类、synchronized、Lock之外还有什么线程安全的方式?

使用final关键字,final修饰的字段在所有线程中是属于不可变(基本类型值不可变,引用类型是引用地址不可变)。还有一点,在对象完全初始化之后,线程才能看到对该对象的引用,这样就可以保证看到该对象的final字段的正确初始化值。

 

10,说说AQS的实现原理?

使用自旋CAS+队列+LockSupport.park()来实现。

用 volatile 修饰的整数类型的 state 状态,用于表示同步状态,提供 getState 和 setState, compareAndSetState来操作同步状态;
提供了一个 FIFO 等待队列,实现线程间的竞争和等待,这是 AQS 的核心;其中, 链表头Head和链表尾Tail也有volatile修饰。
AQS 内部提供了各种基于 CAS 原子操作方法,如 compareAndSetState 方法,并且提供了锁操作的acquire和release方法。
 

11,为什么说AQS的底层是CAS+volatile

表示锁状态的变量state,以及FIFO队列的头,尾,节点的状态都是volatile修饰的

在设置state,队列的头,尾,状态的时候都有用到CAS技术

具体参考:https://blog.csdn.net/baidu_32689899/article/details/106746253

 

12,JUC包中同步组件主要实现了AQS的哪些主要方法?

tryAcquire, tryRelease:在获取锁/释放锁的方法中调用

 tryAcquireShared, tryReleaseShared:在获取共享锁/释放共享锁的方法中调用

 isHeldExclusively:查询当前线程是否持有此锁。

 

13,说说volatile的可见性和禁止指令重排序是怎么实现的?

volatile关键字会多一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障:它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他CPU中对应的缓存行无效。

 

14,对象头具体包括什么?

三部分: Mark Word、指向类的指针、数组长度(只有数组对象才有)

Mark Word记录了对象和锁有关的信息;

 

15,synchronized和ReentrantLock的底层实现和重入锁的底层原理?

从可重入锁的实现原理说起:每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态,如果锁状态是0,代表该锁没有被占用,如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法。

synchronized的线程id和状态是记录在锁对象的对象头中的,在同一个对象中的synchronized方法,锁对象都是this或者class文件,如果在子类中调用了父类的方法,因为调用者是子类对象,所以锁对象也是子类对象或者子类的Class文件。

对于ReentrantLock来说,它使用state来标示锁的状态,获取锁后+1,记录获取锁的线程,当该线程再次需要获取锁的时候,会判断当前锁被哪个线程持有,如果是当前自己这个线程,则会把state再次+1。

 

16,什么叫阻塞队列的有界和无界?

有界和无界指的是队列的容量,有界队列当队列容量达到指定容量时put 操作会阻塞,无界队列put操作永远都不会阻塞,队列的容量限制来源于系统资源的限制。

常见的有界队列有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等

常见的无界队列有:ConcurrentLinkedQueue、PriorityBlockingQueue、DelayedQueue、LinkedTransferQueue等

 

17,PriorityQueue底层是什么?初始容量是多少?扩容方式?

  • 底层是什么:
    /**
     * 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%。

 

 

18,你知道跳表吗?什么场景会用到?

跳表底层基于链表结构,在链表的基础上加了多层索引结构。ConcurrentSkipListMap就是使用跳表来实现的一个可排序的支持并发的Map集合。跳表是可以实现二分查找的有序链表,对于需要排序并且要求高性能查找元素的场景就可以使用跳表结构。

 

19,LinkedTransferQueue和SynchronousQueue有什么区别?

  • SynchronousQueue内部的队列只能存放一个元素,在存放第二个元素的时候会阻塞,需要等队列中的那个元素被取出之后才能放入队列。
  • LinkedTransferQueue内部的队列可以存放多个元素,并且如果有等待的线程,可以直接将元素 “交给” 等待者,在没有消费者线程等待的时候才会进入队列。

 

20,实现一个容器,提供两个方法:add,size;写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。

用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();


    }
}

 

 

你可能感兴趣的:(并发编程)