关于多线程问题的一些总结

1.synchronized的具体实现

synchronized关键字分为三类:

第一类是当作用对象是方法时,锁住的就是对象实例。就是说我要调用这个方法的这个this。

第二类是作用对象是静态方法时,锁住的是this类对应的class对象。

第三类是作用对象是代码块时,指定加锁对象,进入同步代码块时必须获得该对象的锁。

2.对JAVA对象头、monitor的了解

Java对象头和monitor是实现synchronized的基础!

什么是Java对象头?

虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)(如果是数组还有一个数组长度)。

在这里分析一下重量级锁,也就是synchronized的对象锁。锁标识位是10,指针指向的是monitor(监视器锁)的起始地址。在JAVA虚拟机中,monitor是由ObjectMonitor实现的,主要的数据结构:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

1).最开始的时候owner为空,表示没有线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

2).当线程进来的时候,先是都进入一个EntryList这个队列,然后如果count是0则owner指向这个线程,别的 线程全部等待,且这个时候count变为1;

3).当owner指向这个线程的时候,如果发生了wait的指令,那么将释放当前持有的monitor,然后count变为0方便后面的进入。而且该持有的monitor进入waitSet集合里面等等待唤醒。

4).或者是当这个monitor没有收到指令 的时候,就完成这个任务,释放锁,count变为0,然后后面的线程继续完成。

3.实现同步块的语句是什么?

同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。当执行monitorenter的时候,当前线程尝试拿锁,如果这时候拿锁成功,那么计数器变为1。如果别的线程正在占用,那么拿锁就失败。

4.对synchronized这类重量锁的优化

1).锁粗化:即将连续多个存在的单个锁变为一个统一的锁。

2).自旋锁:就是让没有获得锁的进程自己运行一段时间自循环(默认开启),但是不挂起线程。因为当有线程正在执行代码的时候,别的线程只能等待,进入排队队列,然后等待CPU去唤醒他们,但是我们发现:往往大多数锁占用的时间是很短暂的。这样的话,资源浪费浪费比较大。所以就先让一个该线程暂时不进入等待,而是自己执行无关循环,如果等待的时间比较 长,那么不建议采用这种方式。

3).偏向锁:核心思想是:如果在接下来的时间内没有和别的线程和他竞争该锁,那么该线程就不执行同步操作。从而进入偏向模式。原理:1>当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。2>以后该线程在进入和退出同步块时就不需要进行CAS操作来加锁和解锁,只需简单地判断一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果在标识符里面不存在偏向锁的标识符,那么就使用CAS操作来加锁。如果标识符是1,那么就查看线程ID是不是正确的,如果不是正确的,也是通过CAS操作来获取锁。如果在这之中,获取锁失败,那么证明存在锁时间的竞争,偏向锁就失效。转而变为轻量锁。

4).轻量锁:是指使用CAS操作将对象头中的MarkWord替换为指向锁记录的指针。如果成功的话,就OK,如果失败,就将轻量锁转为自旋锁。

4.5 . Java中的同步方案有哪些?

重量级锁:synchronized

重入锁(ReentrantLock):重入锁又自带一系列高逼格UBFF:可中断响应(一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了)、锁申请等待限时(前者不带参数,这时线程尝试获取锁,如果获取到锁则继续执行,如果锁被其他线程持有,则立即返回 false ,也就是不会使当前线程等待,所以不会产生死锁。 后者带有参数,表示在指定时长内获取到锁则继续执行,如果等待指定时长后还没有获取到锁则返回false。)、公平锁(就是按照时间先后顺序,使先等待的线程先得到锁,而且,公平锁不会产生饥饿锁,也就是只要排队等待,最终能等待到获取锁的机会) 

并发容器:ConcurrentHashMap和CopyOnWriteArrayList

同步容器:Vector、Stack、HashTable

CAS:

volatile:

AQS:抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架。它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列。

5.关于线程池的了解

如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。所以说,就使用了线程池。

使用线程池的好处:1.降低资源的消耗;2.提高相应速度;3.提高线程的可管理性。

关于ThreadPoolExecutor类的了解

Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架,目的是提供一种将”任务提交”与”任务如何运行”分离开来的机制。

线程池的五大状态:

RUNNING:能接受新的任务,也能处理阻塞队列的任务;

SHUTDOWN:不能接受新的任务,但是可以 处理阻塞队列的任务;shutdown()接口

STOP:不能接受新的任务,也不能处理阻塞队列的任务;shutdownNow()接口

TIDYING:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,terminated() 方法进入TERMINATED 状态

TERMINATED:线程池彻底终止,就变成TERMINATED状态

线程池的工作图:

ThreadPoolExecutor详解

corePoolSize - 池中所保存的线程数,包括空闲线程。

maximumPoolSize-池中允许的最大线程数。

keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

unit - keepAliveTime 参数的时间单位。

workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。

threadFactory - 执行程序创建新线程时使用的工厂。

handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

ThreadPoolExecutor是Executors类的底层实现。

常见线程池?

①newSingleThreadExecutor

单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

②newFixedThreadExecutor(n)

固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

③newCacheThreadExecutor(推荐使用)

可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor

大小无限制的线程池,支持定时和周期性的执行线程

线程创建的基本过程:

a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。

c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;

d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

当一个线程完成任务时,它会从队列中取下一个任务来执行。

当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

workQueue 线程池所使用的缓冲队列

直接提交:

       工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。 在此,如果不存在可用于立即运行任务的线程,则把任务加入队列将失败,因此会构造一个新的线程。 此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性; 

无界队列:

       使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时,新任务将在队列中等待。这样,创建的线程数量就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性; 

有界队列:            

       当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量.

  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值