JavaSE面试要点六——并发编程(有序性、可见性、原子性、线程池)

多线程

     Java语言是支持多线程的,多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动状态。实现多线程的方式主要有三种:继承Thread类、实现Runable接口、继承Callable接口。

并发与并行

     多线程共享数据
并发:说的是在一个时间段内,多件事情交替执行,宏观上的同时执行。
并行:说的是多个事情在同一时间段内同时执行,微观上的同时执行。

     并发编程是在很多线程对共享资源进行访问时,需要通过控制,让多个线程并发的对共享数据进行访问。

多线程的本质问题

由于CPU、内存、硬盘三者之间的读写速度不一样。所以导致

  1. 多核CPu,每个内核中都有一层高速缓存。每个高速缓存中存储的数据不可见(可见性
  2. 线程中有IO操作,耗时较长,操作系统需要切换线程执行(原子性
  3. 操作系统对指令进行优化,打乱了指令的执行顺序(有序性

volatile——解决可见性和有序性

     volatile修饰变量,它保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变 量的值,这新值对其他线程来说是立即可见的。还禁止进行指令重排序。 但是volatile不能解决原子性问题。

volatile底层实现原理

使用 Memory Barrier 内存屏障 ,禁止在该条指令执行前插入其他指令,在工作内存修改后,结合缓存一致性协议,将工作内存数据更新到主内存,其他工作内存读取更新。

内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指 令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其 后面的指令移到内存屏障指令之前

有序性:

有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定 的内存屏障来禁止指令重排序来保障有序性的。

可见性:

可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现 的。对 volatiile 修饰的变量执行写操作时,JVM
会发送一个 Lock 前缀指令给 CPU,CPU 在执行完写操作后,会立即将新值刷新到内存,同时因为 MESI 缓 存一致性协议,其他各个
CPU 都会对总线嗅探,看自己本地缓存中的数据是否 被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU
里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见 性

锁和原子变量——解决原子性

原子变量——CAS

CAS 比较并交换 ,是一种乐观锁(没有加锁)的实现,采用自旋的思想比较。内部有3个值:V A B

VAB

  1. V:内存值, 操作前先将内存值读到工作内存
  2. A:预期值, 在工作内存修改了变量值后,将要将修改后的值向主内存写入的时候再次读取的主内存数据
  3. B: 内部操作后的变量值

当向主内存写入数据时,必须满足 A==V, 就V=B, 否则就再次读入主内存值

private  static AtomicInteger atomicInteger = new AtomicInteger(0);
    private volatile static int num=0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(){
                @Override
                public void run() {
                    System.out.println(atomicInteger.incrementAndGet());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程"+(num++));
                }
            }.start();
        }
    }

CAS的缺点

     首先CAS是无锁的,采用自旋的方式,线程不会阻塞,如果有大量的线程不断的自旋去尝试,那么CPU消耗较大,所以CAS思想适合低并发情况

     ABA问题: 就内存值由A变为B, 再由B改为A, CAS不知道内存值已经发生过修改的。


     如何解决?添加版本号

通过使用类添加版本号,来避免 ABA 问题。如原先 的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改 为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。

Java中的锁

不完全是锁,有的是锁状态,有的是锁的特性

  1. 乐观锁 采用CAS机制,乐观认为不加锁是没有问题的. 原子类
  2. 悲观锁 就是真正的加锁实现,认为不加锁是会有问题的.
  3. 可重入锁 又名递归锁,当一个线程进行入外层方法获取锁时,如果内存调用另一个需要获得该锁修饰的方法,那么线程是可以进入的,synchronized就是一个可重入锁。
  4. 分段锁 不是具体的锁, 将锁的粒度分的更小,以提高并发效率.
  5. 自旋锁 不断重试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是耗cpu.
  6. 读写锁 里面维护两个锁实现, 一个是写锁,一个读锁。如果使用的是写锁,一次只能有一个线程获得锁,如果是读锁,那么可以允许多个线程获得。写锁的优先级高于读锁
  7. 独占锁 / 共享锁 类似于读写锁。
  8. **公平锁 ** 可以按照请求的顺序分配锁, ReentrantLock中有公平锁实现,里面维护一个队列,按顺序排队获取锁。
  9. 非公平锁 不按照请求顺序分配锁,synchroized就是非公的,ReentrantLock中默认使用非公平锁,非公平锁的线程进来后会先尝试获取锁的状态,如果是0,那么将其++,进入代码块中,其他线程进入阻塞队列中等待。

synchronized中锁的状态

锁的状态是通过对象监视器在对象头中的字段来表明的。

  1. 无锁状态: 没有加锁
  2. 偏向锁: 只有一段线程访问同步代码,此时会将线程的id存入对象头中,下次线程来获取锁时直接分配即可。
  3. 轻量级锁: 当锁的状态为偏向锁时,又有线程访问,那么锁的状态会升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获取锁,以提高效率。
  4. 重量级锁: 当锁的状态为轻量级锁时,如果线程数量太多,线程自旋数达到一定数量,锁会升级为重量级锁,线程进入阻塞状态,有操作系统调度分配。

对象结构(记录锁)

对象在内存中的布局分为三块区域:对象头、实例 数据和对齐填充
在这里插入图片描述

synchronized需要一个对象,在对象头中记录有没有使用锁

锁的实现

AQS(代码层面)

     AQS是juc( java.util.concurrent      java并发包)中实现线程安全的核心组件,是从java代码级别实现.
     内部维护了一个锁状态(0没有,>0有锁)volatile state。 内部还维护一个队列,保存未获取到锁的线程.

多个线程来访问,如果有一个线程访问到了state,就将其改为1,其他线程获取失败后,就会添加到队列中 Node(Thread)

ReentrantLock锁的实现

     AQS是JUC实现线程安全的核心组件,是Java代码级别中的核心组件,内部维护了一个锁状态(由volatile修饰的state,所以他是可见和有序的) 还维护了一个阻塞队列 保存未获取到锁的线程。多个线程访问,如果有一个线程访问到state,就将其改为1(可见性),其他线程就会进入队列中等待(Node(Thread))。

ReentrantLock它实现了Lock接口。

public class ReentrantLock implements Lock, java.io.Serializable{ }

    ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来 对共享资源进行同步。支持可重入锁。其源码有3个内部类,

Sync extends AQS
FairSync extends Sync 公平实现
NonFairSync extends Sync 非公平实现
在这里插入图片描述

     分别是SyncNoFairSyncFairSync。NoFairSync继承了Sync,采用非公平的策略获取锁。FairSync 类也继承了 Sync 类,表示采用公平策略获取锁。

NoFairSync 非公平锁(默认的锁)
NonFairSync   extends   Sync     非公平实现
final void lock() {
            if (compareAndSetState(0, 1))没有排队,直接尝试去获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//获取锁,表示一定能获取锁,获取不到就继续
 }

点进acquire()方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这是对源码的翻译
在这里插入图片描述

FairSync公平锁

而公平锁就比较老实,线程进来先排队,不然不予受理。

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */

在这里插入图片描述

synchronized锁的实现(指令级)

     synchronized锁主要依靠底层指令实现,它是关键字,可以修饰方法。代码块,一次只允许一个线程进入。

     指令级别实现: 如果synchronized修饰方法,在编译后的指令中添加ACC_SYNCHRONIZED表示此方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1,方法运行结束,或者出现异常,锁标志-1synchroniezd如果修饰代码块: 在进入代码前加入monitorenter指令 ,对象锁标志+1, 同步代码块结束/或者出现异常执行monitorexit,锁标志-1
在这里插入图片描述

JUC常用类(线程安全且高效的集合)

ConcurrentHashMap (线程安全且高效的HashMap)

     首先,HashMap是线程不安全的JDK1.8之前,HashMap在进行扩容时采用的是头插法,链表转移后,前后链表顺序倒置(头插法导致),在转移过程中修改了原来链表中节点的引用关系,导致链表结点互相引用,即形成了环, 这种情况下,当我们使用get曹操获取到环形链表处的数据,就会发生死循环。
     在JDK1.8之后,高并发的情况下,比如现在有两个线程都要调用put方法,都进行了判断,且都满足条件可以直接插入,这时线程1先插入,线程2在执行的时候就不会再次进行判断,也是直接插入,这就出现了元素覆盖,也就是说线程1做了无用功。这时候就是报错!
     在JDK1.5没有引入ConcurrentHashMap的时候,HashTable是线程安全的hashmap,但是HashTable的线程安全非常的存粹。他给每个HashMap的方法都加入了synchronized锁 但是hahsmap通过扰动函数后n-1&hash计算的索引值,其hash碰撞的概率已经非常小的,所以他们在同一索引位置的情况并不高,但是由于synchronized在高并发情况下会阻塞其他所有线程,所有效率会变得很低很低。

     所以在JDK1.5后,java引入了ConcurrentHashMap的线程安全且高效的hashmap,使用cas+synchronized的机制,实现高效率。源码:

public V put(K key, V value) {
        return putVal(key, value, false);
    }

可以看出,ConcurrentHashMap不是在put方法加synchronized,而是吧每个数组的位置看做独立区间

if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }

