3. Interview-JUC

Catalog

  • 1 线程池原理
    • 1.1 ThreadPoolExecutor构造器
    • 1.2 拒绝策略
    • 1.3 线程池工作过程
    • 1.4 ExectorService四种线程池
    • 1.5 BlockingQueue
  • 2 Java阻塞队列原理
    • 2.1 阻塞队列的主要方法
    • 2.2 Java常用阻塞队列
  • 3 synchronized原理
    • 3.1 synchronized特点
    • 3.2 synchronized作用范围
    • 3.3 synchronized核心组件
    • 3.4 synchronized实现原理
    • 3.5 synchronized底层实现原理
    • 3.6 synchronized锁升级过程
    • 3.7 锁优化
  • 4 synchronized和ReetrantLock性能比较?synchronized和CAS呢?
  • 5 ReentrantLock
    • 5.1 ReetrantLock特点
    • 5.2 ReetrantLock常用方法
    • 5.3 ReetrantLock & synchronized
    • 5.4 lock & tryLock & lockInterruptibly
    • 5.5 Condition类和Object类锁方法的区别
  • 6 AQS
    • 6.1 AQS特点
    • 6.2 AQS原理
    • 6.3 自定义同步器
  • 7 线程池的线程数一般设置多少?
  • 8 volatile关键字解决了什么问题?
    • 8.1 volatile特点
    • 8.2 DCL需要volatile吗?
    • 8.3 volatile底层实现(JVM:内存屏障,CPU:lock指令)
    • 8.4 volatile代码示例
  • 9 atomic解决什么问题?原理是什么?
  • 10 CAS
  • 11 ThreadLocal原理
  • 12 CountDownLatch原理
  • 13 CyclicBarrier
  • 14 Semaphore信号量
  • 15 CountDownLatch&CyclicBarrier&Semaphore区别?
  • 16 Java中如何保证线程安全性?
  • 17 Java有哪些锁?
    • 17.1 公平锁/非公平锁
    • 17.2 乐观锁/悲观锁
    • 17.3 独享锁/共享锁
    • 17.4 互斥锁/读写锁
    • 17.5 可重入锁/不可重入锁
    • 17.6 自旋锁/自适应自旋锁
    • 17.7 偏向锁/轻量级锁/重量级锁
    • 17.8 分段锁
  • 18 Java多线程同步的7种方法
    • 18.1 synchronized
    • 18.2 wait与notify
    • 18.3 volatile
    • 18.4 Lock
    • 18.5 ThreadLocal
    • 18.6 Atomic
    • 18.7 使用阻塞队列实现线程同步
  • 19 为什么wait()和notify()属于Object类?
  • 20 什么是可重入锁和不可重入锁?
  • 21 sleep与wait区别?
  • 22 start与run区别?
  • 23 Java后台线程 / 守护线程
  • 24 线程 & 进程 & 协程 & 超线程
  • 25 创建线程的四种方式
  • 26 Thread的五种状态
  • 27 Thread的优先级
  • 28 Thread类常用方法
  • 29 synchronized方法可以调用普通方法吗,反之呢?
  • 30 读写锁ReadWriteLock
  • 31 线程上下文切换
  • 32 死锁
  • 33 多线程之间怎么共享数据?
  • 34 进程调度算法
  • 35 Timer和ExcutorService有什么区别?
  • 36 synchronized括号里面怎么填?直接new一个对象行不行?
  • 37 线程安全的实现方法?
  • 38 线程的实现方式?
  • 39 非公平锁为什么性能比公平锁高,两者有啥区别?各自怎么实现?
  • 40 线程不安全会有什么问题?
  • 41 什么场景下用newFix,什么场景用newCache?满了会怎么样?
  • 42 并行与并发
  • 43 缓存一致性协议&缓存行
JUC知识图谱
大厂面试题

1 线程池原理

Executor继承图谱

1.1 ThreadPoolExecutor构造器

