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 缓存一致性协议&缓存行
1 线程池原理
1.1 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;
}
- corePoolSize:线程池中的线程数量
- maximumPoolSize:线程池中的最大线程数量
- keepAliveTime:当前线程池中的线程数>corePoolSize,多余的空闲线程存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列
- threadFactory:线程工厂,用于创建线程
- 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阻塞队列原理
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实现原理
- 所有请求锁资源的线程都会进入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位锁升级
64位锁升级
偏向锁
自旋锁
自旋超过10次,或者等待线程超过1/2,升级为重量级锁
轻量级锁
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
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吗?
8.3 volatile底层实现(JVM:内存屏障,CPU:lock指令)
- 写屏障
- StoreStoreBarrier
- StoreLoadBarrier
- 读屏障
- LoadLoadBarrier
- LoadStoreBarrier
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,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 线程安全在三个方面体现
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(synchronized,Lock,Atomic );
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(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类常用方法
- sleep()
- public static void sleep(long n) throws InterruptedException
- 使当前线程休眠n毫秒,线程进入timed-waiting状态
- sleep是静态方法,可由类名直接调用,可能会抛出InterruptedException
- 调用sleep()过程中,不会释放对象锁
- join()
- public final void join() throws InterruptedException
- 在main线程中对A线程调用了A.join(),那么main线程会等待A线程结束后才继续执行
- yield()
- public static void yield()
- 线程礼让,当线程A调用了yield方法,会把自己的CPU资源让出来,让其他线程或者A自己运行。
- yield并不能保证其他其他具有相同优先级的线程一定会执行,有可能当前线程自己又获得了线程执行权继续执行了
- wait()
- 调用wait方法会释放对象的锁,线程进入waiting状态
- wait方法一般用在同步方法或同步代码块中
- interrupt()
- 调用interrupt()并不会中断一个正在运行的线程,仅仅改变了内部维护的中断标识位。
- 调用sleep()使线程处于timed-waiting状态,这时候调用interrupt(),会抛出InterruptedException,使线程提前结束timed-waiting状态。
- 中断状态是线程固有的一个标识位,在线程的run()内部可以通过调用thread.isInterrupted的值来优雅终止线程。
- join()
- 等待其他线程终止,调用join方法,当前线程处于阻塞状态,等待其他线程终止,当前线程再由阻塞状态变为就绪状态,等待CPU的宠幸。
- join常用于主线程中启动了子线程,需要等待子线程结束后返回结果给主线程使用,子线程要在主线程前结束。
- notify() & notifyAll()
- notify()随机唤醒监视器上等待的单个线程
- notifyAll()唤醒监视器上等待的所有线程
- isAlive()
- 判断一个线程是否存活
- currentThread()
- 得到当前线程
- isDaemon()
- 一个线程是否为守护线程
- setDaemon()
- 设置一个线程为守护线程
- setName()
- 为线程设置一个名称
- setPriority()
- 设置一个线程的优先级
- 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 并行与并发
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
它们最关键的点就是:是否是『同时』。
43 缓存一致性协议&缓存行
Disruptor,缓存行对齐