java并发

线程状态转换

状态名说明
new初始状态,线程被构建,但是还没有调用start()方法
Runnable运行状态
Blocked阻塞状态,表示线程阻塞
waitting等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出特定动作
Time_Waiting超时等待状态,它可以在指定的时间自行返回
Terminated终止状态,表示当前线程执行完毕

创建线程的三种方法

  • 继承 Thread 类
  • 实现 Runnable 接口;
  • 实现 Callable 接口;

Runnable接口和Callable接口的区别

  • 实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果
  • callable接口实现类中的call方法允许异常向上抛出,可以在内部处理,try catch,但是runnable接口实现类中run方法的异常必须在内部处理,不能抛出

1.悲观锁和乐观锁
  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
锁的状态

四种锁状态理解

优点缺点使用场景
偏向锁除了第一次加锁,其余加锁和解锁不需要额外的消耗如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到索竞争的线程,使用自旋会消耗CPU追求响应速度,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,线程调度和切换需要消耗额外资源追求吞吐量,同步块执行速度较慢
可重入锁

可重入锁是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁
ReentrantLock底层原理
ReentrantLock继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。 当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status ==0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。 释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 = 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

Synchronized底层原理

  • Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下情况之一:
  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加 这把锁已经被别的线程获取了,等待锁释放 monitorexit指令:
  • 释放对于monitor的所有权,释放过程很简单,就是将monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

synchronized 和 volatile 的区别?

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

synchronized 和 lock 的区别?

  • Synchronized是内置Java关键字,Lock是一个java类
  • Synchronized无法判断所得状态,Lock可以判断是否获得锁
  • Synchronized会自动释放锁,Lock必须手动释放锁,如果不释放,就是产生死锁
  • Synchronized如果线程1(获得锁,发生阻塞),线程2(会一直等下去),Lock锁不一定会等下去(lock.trylock()试图获取锁)
  • Synchronized 可重入锁,不可中断的,非公平的;Lock 可重入锁,可以自己设置公平与非公平
  • Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步问题

synchronized 和 ReentrantLock 的区别

  • synchronized 是 JVM 层面通过监视器(Monitor)实现的,而 ReentrantLock 是通过 AQS(AbstractQueuedSynchronizer)程序级别的 API 实现。
  • synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • synchronized 不能响应中断,也就是如果发生了死锁,使用 synchronized 会一直等待下去,而使用 ReentrantLock 可以响应中断并释放锁,从而解决死锁的问题,
  • synchronized 会自动加锁和释放锁,而 ReentrantLock 需要手动加锁和释放锁。

volatile解决不可见问题

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

共享的long和double变量的为什么要用volatile?

在java中,long和double两种数据类型都8个字节(64位),在32位系统中
读完一个64位变量要分为两步操作,如果有两个线程同时处理,一个线程写前32位,另一个线程写后32位,数据可能发生变化,从而数据失效。
如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
如果使用volatile修饰long和double,那么其读写都是原子操作。

AQS 核心思想

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 ,当前一个线程释放锁之后会唤醒队列的头节点。
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行。

CyclicBarrier 和 CountDownLatch 的区别

  • CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。
  • 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。

阻塞队列

  • ArrayBlockingQueue:是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。它不能够存储无限多数量的元素。
  • DelayQueue:延迟队列,对元素进行持有直到一个特定的延迟到期。
  • LinkedBlockingQueue :由链表结构组成的有界阻塞队列。内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • PriorityBlockingQueue:具有优先级的阻塞队列
  • SynchronousQueue:同步队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。

为什么使用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程的执行过程

当一个任务提交至线程池之后: 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2. 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3. 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给拒绝策略来处理任务。

线程池参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)

  • corePoolSize:线程池中的核心线程数
  • maximumPoolSize: 线程池中允许的最大线程数
  • keepAliveTime:线程空闲时的存活时间,
  • unit:存活单位
  • BlockingQueue:阻塞队列
  • RejectedExecutionHandler:拒绝策略

拒绝策略

new ThreadPoolExecutor.AbortPolicy());//拒绝策列的一种,服务口和等待队列都满了的时候,还有人进来,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy());//哪来的回哪去(main处理)
new ThreadPoolExecutor.DiscardPolicy());//拒绝策列的一种,会丢掉多余的任务,不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy());//拒绝策列的一种,服务口和等待队列都满了的时候,后面进来的人会尝试去和第一个竞争,如果竞争失败则被丢弃,不会跑出异常。

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。
Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。 -
  • newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

(1)newFixedThreadPool:工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:

  • 线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数 ;
  • 由于使用了无界队列, 所以FixedThreadPool永远不会拒绝, 即饱和策略失效
    (2):newSingleThreadExecutor:由于使用了无界队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效
    (3)newCachedThreadPool:内部使用SynchronousQueue作为阻塞队列;在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销.

ThreadLocal

每一个Thread线程都会拥有自己的一个成员变量ThreadLocalMap,该变量默认为空(实际是ThreadLocal的静态内部类,只是Thread持有引用)。当往ThreadLocal存数据时,调用的是ThreadLocal的set方法,该方法先拿到当前线程的ThreadLocalMap,然后以当前threadLocal实例为key把数据存进该map中。当取数据时,一样拿到当前线程的ThreadLocalMap,并以当前threadLocal实例为key从里面拿出数据

内存泄露

内存泄漏就是程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时, Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着。就会造成内存泄漏。

**hreadLocal内存泄漏的根源是:**由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

内存泄漏解决方案

  • 对于不需要使用的对象手动设置null值;
  • 减少长生命周期持有短生命周期的引用;
  • 使用StringBuilder和StringBuffer进行字符串拼接,减少使用String,如果使用多个String对象进行字符串连接运算,在运行时可能产生大量临时数据
  • 各种连接,使用完成之后使用close关闭(例如:每次使用完ThreadLocal都调用它的remove()方法清除数据)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值