ThreadPoolExecutor执行流程
 public ThreadPoolExecutor(int corePoolSize, // 1
                              int maximumPoolSize,  // 2
                              long keepAliveTime,  // 3
                              TimeUnit unit,  // 4
                              BlockingQueue workQueue, // 5
                              ThreadFactory threadFactory,  // 6
                              RejectedExecutionHandler handler ) { //7
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. corePoolSize:线程池中的线程数量
  2. maximumPoolSize:线程池中的最大线程数量
  3. keepAliveTime:当前线程池中的线程数>corePoolSize,多余的空闲线程存活时间
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列
  6. threadFactory:线程工厂,用于创建线程
  7. handler:线程的拒绝策略

1.2 拒绝策略

线程池中线程用完了,同时等待队列也满了,这时候拒绝策略就起作用了。常用的线程池拒绝策略有如下:

系统内置拒绝策略

  • AbortPolicy
    直接抛出异常,程序报错
  • DiscardPolicy
    直接丢弃任务,不予处理
  • DiscardOldestPolicy
    丢弃最老的一个请求
  • CallerRunsPolicy
    强制塞进去处理,线程池性能下降

自定义拒绝策略

  • 实现RejectedExecutionHandler接口

1.3 线程池工作过程

  • 线程池刚创建,里面没有线程,任务队列作为参数传进来,即使队列里面有任务,也不会马上执行。
  • 调用execute()方法添加任务,线程池会做以下判断:
    • 正在运行线程数 < corePoolSize,马上创建线程执行任务
    • 正在运行线程数 >= corePoolSize,将任务放入队列
    • 如果队列满了,且正在运行线程数 < maximumPoolSize,创建非核心线程立刻执行任务
    • 如果队列满了,且正在运行线程数 >= maximumPoolSize,线程池抛出异常RejectedExecutionException
  • 当一个线程完成任务,会从线程池取下一个任务执行
  • 当一个线程空闲并且超过keepAliveTime时,线程池就会判断,如果当前运行线程数大于corePoolSize,该空闲线程就会被销毁。
  • 线程池所有任务完成后,线程池最终会收缩到corePoolSize的大小。

1.4 ExectorService四种线程池

  • newFixedThreadPool
    • 使用LinkedBlockingQueue
  • newCachedThreadPool
    • 缓存、无界,最大Integer.MAX_VALUE
    • 使用SynchronousQueue
  • newSingleThreadPool,
  • newScheduleThreadPool
    • 内部使用DelayedWorkQueue

1.5 BlockingQueue

  • 直接提交SynchronousQueue
    • 无缓冲队列,类似直接提交
  • 无界队列LinkedBlockingQueue
    • 基于链表,FIFO
    • 不指定容量,最大Integer.MAX_VALUE
  • 有界队列ArrayBlockingQueue
    • 基于数组,FIFO
    • 默认非公平,可以指定公平性
  • 优先级队列PriorityBlockingQueue
    • 无界队列,按照优先级出队,不是FIFO
  • 延时队列DelayQueue
    • 无界队列,只有当指定延迟时间到了才从队列中取元素
    • 插入数据永远不会阻塞,获取数据才会阻塞

2 Java阻塞队列原理

Java阻塞队列家族

2.1 阻塞队列的主要方法

阻塞队列主要方法

2.2 Java常用阻塞队列

  • ArrayBlockingQueue:数组结构组成的有界阻塞队列
    • FIFO,先进先出的顺序
    • 默认是不公平的,可以修改构造器参数改为公平的阻塞队列
  • LinkedBlockingQueue:链表结构组成的有界阻塞队列
    • 生产者端和消费者端两把独立锁提高并发度
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
    • 默认是按照自然顺序升序,可通过修改compareTo()修改排序规则
  • DelayQueue:使用优先级队列实现的无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列
  • LinkedTransferQueue:链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:链表结构组成的双向阻塞队列

3 synchronized原理

3.1 synchronized特点

  • 任意非null对象都可以加锁
  • 独占式的悲观锁,同时也属于可重入锁

3.2 synchronized作用范围

  • 作用于方法(对象锁)
    锁住的是对象的实例(this)
  • 作用于静态方法(类锁/全局锁)
    锁住的是Class实例,因为Class的相关数据存储在永久代PermGen(JDK8是metaspace),永久代是全局共享的,因此synchronized的静态方法锁相当于类的全局锁,会锁住所有调用该方法的线程。
  • 作用于对象实例
    锁住的是所有以该对象为锁的代码块。

3.3 synchronized核心组件

  • Wait Set
    调用wait()被阻塞的线程放在这里
  • Contention List
    竞争队列,所有请求锁的线程首先放到这里
  • Entry List
    Contention List中那些有资格成为候选资源的线程被移动到Entry List
  • OnDeck
    任意时刻,最多只有一个线程在竞争锁资源,该线程成为OnDeck
  • Owner
    已经获得锁资源的线程
  • !Owner
    当前释放锁的线程

3.4 synchronized实现原理

synchronized实现原理
  • 所有请求锁资源的线程都会进入Contention List,JVM每次从队列的尾部取,为了降低对尾部元素的竞争,JVM会将一部分线程移动到Entry List中
  • Owner线程会在unlock时,将Contention List中的部分线程移动到Entry List中,并指定Entry List的某个线程为OnDeck线程,一般是最先进去的那个线程。
  • Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利给OnDeck线程,OnDeck线程需要重新竞争锁,牺牲了公平性,提高了系统的吞吐量,JVM中这种选择行为成为“竞争切换”。
  • Owner线程被wait()阻塞会进入Wait Set队列,直到某个时刻通过notify()或notifyAll()唤醒后,重新进入Entry List队列。
  • 处于Contention List、Entry List、Wait Set中的线程都处于阻塞状态,该阻塞操作是由操作系统完成的(Linux下由pthread_mutex_lock内核函数实现)。
  • synchronized是非公平锁
    • synchronized在线程进入Contention List时,等待的线程会先尝试自旋获取锁,如果获取不到就进入Contention List,这对已经进入队列的线程是不公平的;
    • 自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源,这也是不公平的。
  • synchronized是一个重量级操作,需要调用操作系统相关接口,性能很低,JDK6做了很多优化,有自适应自旋、锁消除、锁优化等,JDK7/8引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要操作系统加锁。
  • JDK6默认开始偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking禁用偏向锁。
  • 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这个升级过程叫做锁膨胀。

3.5 synchronized底层实现原理

  • 代码层面:synchronized
  • monitorenter、moniterexit
  • 执行过程中锁自动升级
  • lock comxchg,锁北桥芯片,不是锁总线

3.6 synchronized锁升级过程

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

32位锁升级

synchronized锁升级-32位

64位锁升级

synchronized锁升级-64位

偏向锁

偏向锁获取流程

自旋锁

自旋超过10次,或者等待线程超过1/2,升级为重量级锁

自旋
自旋锁

轻量级锁

轻量级锁获取流程
偏向锁&轻量级锁获取流程
synchronized锁升级过程

3.7 锁优化

  • 减少锁持有时间
  • 减少锁粒度
    最经典案例是ConcurrentHashMap
  • 锁分离
    比如ReadWriteLock,读读不互斥、读写互斥、写写互斥
  • 锁粗化
    每个线程持有锁时间尽量短,用完立即释放资源,但是凡事有个度,如果对同一个锁不停地请求、同步和释放,反而会消耗很多资源,不利于性能提升。
  • 锁消除
    锁消除是编译器级别的事情,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是编码不规范引起的。
  • 自旋锁与自适应自旋
    自选次数默认是10次,可以使用参数-XX:PreBlockSpin来更改。
  • 偏向锁
    • 用参数-XX:+UseBiasedLocking开启偏向锁,JDK1.6默认开启,可以使用-XX:-UseBiasedLocking来禁止偏向锁。
    • 偏向模式:虚拟机会把对象头中的标志位设为01
  • 轻量级锁
    • 轻量级锁的标志位为00,重量级锁的标志位为10
锁消除&锁粗化

4 synchronized和ReetrantLock性能比较?synchronized和CAS呢?

synchronized&ReentrantLock

  • synchronized依赖底层操作系统实现,实现比较重,性能方向不如lock,这是JDK1.6之前;
  • JDK1.6之后synchronized做了很多优化提升性能:
    • 自旋锁
    • 适应性自旋锁
    • 锁消除
    • 锁粗化
    • 偏向锁
    • 轻量级锁
  • 如果竞争资源不激烈,两者的性能是差不多的,而竞争资源非常激烈是(既有大量线程同时竞争),此时lock的性能要远远优于synchronized。

synchronized&CAS

  • 竞争激烈,synchronized性能高于CAS
  • 低竞争,CAS性能高于synchronized
  • synchronized升级到重量级锁后的等待队列不消耗CPU,CAS等待期间消耗CPU

5 ReentrantLock

5.1 ReetrantLock特点

  • ReetrantLock继承自Lock,能完成synchronized能完成的所有工作。在此基础上还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
  • ReetrantLock是一种可重入锁。

5.2 ReetrantLock常用方法

  • lock:加锁

  • unlock:释放锁

  • tryLock:加锁

    • lock是一定要获取到锁,如果锁不可用,则一直等待,在未得到锁之前,当前线程不继续向下执行。
    • tryLock是先尝试获取锁,如果锁可用,获得到锁;如果不可用,当前线程可以继续向下执行。
    • tryLock(long timeout, TimeUnit unit):如果锁在给定等待时候没有被另一个线程保持,也获取该锁。
  • isLock:判断锁是否被任意线程占用

  • isFair:判断是否是公平锁,ReetrantLock默认是非公平锁,可通过构造器指定。

    • 公平锁:Lock lock = new ReetrantLock(true)
    • 非公平锁:Lock lock = new ReetrantLock(false)
  • lockInterruptibly:如果当前线程未被中断,获取锁

5.3 ReetrantLock & synchronized

相同点

  • 都是协调多线程对共享变量、对象的访问问题
  • 都是可重入锁,同一线程可以多次获得锁
  • 都保证了可见性和互斥性

不同点

  • ReetrantLock是API级别的,synchronized是JVM级别的
  • synchronized通过JVM自动加锁释放锁,ReetrantLock要显示的手动的加锁和释放锁,加锁后要在finally中通过unlock()手动释放锁;
  • ReetrantLock相比synchronized的优势是,可响应中断、公平锁、多个锁。
  • synchronized是非公平锁,ReetrantLock默认是非公平锁,可通过构造器参数变为公平锁
  • 底层实现不一样,synchronized的实现依赖于底层的监视器锁monitor,监视器锁又依赖于底层操作系统的Mutex Lock,操作系统上下文切换成本很高,synchronized是同步阻塞,是悲观并发策略;ReetrantLock是同步非阻塞,是乐观并发策略。
  • synchronized发生异常时会自动释放线程持有的锁,不会发生死锁;ReetrantLock发生异常需要手动在finally中unlock()释放锁,如果没手动显示释放锁,可能发生死锁。
  • ReetrantLock可以让等待的线程响应中断,synchronized不能够响应中断,等待的线程会一直等待下去。

5.4 lock & tryLock & lockInterruptibly

  • lock能获得锁返回true得到锁,不能就一直等待,当前线程不能继续向下执行;
  • tryLock能获得锁就返回true得到锁,不能就立即返回false,当前线程可以继续向下执行;
  • tryLock(long timeout, TimeUnit unit),超过等待时间还没获得锁,返回false;
  • lock和lockInterruptibly,两个线程分别执行这两个方法,此时中断线程,lock不会抛出异常,lockInterruptibly会抛出异常。

5.5 Condition类和Object类锁方法的区别

  • Condition类的await方法 = Object.wait()
  • Condition.signal() = Object.notify()
  • Condition.signalAll() = Object.notifyAll()
  • ReetrantLock类可以唤醒指定条件的线程,Object的唤醒是随机的

6 AQS

AQS原理

6.1 AQS特点

  • AQS,AbstractQueueSynchronizer,抽象的队列同步器,AQS定义了一套多线程访问共享资源的同步器框架。很多同步器都基于此实现,如ReetrantLock、CountDownLatch、Semaphore
  • AQS定义了三种资源共享方式:
    • 独占式Exclusive,如ReetrantLock
    • 共享式Share,如CountDownLatch、Semaphore
    • 独占式和共享式,如ReentrantReadWriteLock

6.2 AQS原理

  • 以ReentrantLock为例
    • state=0,初始化状态
    • A线程调用tryAcquire()独占锁并且state+1
    • 其他线程再尝试获取锁就失败了,直到A线程unlock()释放,state=0为止
    • 在A线程释放之前,A线程可以重复获取该锁,state会累加,这就是可重入的概念。
  • 以CountDownLatch为例
    • 初始化state=n,等于线程数,每次调用countDown(),state会CAS减1,等所有子线程执行结束state=0,然后主线程调用await()继续其他操作。

6.3 自定义同步器

7 线程池的线程数一般设置多少?

线程数计算公式

  • 公式一:线程数=CPU核心数CPU使用率(1+w/c),来源于《Java并发编程实战》
    • CPU使用率为0~1
    • w/c,阻塞时间/计算时间
  • 公式二:线程数=CPU核心数/1-阻塞系数
    • 阻塞系数为01,一般为0.80.9
    • 令公式一=公式二得出,阻塞系数=w/w+c。阻塞时间/阻塞时间+计算时间,公式一和公式二其实是同一公式

线程数设置结论

  • CPU密集型/计算密集型,线程数=CPU核心数+1
  • IO密集型,线程数=2*CPU核心数+1
  • CPU核心数=Runtime.getRuntime().avaiableProcessors()

区分CPU密集型和IO密集型

  • CPU密集型就是计算密集型,需要进行大量计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等;
  • IO密集型,涉及到网络、磁盘IO等,磁盘IO的速度远远低于CPU和内存,大部分时间都在等IO操作完成,大部分任务都是IO密集型任务,比如web应用。

8 volatile关键字解决了什么问题?

8.1 volatile特点

  • volatile关键字可以保证可见性,当多个线程操作共享数据时,可以保证内存中的数据可见
  • volatile和synchronized都能保证可见性,但是synchronized是一种重量级的互斥锁,volatile是一种较synchronized轻量级的"无锁"同步策略
  • volatile不具备互斥性,不能保证变量的原子性操作
  • volatile的底层原理是内存屏障或者内存栅栏,通常情况下为了提高性能,编译器和处理器通常会进行指令重排序,分为编译器重排序和处理器重排序,指令重排序对单线程没影响,但是会影响多线程处理的正确性,加入volatile关键字就相当于多出一个lock前缀指令,就相当于内存屏障,就是一组处理指令,用来实现对内存操作的顺序限制。
  • volatile经常用到的两个场景
    • 状态标记量
    • double check

8.2 DCL需要volatile吗?

DCL+volatile

8.3 volatile底层实现(JVM:内存屏障,CPU:lock指令)

  • 写屏障
    • StoreStoreBarrier
    • StoreLoadBarrier
  • 读屏障
    • LoadLoadBarrier
    • LoadStoreBarrier
JSR内存屏障
JVM层面volatile实现
HotSpot实现volatile

8.4 volatile代码示例

package com.crt.java.multithread;

/**
 * volatile关键字:
 *
 * 1. 可以保证可见性,当多个线程操作共享数据时,可以保证内存中的数据可见。
 * 2. volatile和synchronized都能保证可见性,但是synchronized是一种重量级的互斥锁,volatile是一种较synchronized轻量级的"无锁"同步策略
 * 3. volatile不具备互斥性,不能保证变量的原子性操作
 *
 * Created by lhr on 2018/12/28.
 */
public class TestVolatile {
    public static void main(String[] args) {
        VolatileThreadDemo volatileThreadDemo = new VolatileThreadDemo();
        new Thread(volatileThreadDemo).start();
        while (true) {
//            synchronized (volatileThreadDemo) {
                if (volatileThreadDemo.isFlag()) {
                    System.out.println("*************");
                    break;
//                }
            }
        }
    }
}

class VolatileThreadDemo implements Runnable {

    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

9 atomic解决什么问题?原理是什么?

  • volatile解决了多线程操作共享数据时候的内存可见性问题,但是对于i++这种原子操作问题要借助atomic来解决
  • atomic的核心是CAS,Compare and Set,有三个操作数,变量的内存制V(value),变量的当前预期值E(except),变量想要更新的新值U(update),if(V == E) 则V == U
  • atomic核心类中的核心方法都会调用unsafe类的几个本地方法,unsafe类是非安全的,在sun.misc.Unsafe包下,包含了大量的对C代码的操作。按照操作的数据类型主要分为四类:
    • 线程安全的基本数据类型的原子性操作:AtomicInteger、AtomicLong、AtomicBoolean
    • 线程安全的数组类型的原子性操作:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
    • 基于反射原理对象中的基本类型的线程安全的原子性操作: AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
    • 线程安全的引用类型和防止ABA问题的引用类型的原子操作:AtomicReference、AtomICMarketableReference、AtomicStampedReference
  • AtomicInteger源码分析
    • volatile修饰的成员变量
private volatile int value;
  • 有参构造器:
public AtomicInteger(int initialValue) { 
  value = initialValue;
}
  • compareAndSet方法(value的值通过内部this和valueOffset传递),最核心的CAS操作
public final boolean compareAndSet(int expect, int update) {
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • getAndSet方法,在该方法中调用了compareAndSet方法
public final int getAndSet(int newValue) {
    for (;;) {
      int current = get();
      if (compareAndSet(current, newValue))
        return current;
    }
}
  • i++的实现
public final int getAndIncrement() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return current;
    }
}
  • ++i的实现
public final int incrementAndGet() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return next;
    }
}
  • ABA问题
    • 做i++操作的时候,i的值可能经历了多次变化,只是最终变回了初值,考虑使用版本号的思想,乐观锁的思想解决
    • atomic包中有现成的类解决此问题,AtomicMarkableReference,AtomicStampedReference,做CAS比较时候增加两个参数,当前的戳值,预期的戳值,对于AtomicMarkableReference而言,戳值是一个布尔类型的变量,而AtomicStampedReference中戳值是一个整型变量。