putVal()中先进行判断,如果计算出的索引位置没有没有任何元素,它会采用CAS机制将改元素添加到第一个位置。

else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }

如果计算的索引位置有元素,那么将会使用当前链表(红黑树)的头结点作为锁的标记对象,在经过hash碰撞确认不是重复元素后,这些线程进入阻塞状态,然后依次添加。
在这里插入图片描述

     ConcurrentHashMap 不支持存储 null 键和 null 值。为消除歧义,是因为无法区分是拿到的索引位置为null,还是拿不到值返回的为null

CopyOnWriteArrayList / CopyOnWriteSet

     同HashMap一样,ArrayList是一样线程不安全的,但是Vector也是给每一个方法都加入了synchronized,包括add()、get()。这就导致很多有线程在写值的时候,其他线程不能读,而且一般情况下,读操作比写操作多,这就导致效率低下。

     CopyOnWriteArrayList给add加入了锁,但是在写数据时,先会创建一个新的副本,将新元素写入到新数组中,写完后再将新的数组赋给底层原来数组的引用。读没有做任何控制。
     CopyOnWriteArraySet底层就是CopyOnWriteArrayList,不同的就是在添加是判断元素是否重复。

线程池

池? 线程池 & 数据库池?
     为什么要有数据库池: 每次链接数据库都要创建数据库;链接对象,用完后销毁,太麻烦。但如果事先创建出一些链接对象放入池子中,每次使用时从池子获取,用完后返回到池子中,这样效率就会大大提升。

     线程池: JDK5后提供ThreadPoolExecutor类事先线程池的创建(推荐)。
