多线程
并发和并行的区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
parallel.jpg
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
1进程&线程
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。在一个时间点上,每个CPU上只有一个线程在执行;双CPU就是真正的多线程。
一个应用有一个或多个进程组成,一个进程由多个线程组成。
2线程的状态?
3. 如何创建线程
2.1继承自Thread类
public class ThreadA extends Thread {
int m = 100;
// 重写 run 方法
@Override
public void run() {
for (int i=0;i<m;i++){
System.out.println("i:"+i);
}
}
}
2.2实现Runnable接口
public class ThreadB implements Runnable {
int m = 100;
// 实现 run 方法
@Override
public void run() {
for (int j=0;j<m;j++){
System.out.println("j:"+j);
}
}
}
测试
public class Test1 {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
//线程A处于就绪状态,只能调用一次start方法
// 启动线程
threadA.start();
// 启动线程
Thread threadB = new Thread(new ThreadB());
threadB.start();
// 主线程阻塞,等待着线程threadA 执行完毕
threadA.join();
sou("dddd")
}
}
2.3实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
4线程的同步 synchronized
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
当多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步,没有同步就没有办法保证一个线程所做的修改可以被另外一个线程获知。
为什么要同步?,先看下面的情况
由如下银行账户,提供存钱和取钱方法
public class BankAccount implements Serializable {
// 账户余额
private double balance = 100.0;
// 存钱
public synchronized void saveMoney(double money){
balance = balance + money;
}
// 减钱
public void drawMoney(double money){
synchronized (this){
balance = balance -money;
}
}
public double getBalance() {
return balance;
}
}
测试
public class Test {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
int m = 1000;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<m;i++){
bankAccount.saveMoney(10);
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<m;i++){
bankAccount.drawMoney(10);
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待上边两个线程结束
Thread.sleep(10000);
// 获取账户余额
System.out.println("balance:"+bankAccount.getBalance());
}
}
synchronized的使用
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前
Class
对象。 - 同步块,锁的是
()
中的对象。
5.生产者消费者模型
1、仓库(消息队列)
2、生产者类
3、消费者类
package com.aaa.deamo2;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Store {
int max = 10;
private LinkedList<String> queue = new LinkedList<String>();
public synchronized void push(String str) throws InterruptedException {
Thread.sleep(1000);
if (queue.size()==max){
System.out.println("当前队列已满--线程等待");
try {
// 让当前线程等待,释放当前的锁,带别人唤醒时,接着执行
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(str);
// 唤醒其他等待线程
notifyAll();
}
public synchronized String pull() throws InterruptedException {
Thread.sleep(1000);
if (queue.isEmpty()){
System.out.println("当前队列为空,程序进入等待");
// 让当前线程等待,释放当前的锁,带别人唤醒时,接着执行
wait();
}
String str = queue.poll();
notifyAll();
return str;
}
}
测试
public class Test {
public static void main(String[] args) {
Store store = new Store();
int m = 100;
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<m;i++){
try {
store.push(i+"");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<m;i++){
try {
String result = store.pull();
System.out.println("result:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadA.start();
threadB.start();
}
}
备注:
wait(); notifyAll();1.成对出现 2.必须配合synchronized使用
notifyAll() ¬ify() 区别?
先解释两个概念。
- 等待池:
假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池,等待池中的线程不会去竞争该对象的锁。
- 锁池:
只有获取了对象的锁,线程才能执行对象的 synchronized 代码,对象的锁每次只有一个线程可以获得,其他线程只能在锁池中等待
区别:
notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;
notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。
6死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
public class Test {
public static void main(String[] args) {
Object objectA = new Object();
Object objectB = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (objectA){
try {
System.out.println("Thread1 has locked objectA");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB){
System.out.println("Thread1 has locked objectB");
System.out.println("A--->B");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (objectB){
System.out.println("Thread2 has locked objectB");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA){
System.out.println("Thread2 has locked objectA");
System.out.println("B--->A");
}
}
}
});
thread1.start();
thread2.start();
}
}
在java并发编程领域已经有技术大咖总结出了发生死锁的条件,只有四个条件都发生时才会出现死锁:
1.互斥,共享资源X和Y只能被一个线程占用
2.占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
3.不可抢占,其他线程不能强行抢占线程T1占有的资源
4.循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待
避免死锁有效方式方式就是避免循环等待,将上述死锁的方式改变为顺序锁
7.线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 利用线程池管理并复用线程、控制最大并发数等。
- 实现任务线程队列缓存策略和拒绝机制。
- 实现某些与时间相关的功能,如定时执行、周期执行等。
- 隔离线程环境。
public ThreadPoolExecutor(
int corePoolSize, //第1个参数
int maximumPoolSize, //第2个参数
long keepAliveTime, //第3个参数
TimeUnit unit, //第4个参数
BlockingQueue<Runnable> 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:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
-
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
-
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
-
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
-
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
内存飙升问题
无界队列引发的内存飙升
明白了线程池的运行原理了,这个面试题就好解答了。
我们以最常用的fixed线程池举例,他的线程池数量是固定的,因为他用的是近乎于无界的LinkedBlockingQueue,几乎可以无限制的放入任务到队列里。
所以只要线程池里的线程数量达到了corePoolSize指定的数量之后,接下来就维持这个固定数量的线程了。
然后,所有任务都会入队到workQueue里去,线程从workQueue获取任务来处理。
这个队列几乎永远不会满,当然这是几乎,因为LinkedBlockingQueue默认的最大任务数量是Integer.MAX_VALUE,非常大,近乎于可以理解为无限吧。
只要队列不满,就跟maximumPoolSize、keepAliveTime这些没关系了,因为不会创建超过corePoolSize数量的线程的。
线程池分类?
1、newCachedThreadPool
作用:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。
特征:
(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
(3)当线程池中,没有可用线程,会重新创建一个线程
创建方式: Executors.newCachedThreadPool();
2、newFixedThreadPool
作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征:
(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量
(2)线程可以重复被使用,在显示关闭之前,都将一直存在
(3)超出一定量的线程被提交时候需在队列中等待
创建方式:
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
(2)Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式
3、newSingleThreadExecutor
作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征:
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
创建方式:
(1)Executors.newSingleThreadExecutor() ;
(2)Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式
4、newScheduleThreadPool
作用: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
特征:
(1)线程池中具有指定数量的线程,即便是空线程也将保留
(2)可定时或者延迟执行线程活动
创建方式:
(1)Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数
(2)newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);// corePoolSize线程的个数,threadFactory创建线程的工厂
5、newSingleThreadScheduledExecutor
作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
特征:
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
(2)可定时或者延迟执行线程活动
创建方式:
(1)Executors.newSingleThreadScheduledExecutor() ;
(2)Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory) ;//threadFactory创建线程的工厂
线程池使用
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
String result = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "sssss";
}
}).get();
Thread.sleep(5000);
System.out.println("result = " + result);
}
7保证线程安全的几种方式?
volatile 只可以保证可见性,不能保证线程安全
threadLocal
synchronized
Lock
juc包 concurxxx AutoMicXXXX
cas
12volital
volatile主要作用是保证可见性以及有序性(禁止指令重排)。
非volital变量
volital变量
8.什么是cas?
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
基于cas的实现有AtomicBoolean,AtomicInteger Lock 等,CAS 基于 利用unsafe提供了原子性操作方法实现
举例
volatile int a = 1
目标是要线程安全的修改 a 使用CAS 修改
1.获取 a 值 (旧值(1))
2.调用CAS(旧值(1),要修改的值(2))
cas 底层是Unsafe 的cas 实现( 将传入的旧值(1) 和 现在a 的值相比,如果相等,就将要修改的值2 赋值给 a,如果不相等,则不设置(因为不相等代表其他线程修改过 a的值))
AtomicInteger 自增实现
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Unsafe类中的compareAndSwapInt(Object var1, long var2, int var4, int var5)
var1:要修改的对象起始地址 如:0x00000111
var2:需要修改的具体内存地址 如100 。0x0000011+100 = 0x0000111就是要修改的值的地址
注意没有var3
var4:期望内存中的值,拿这个值和0x0000111内存中的中值比较,如果为true,则修改,返回ture,否则返回false,等待下次修改。
var5:如果上一步比较为ture,则把var5更新到0x0000111其实的内存中。
原子操作,直接操作内存。
cas 存在 aba问题?
什么意思呢?就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A。这就是有名的ABA问题。ABA问题会带来什么后果呢?我们举个例子。
一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆吗?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
账户: 100
提款机1 : 100 ,50
提款机2 : 100 ,50
只有一个成功 账户余额 50,另外一个提款机1 失败
账户: 100
提款机1 : 100 ,50 A
用户:存50 50, +50 结果 100 B
提款机2 : 100 ,50 A 成功,但是结果是错误的
使用版本号解决ABA?
在每修改一次目标值,都给对应的目标值,绑定一个版本号
具体使用 AtomicStampedReference 为对应的目标对象进行包装 增加版本(时间标记),解决ABA 问题
public static void main(String[] args) {
String str1 = "aaa";
String str2 = "bbb";
AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1,1);
reference.compareAndSet(str1,str2,reference.getStamp(),reference.getStamp()+1);
System.out.println("reference.getReference() = " + reference.getReference());
boolean b = reference.attemptStamp(str2, reference.getStamp() + 1);
System.out.println("b: "+b);
System.out.println("reference.getStamp() = "+reference.getStamp());
boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1);
System.out.println("reference.getReference() = "+reference.getReference());
System.out.println("c = " + c);
}
AtomicLong,AtomicInteger性能问题?
除此之外,在并发量非常高的情况下,CAS失败的几率将变得非常高,重试的次数也会跟着增加,越多线程重试,CAS失败的几率就越高,变成恶性循环。因此在并发量非常高的环境中,如果仍然想通过原子类来更新的话,可以使用AtomicLong的替代类:LongAdder。 Adder 类解决
将单一value的更新压力分担到多个value中去,降低单个value的“热度”,分段更新,这样,线程数再多也会分担到多个value上去更新,只需要增加value的个数就可以降低value的 “热度”,这样AtomicLong中的恶性循环就可以解决了。
在LongAdder中cells就是这个“段”,cell中的value就是存放更新值的,这样,当我需要总数时,把cell中的value都累加一下不就可以了么
让我们看一下LongAdder更新的原则:
1.当并发低时先采用CAS进行更新,如果更新成功即返回
2.当并发高且CAS更新失败时,则进入分段更新
测试
public class AtomicityLongAdder {
private final LongAdder count = new LongAdder();
public void increase() {
count.increment();
}
public long count(){
return count.longValue();
}
}
public class Test {
public static void main(String[] args) {
Long time = System.currentTimeMillis();
final AtomicityLongAdder atomicityLongAdder = new AtomicityLongAdder();
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 10000; j++) {
atomicityLongAdder.increase();
}
}
}).start();
}
System.out.println("Thread.activeCount()"+Thread.activeCount());
while(Thread.activeCount() > 2) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("运行时间:" + (System.currentTimeMillis() - time));
System.out.println("LongAdder(乐观锁):" + atomicityLongAdder.count());
}
}
AtomicInteger 对比?
public class Test2 {
public static void main(String[] args) {
Long time = System.currentTimeMillis();
final AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 10000; j++) {
atomicInteger.incrementAndGet();
}
}
}).start();
}
System.out.println("Thread.activeCount()"+Thread.activeCount());
while(Thread.activeCount() > 2) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("运行时间:" + (System.currentTimeMillis() - time));
System.out.println("LongAdder(乐观锁):" + atomicInteger.intValue());
}
}
Unsafe类介绍**
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等。
使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class UnsafePlayer {
public static void main(String[] args) throws Exception {
//通过反射实例化Unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
//实例化Player
Player player = (Player) unsafe.allocateInstance(Player.class);
player.setName("li lei");
System.out.println(player.getName());
}
}
class Player{
private String name;
private Player(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
9juc
在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。下面一起来看看它怎么使用。
10什么是aqs?
AbstractQueuedSynchronizer,抽象队列同步器。ReentrantLock、ReentrantReadWriteLock底层都是基于AQS来实现的。给大家画一个图先,看一下ReentrantLock和AQS之间的关系。
https://blog.csdn.net/fangjialue/article/details/103236744
公平锁和非公平锁的区别?
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
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.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
11.什么乐观锁与悲观锁?
乐观锁:Lock ,CAS
悲观锁:synchronized
15.HashMap 底层原理?
jdk1.7
jdk 1.8
jdk1.7 于jdk 1.8的区别?
jdk 1.7 数组 加 链表
jdk 1.8 数组 加 链表/红黑树
jdk 1.7 头插法
jdk 1.8 尾插法
扩容机制
jdk 1.8 扩容机制 链表长度大于8 并且数组长度大于64 链表转化为 红黑树
不大于64 对数组进行扩容
链表和红黑树转换 低于6 转化为链表 高于8
核心概念
DEFAULT_INITIAL_CAPACITY =16:默认初始容量
MAXIMUM_CAPACITY :最大容量1 << 30
DEFAULT_LOAD_FACTOR =0.75f:负载因子
TREEIFY_THRESHOLD =8:阈值 如果链表长度超过阀值就把链表转成红黑树
UNTREEIFY_THRESHOLD =6:链表长度低于6,就把红黑树转回链表
MIN_TREEIFY_CAPACITY =64:开始转换为树结构的值
扩容机制
数据结构转变规则
如果这个桶中bin的数量小于 TREEIFY_THRESHOLD 当然不会转化成树形结构存储;
如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD ,但是capacity小于 MIN_TREEIFY_CAPACITY 则
依然使用链表结构进行存储,此时会对HashMap进行扩容;
如果capacity大于了 MIN_TREEIFY_CAPACITY ,则会进行树化。
头插法vs 尾插法
jdk7扩容
https://blog.csdn.net/weixin_29628479/article/details/113076225
https://blog.csdn.net/y277an/article/details/94368875
jdk 7.遍历数组中的每一个node(链表节点,重新分配到新的 数组中),
while (null != e) { // 数组中的每一个node
Entrynext = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
jdk 7 造成链表死循环的原因
1.在多线程情况下同时插入触发扩容机制
2.每个线程都使用头插法发扩容,(会造成原来对应的node 节点顺序发生调换)
3.其他再次引用对应的节点进行扩容是,发生链表循环
4.在获取key 在循环链表时,发生死循环
jdk8 使用尾插法解决循环链表问题
jdk8扩容,为什么是 6 8?
1.设置 6 8 而不是一个中间点 7, 避免 不断链表和树化的转换(如果有一个区间,避免频繁切换,提高性能)
2.根据统计分析,很少有 链表长度超过8
名词解释
- 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
- 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
- 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
16.ConcurrentHashMap实现原理及源码分析?
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
ConcurrentHashMap 1.7
1.8构造
https://blog.csdn.net/qq_43253123/article/details/105420159
java8的ConcurrentHashMap为何放弃分段锁?
1、加入多个分段锁浪费内存空间。
2、生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
3.有助于GC优化
4.synchronized jdk 已经做了优化,粒度更加精细化
可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。