atomic示例代码

package com.crt.java.multithread;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by za-lihaoran on 2019/1/15.
 */
public class TestAtomic {

    public static void main(String[] args) {
        AtomicThread thread = new AtomicThread();
        for (int j = 0; j < 10; j++) {
            new Thread(thread).start();
        }
    }
}

class AtomicThread implements Runnable {
//    private int i = 0;  // 加上volatile也解决不了i++原子性问题
    private AtomicInteger i = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("i++ = " + getI());
    }

    public int getI() {
//        return i++;
        return i.getAndIncrement();
    }
}

10 CAS

10.1 CAS原理

CAS
  • CAS,compare and swap,比较和交换,乐观锁的机制,非阻塞算法的一种实现。
  • 当且仅当内存值=旧值时,内存值=新值

10.2 原子包java.util.concurrent.atomic(锁自旋)

  • atomic包提供了一组原子类,可解决i++原子性问题
  • 多线程同时执行这些原子类的实例的方法时,当一个线程进入方法,其他线程就像自旋锁一样,一直等待该方法执行完成,才由JVM从队列中选取另一个线程进入方法。
  • getAndIncrement采用了CAS操作,compareAndSet利用JNI来完成CPU指令的操作。

10.3 ABA问题

  • 做i++操作的时候,i的值可能经历了多次变化,只是最终变回了初值,考虑使用版本号的思想,乐观锁的思想解决
    • atomic包中有现成的类解决此问题,AtomicMarkableReference,AtomicStampedReference,做CAS比较时候增加两个参数,当前的戳值,预期的戳值,对于AtomicMarkableReference而言,戳值是一个布尔类型的变量,而AtomicStampedReference中戳值是一个整型变量。