线程池的优点:

  1. 重复利用线程。
  2. 统一管理。
  3. 提高响应速度。

ThreadPoolExecutor

七个参数

corePoolSize 核心线程池大小
maximumPoolSize 线程池最大线程数
keepAliveTime 非核心线程池无任务时存活时间
unit 存活时间的单位
workQueue 阻塞队列 核心线程池满了后先进入阻塞队列
thredFactory 线程工厂,用于创建线程
hander 拒绝策略

流程

在这里插入图片描述

线程池中的阻塞队列

    SynchronousQueue:同步队列是一个容量只有 1 的队列,这个队列比较特殊, 它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务,每个 put 必须等待一个 take。
    ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
    LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列, 最大长度为 Integer.MAX_VALUE;

线程池中的拒绝策略

    构造方法的中最后的参数 RejectedExecutionHandler 用于指定线程池的拒绝 策略。当请求任务不断的过来,而系统此时又处理不过来的时候,我们就需要采 取对应的策略是拒绝服务。

  1. AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。
  2. CallerRunsPolicy 策略:只要线程池未关闭,该策略在调用者线程中运行当前的
    任务(如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务)。
  3. DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执 行的任务,并尝试再次提交当前任务。
  4. DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理。
public static void main(String[] args) {
        //创建线程池                                 7
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
                                          5, 200,
                                                         TimeUnit.MILLISECONDS,
                                                         new ArrayBlockingQueue<>(2),
                                                         Executors.defaultThreadFactory(),
                                                         new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i=1;i<=10;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池
            //Future<?> submit = executor.submit(myTask);
        }
        executor.shutdown();

        
    }

在这里插入图片描述

提交与关闭任务

  1. 提交任务
    exccute()不需要返回值
    submit()需要返回值
  2. 关闭线程池:
    shutdownNow是全部interrupt,停止执行,包括未执行的线程,全部取消。
    shutdown:没有执行完的线程全部执行玩,然后停止,期间不接受新任务。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值