10.4 模拟CAS算法

  • CAS算法的定义
    CAS是单词compare and set的缩写,意思是指在set之前先比较该值有没有变化,只有在没变的情况下才对其赋值。
  • CAS算法的原理(读-改-写)
步骤 1.读旧值(即从系统内存中读取所要使用的变量的值,例如:读取变量i的值)

步骤2.求新值(即对从内存中读取的值进行操作,但是操作后不修改内存中变量的值,例如:i=i+1,这一步只进行 i+1,没有赋值,不对内存中的i进行修改)

步骤3.两个不可分割的原子操作

第一步:比较内存中变量现在的值与 最开始读的旧值是否相同(即从内存中重新读取i的值,与一开始读取的 i进行比较)

第二步:如果这两个值相同的话,则将求得的新值写入内存中(即:i=i+1,更改内存中的i的值)

如果这两个值不相同的话,则重复步骤1开始
注:这两个操作是不可分割的原子操作,必须两个同时完成

10.5 CAS的缺陷

  • ABA问题
    • JDK解决方案:AtomIcStampedReference
    • 解决方案原理:版本号
  • 循环时间开销大
    • 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
  • 只能保证一个共享变量的原子操作
    • 解决方案一:多个共享变量合并为一个
    • 解决方案二:加锁

11 ThreadLocal原理

12 CountDownLatch原理

  • CountDownLatch可以实现线程计数器的功能
  • 应用场景:线程A等待其他N个线程执行完后才能执行
  • 实现示例:
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){public void run() {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
};}.start();
new Thread(){ public void run() {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
};}.start();
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
}

13 CyclicBarrier

  • 回环栅栏,一组线程等待至barrier状态再全部同时执行。
  • 线程调用await()之后就进入barrier状态
  • CyclicBarrier最重要的就是await(),有两种重载方法:
    • public int await(),一直等,等到都到了barrier状态再执行后续任务
    • public int await(long timeout, TimeUnit unit):等到一定时间,还有线程没达到barrier,让到了barrier的执行后续任务
  • CyclicBarrier是可重用的,示例如下:
public static void main(String[] args) { int N = 4; CyclicBarrier barrier = new CyclicBarrier(N); for(int i=0;i

14 Semaphore信号量

14.1 Semaphore作用

  • Semaphore是一种基于计数的信号量,可以设定一个阈值,多线程竞争获取许可,做完自己事情后归还,超过阈值的将会被阻塞。
  • Semaphore常用于构建线程池、对象池、资源池等。
  • 计数器为1的Semaphore称为互斥锁,又叫二元信号量,表示两种互斥状态。

14.2 Semaphore常用方法

  • 阻塞方法:

    • public void acquire():获得一个许可,若无许可获得,则一直等待,直到获得许可
    • public void acquire(int n):获得n个许可
    • public void release():释放许可,在释放许可之前,必须先获得许可
    • public void release(int n):释放n个许可
  • 非阻塞方法:

    • public boolean tryAcquire():尝试获取一个许可,若成功则true,失败立即false
    • public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取一个许可,若在指定时间内获取成功则true,否则立即false
    • public boolean tryAcquire(int n):尝试获取n个许可,若成功则true,失败立即false
    • public boolean tryAcquire(int n, long timeout, TimeUnit unit):尝试获取n个许可,若在指定时间内获取成功则true,否则立即false
    • availablePermits():获取可用的许可数目

14.3 Semaphore使用场景

5台机器8个工人,一台机器只能1个工人使用,只有使用完了,其他工人才能继续使用。

int N = 8; //工人数 Semaphore semaphore = new Semaphore(5); //机器数目 for(int i=0;i

14.4 Semaphore & ReetrantLock

  • Semaphore基本能完成ReetrantLock的所有工作,方法也类似;
  • Semaphore.acquire()默认为可响应中断锁,跟ReetrantLock.lockInterruptibly()作用类似,在等待临界资源的过程中可以被Thread.interrupt()中断;
  • Semaphore也提供了公平锁与非公平锁机制,也可以在构造函数中设定;
  • Semaphore也实现了可轮询的锁请求以及定时锁的功能,使用方法跟ReetrantLock类似;
  • Semaphore的锁释放跟ReetrantLock类似,也需要在finally中手动释放。

Semaphore控制同时访问的线程数量,通过acquire()获得一个许可,如果没有获得许可就等待,通过release()释放一个许可。

15 CountDownLatch&CyclicBarrier&Semaphore区别?

  • CountDownLatch和CyclicBarrier都能实现线程间的等待,但是侧重点不同:
    • CountDownLatch是A线程等待其他若干线程执行完毕A自己再执行
    • CyclicBarrier是一组线程互相等待到了某个状态,再一起执行
  • CountDownLatch不能够重用,CyclicBarrier可以重用
  • Semaphore和ReetrantLock类似,控制对一组资源的访问权限

16 Java中如何保证线程安全性?

16.1 线程安全在三个方面体现

  1. 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(synchronized,Lock,Atomic );
  2. 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

16.2 原子性

  • 原子性---atomic
  • 原子性---synchronized

16.3 可见性---volatile

  • volatile原理

    • volatile的可见性是通过内存屏障和禁止重排序实现的
    • 但是volatile不是原子性的,进行++操作不是安全的
  • volatile适用的场景原则

    • 对变量的写操作不依赖于当前值
    • 该变量没有包含在具有其他变量不变的式子中
  • volatile适用的场景

    • 状态标记量
    • double check

16.4 有序性

有序性是指,在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。另外,JMM具有先天的有序性,即不需要通过任何手段就可以得到保证的有序性。这称为happens-before原则。

  • 加锁

    • volatile
    • synchronized
    • lock
  • happens-before原则:

    • 程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行。
    • 锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
    • volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
    • 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
    • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    • 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
    • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
    • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

17 Java有哪些锁?

17.1 公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。加锁前先检查是否有排队等待的线程,优先排队等待的线程,FIFO
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
  • 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
  • 非公平锁的优点在于吞吐量比公平锁大。一般高5~10倍。因为公平锁需要维护一个队列。
  • 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

17.2 乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

  • 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

    • 悲观锁适合写操作非常多的场景,Java中经典的悲观锁就是Synchronized
    • AQS框架下的锁如RetreenLock,先尝试CAS乐观锁去获取锁,若获取不到锁,才会转为悲观锁。
  • 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁适合读操作非常多的场景**,不加锁会带来大量的性能提升。

    • 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
    • 更新时候会加版本号然后加锁

17.3 独享锁/共享锁

  • 独享锁是指该锁一次只能被一个线程所持有。独占锁是一种悲观保守的加锁策略。避免了读/读冲突,限制了并发性。
  • 共享锁是指允许多个线程同时获取锁,并发访问共享资源,共享锁是一种乐观加锁策略,如ReadWriteLock
  • 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  • 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
  • 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

17.4 互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock读写锁在Java中的具体实现就是ReentrantReadWriteLock

17.5 可重入锁/不可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

public synchronized void test() {
        xxxxxx;    
        test2();
    }

    public synchronized void test2() {
        yyyyy;
    }

在上面代码段中,执行 test 方法需要获得当前对象作为监视器的对象锁,但方法中又调用了 test2 的同步方法。

  • 如果锁是具有可重入性的话,那么该线程在调用 test2 时并不需要再次获得当前对象的锁,可以直接进入 test2 方法进行操作。
  • 如果锁是不具有可重入性的话,那么该线程在调用 test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得。

如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。

17.6 自旋锁/自适应自旋锁

  • 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
  • 自适应自旋锁
    自旋锁 + 自旋周期。JDK1.5时候是写死的。JDK1.6才出现适应性自旋锁。
    • JDK1.6:上一次锁的自选时间和锁的拥有者的状态来确定,-XX:+UseSpinning开启;-XX:PreBlockSpin=10为自旋次数
    • JDK1.7:去掉此参数,由JVM控制

17.7 偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的

锁的状态

  • 无锁
  • 偏向锁:在只有一个线程执行同步块时候进一步提高性能
  • 轻量级锁:线程交替执行同步块时候提高性能
  • 重量级锁

偏向锁

  • 偏向锁只需要在置换ThreadID时候依赖一次CAS原子指令,轻量级锁的获取和释放需要依赖多次CAS原子指令。
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价

轻量级锁

  • 锁可以从偏向锁升级到轻量级锁,然后升级到重量级锁,但是锁的升级是单向的,只会从低到高,不会出现锁的降级
  • 轻量级锁是相对于使用操作系统互斥量来实现重量级锁而言的,并不是要替代重量级锁,而是要减少传统重量级锁性能损耗。
  • 轻量级锁适应的场景是多线程交替执行同步块,当某一时刻访问同一把锁的时候,就会导致轻量级锁膨胀为重量级锁。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁

  • synchronized通过对象内部一个叫做监视器锁monitor来实现得,监视器锁本质又是依赖底层操作系统的Mutex Lock来实现的。这种依赖操作系统Mutex Lock实现的锁称为“重量级锁”。
  • 操作系统实现线程之间切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要很长时间,这就是synchronized效率低的原因。
  • JDK对synchronized的优化,核心都是为了减少这种重量级锁的使用,为了提高性能,JDK6后引入了“轻量级锁”和“偏向锁”。
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

17.8 分段锁

分段锁其实是一种锁的设计思想,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

18 Java多线程同步的7种方法

18.1 synchronized

  • 同步方法
    即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
  • 同步代码块
    即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

18.2 wait与notify

  • wait():使一个线程处于等待状态,并且释放所持有的对象的lock。调用wait\notify\notifyall方法时,需要与锁或者synchronized搭配使用,不然会报错java.lang.IllegalMonitorStateException,因为任何时刻,对象的控制权只能一个线程持有,因此调用wait等方法的时候,必须确保对其的控制权。

  • sleep():使一个正在运行的线程暂停执行指定的时间,是一个静态方法,调用此方法要捕捉InterruptedException异常。在调用sleep()方法的过程中,线程不会释放对象锁。

  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

  • notifyAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

18.3 volatile

  • volatile关键字为域变量的访问提供了一种免锁机制
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile解决了内存可见性问题,但是不会提供任何原子操作,它也不能用来修饰final类型的变量

18.4 Lock

  • ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
  • ReenreantLock类的常用方法有:
    • ReentrantLock() : 创建一个ReentrantLock实例
    • lock() : 获得锁
    • unlock() : 释放锁

18.5 ThreadLocal

  • 线程本地变量/线程本地存储,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

  • ThreadLocal 类的常用方法

    • ThreadLocal() : 创建一个线程本地变量
    • get() : 返回此线程局部变量的当前线程副本中的值
    • initialValue() : 返回此线程局部变量的当前线程的"初始值"
    • set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
  • ThreadLocal最常用的场景是解决数据库连接、Session管理等。

private static final ThreadLocal threadSession = new ThreadLocal(); public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); 
try { if (s == null) { 
s = getSessionFactory().openSession(); 
threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s; }

18.6 Atomic

  • 需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
    那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

  • AtomicInteger类常用方法:

    • AtomicInteger(int initialValue) : 创建具有给定初始值的新的
    • AtomicIntegeraddAddGet(int dalta) : 以原子方式将给定值与当前值相加
    • get() : 获取当前值
  • 跟Atomic Integer类似的还有Atomic Boolean、AtomicLong、AtomicReference等。可以通过AtomicReference将一个对象的所有操作转化为原子操作。

18.7 使用阻塞队列实现线程同步

  • 前面6种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。

  • 使用LinkedBlockingQueue来实现线程的同步,LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。 队列是先进先出的顺序(FIFO)

  • LinkedBlockingQueue 类常用方法

    • LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
    • put(E e) : 在队尾添加一个元素,如果队列满则阻塞 size() : 返回队列中的元素个数
    • take() : 移除并返回队头元素,如果队列空则阻塞代码实例
  • BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:

    • add()方法会抛出异常
    • offer()方法返回false
    • put()方法会阻塞

19 为什么wait()和notify()属于Object类?

  • 因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。

  • 因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。

  • 等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中。

20 什么是可重入锁和不可重入锁?

  • 可重入锁也叫递归锁,可重入意思是某个线程已经获得到锁,可以再次/多次获取锁而不会出现死锁。
  • ReetrantLock默认的lock()和synchronized都是可重入锁。

21 sleep与wait区别?

  • sleep()属于Thread类,wait()属于Object类。
  • 调用sleep()线程不会释放对象锁,休眠指定时间到了会自动恢复运行状态;调用wait()线程会释放对象锁,待notify()调用后线程进入对象锁定池获取对象锁进入运行状态。

22 start与run区别?

  • run()是方法调用,start()是启动线程。
  • start()是正确启动线程的方式,无需等待run()执行,可以继续执行下面的代码。
  • 通过start()启动线程,线程此时是就绪状态,并没有运行。
  • run()俗称线程体,调用run()线程进入运行状态,run运行结束,线程终止,CPU再调度其他线程。

23 Java后台线程 / 守护线程

  • 守护线程、后台线程、服务线程,为用户线程提供公共服务,没有用户线程可服务时自动离开。
  • 优先级:守护线程优先级比较低,为其他对象和线程提供服务。
  • 设置方法:setDaemon(true)
  • 在Daemon线程中产生的线程也是Daemon的
  • 生命周期:守护线程是JVM级别的,应用停止了,守护线程依然存活。
  • 垃圾回收线程是经典的守护线程。

24 线程 & 进程 & 协程 & 超线程

  • 进程
    • 程序在计算机上的执行过程
    • 操作系统分配资源的基本单元
  • 线程
    • 操作系统调度的基本单元
  • 联系与区别
    • 一个进程可以包含多个同时执行的线程
  • 协程
    • 协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。 一个程序可以包含多个协程,可以对比与一个进程包含多个线程。
    • 多个线程相对独立,有自己的上下文,切换受CPU控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
    • 协程和线程区别:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
超线程

25 创建线程的四种方式

  • extends Thread
  • implements Runnable
  • implements Callable
  • 使用线程池

实现Runnable比继承Thread的优势

  • 避免java单继承的局限
  • 适合用资源共享

实现Runnable和实现Callable的区别

  • Runnable没有返回值,Callable有返回值,可以通过Future拿到返回值
  • Runnable的方法是run(),Callable的方法是call()
  • call()可以抛出异常,run()不可以

使用Executor/ExecutorService接口创建线程池的四种方式

  • newFixedThreadPool:创建固定大小线程池
  • newCachedThreadPool:创建可缓存的线程池
  • newSingleThreadExecutor:创建单线程的线程池,只有一个线程
  • newScheduledThreadPool:创建定时调度型线程池,一般情况下可以用job实现

26 Thread的五种状态

线程五态模型
  • 创建New:Thread thread = new Thread ()
  • 就绪Runnable:thread.start()
  • 运行Running:线程执行体run()
  • 阻塞Blocked:由于某种原因放弃了CPU使用权,暂时停止运行。
    • 等待阻塞:o.wait -> 等待队列,运行的线程执行o.wait(),JVM会将该线程放入等待队列
    • 同步阻塞:lock -> lock pool,运行的线程在获取对象的同步锁时,若该同步锁被其他线程占用,JVM会把该线程放入锁池(lock pool)
    • 其他阻塞:Thread.sleep(long ms)或者t.join()
  • 终止Dead:run()正常结束或者程序异常结束
    • 正常结束:run()或call()执行完成
    • 异常结束:调用interrupt(),线程抛出未捕获的Exception或error
      • 线程处于阻塞状态:先捕获InterruptException后通过break来跳出循环
      • 线程处于非阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环
    • 调用stop()方法:容易导致死锁,废弃的方法,不推荐使用
    • 使用退出标志退出线程:通常用Boolean类型的true/false控制

27 Thread的优先级

  • 线程有十个优先级,用1~10数字表示,数字越大代表优先级越高,缺省是5
  • 获取线程优先级的方法int getPriority()
  • 设置线程优先级的方法void setPriority(int newPriority)
  • 设置了线程优先级也不一定代表一定会优先执行

28 Thread类常用方法

Thread类常用方法
  1. sleep()
  • public static void sleep(long n) throws InterruptedException
  • 使当前线程休眠n毫秒,线程进入timed-waiting状态
  • sleep是静态方法,可由类名直接调用,可能会抛出InterruptedException
  • 调用sleep()过程中,不会释放对象锁
  1. join()
  • public final void join() throws InterruptedException
  • 在main线程中对A线程调用了A.join(),那么main线程会等待A线程结束后才继续执行
  1. yield()
  • public static void yield()
  • 线程礼让,当线程A调用了yield方法,会把自己的CPU资源让出来,让其他线程或者A自己运行。
  • yield并不能保证其他其他具有相同优先级的线程一定会执行,有可能当前线程自己又获得了线程执行权继续执行了
  1. wait()
  • 调用wait方法会释放对象的锁,线程进入waiting状态
  • wait方法一般用在同步方法或同步代码块中
  1. interrupt()
  • 调用interrupt()并不会中断一个正在运行的线程,仅仅改变了内部维护的中断标识位。
  • 调用sleep()使线程处于timed-waiting状态,这时候调用interrupt(),会抛出InterruptedException,使线程提前结束timed-waiting状态。
  • 中断状态是线程固有的一个标识位,在线程的run()内部可以通过调用thread.isInterrupted的值来优雅终止线程。
  1. join()
  • 等待其他线程终止,调用join方法,当前线程处于阻塞状态,等待其他线程终止,当前线程再由阻塞状态变为就绪状态,等待CPU的宠幸。
  • join常用于主线程中启动了子线程,需要等待子线程结束后返回结果给主线程使用,子线程要在主线程前结束。
  1. notify() & notifyAll()
  • notify()随机唤醒监视器上等待的单个线程
  • notifyAll()唤醒监视器上等待的所有线程
  1. isAlive()
  • 判断一个线程是否存活
  1. currentThread()
  • 得到当前线程
  1. isDaemon()
  • 一个线程是否为守护线程
  1. setDaemon()
  • 设置一个线程为守护线程
  1. setName()
  • 为线程设置一个名称
  1. setPriority()
  • 设置一个线程的优先级
  1. getPriority()
  • 获得一个线程的优先级

29 synchronized方法可以调用普通方法吗,反之呢?

30 读写锁ReadWriteLock

  • 读写锁,读的地方使用读锁,写的地方使用写锁,灵活控制。
  • 多个读锁不互斥,读锁与写锁互斥,由JVM控制,程序只需要上好相应的锁即可。
  • Java读写锁的接口是java.util.concurrent.locks.ReadWriteLock,具体实现是ReetrantReadWriteLock

31 线程上下文切换

  • 上下文切换:通过时间片轮转使得单核CPU同一时刻运行多任务成为可能,执行一个任务后把任务状态和数据保存起来,然后下一任务,任务状态的保存和加载,这个过程就叫做上下文切换。
  • 上下文:某一时间点CPU寄存器和程序计数器的内容。
    • 寄存器:CPU内部数量很少但是速度很快的内存
    • 程序计数器:存的值是正在执行的指令的位置或下一个将要被执行的指令的位置,具体依赖于特定的系统。
  • PCB:process control block,切换帧,上下文切换过程中的信息保存在PCB,直到他们再次被使用。
  • 上下文切换的活动:
    • 挂起一个进程,将上下文存储于内存中某处;
    • 在内存中检索下一个进程的上下文并将其在CPU寄存器恢复;
    • 跳转到程序计数器指向的位置,在该程序中恢复该进程。
  • 引起上下文切换的原因:
    • 当前时间片用完后,CPU正常调度下一个任务;
    • 当前任务IO阻塞,调度器将其挂起,继续调度下一个任务;
    • 多个任务抢占锁资源,当前任务没有抢到被挂起,继续调度下一任务;
    • 用户代码挂起当前任务,让出CPU时间;
    • 硬件中断。

32 死锁

33 多线程之间怎么共享数据?

多线程之间通过共享内存的方式共享数据,主要有三个问题:内存可见性、有序性和原子性。

  • JMM(Java内存模型):解决了可见性和有序性的问题;
  • 锁:解决了原子性问题,理想情况下希望做到“同步”和“互斥”,常规实现如下:
    • 将数据抽象成一个类,并将数据的操作作为这个类的方法,在方法上加上synchronized
    • Runnable对象作为一个类的内部类

34 进程调度算法

  • 优先调度算法
    • FIFO调度算法:先进先出调度算法
    • 短作业优先调度算法SJF:队列中选择运行时间最短的作业
    • 短进程优先调度算法SPF:队列中选择运行时间最短的进程
  • 高优先权调度算法FPF
    • 非抢占式优先权调度算法:给你最高优先权就一直执行下去,不管是否有更高优先权进入队列,批处理系统
      • 高响应比优先调度算法:
    • 抢占式优先权调度算法:给你最高优先权就执行,直到遇到更高优先权,实时系统
  • 基于时间片轮转的调度算法
    • 时间片轮转调度算法
    • 多级反馈队列调度算法

35 Timer和ExcutorService有什么区别?

  • Java里面用Timer和TimerTask一起完成定时任务功能,有以下缺陷:
    • Timer对任务调度是基于绝对时间的,不是相对时间,对系统时间改变很敏感
    • Timer线程不会捕获异常,TimerTask抛出异常会导致Timer线程终止。
    • Timer内部是单线程,ScheduledThreadPoolExcutor是多线程

36 synchronized括号里面怎么填?直接new一个对象行不行?

  • 修饰实例方法,对当前实例对象加锁
  • 修饰静态方法,多当前类的Class对象加锁
  • 修饰代码块,对synchronized括号内的对象加锁

37 线程安全的实现方法?

  • 互斥同步/阻塞同步(悲观并发策略)
    • synchronized
    • ReentrantLock
  • 非阻塞同步(乐观并发策略)
    • CAS
  • 无同步
    • ThreadLocal

38 线程的实现方式?

  • 使用内核线程实现
    内核线程(Kernel-Level Thread,KLT)直接由操作系统内核支持的线程
  • 使用用户线程实现
    现在几乎没有使用了
  • 使用用户线程加轻量级进程混合实现

39 非公平锁为什么性能比公平锁高,两者有啥区别?各自怎么实现?

40 线程不安全会有什么问题?

  • 多线程操作有状态的共享变量线程安全问题,一致性问题
  • 程序出错
  • 不可预知的问题、灾难性问题

41 什么场景下用newFix,什么场景用newCache?满了会怎么样?

42 并行与并发

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

它们最关键的点就是:是否是『同时』。

Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别

43 缓存一致性协议&缓存行

计算机组成原理
存储器
速度差异
多核CPU
缓存行

Disruptor,缓存行对齐

MESI
缓存一致性协议
CPU乱序执行

你可能感兴趣的:(3. Interview-JUC)