Java并发(多线程和锁机制)

part 01:Java线程

1、进程和线程的区别:

  • 进程是操作系统进行资源分配的最小单位,线程是操作系统进行任务分配的最小单位,一个进程可以有多个线程。

2、Java开启线程的方式:

  • 继承Tread类,重写run方法;
  • 实现Runnable接口,实现run方法;
  • 实现Callable接口,实现call方法;(FutureTask创建线程,获取线程返回值);
  • 通过线程池开启线程(优点:提升线程池中线程的使用率,减少线程的创建、销毁所花的时间以及系统资源的开销;线程池可以控制线程数,有效提升服务器的使用资源)

3、四种常用的线程池:

  • newCachedThreadPool:可缓存线程池,灵活回收空闲线程,若无可回收,则新建线程。没有核心线程,线程数量没有上线,默认消亡时间为60秒,阻塞队列是SynchronousQueue,没有存储性质的阻塞队列,取值操作和放入操作必须是互斥的,可以理解为每当有任务放入时立即有线程将它取出执行。
  • 优点:提高了线程的复用率,当第二个任务开始,第一个任务已经结束,那么第二个任务会服用第一个任务创建的线程,并不会重新创建新的线程。
  • 缺点:无法控制最多需要多少个线程同时处理,它会自动扩展线程数
                    
  • newFixedThreadPool:固定核心线程数线程池,可以控制线程的最大并发数量,使服务器达到最大的使用率,同时又可以保证流量突然增大也不会占用服务器过多的资源。
  • 缺点:核心线程数量就是最大线程数量,所以线程池内的线程永远不会销毁,采用无参数的链表阻塞队列,存在任务积压导致内存溢出的风险。
                  
  • newScheduledThreadPool:固定调度线程池。有固定的核心线程,线程的数量没有限制,支持定时以及周期性任务执行,可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。两种延迟方法:
  • scheduleAtFixedRate:当前任务时间小于间隔时间,每次到点即执行;当前任务执行时间大于等于间隔时间,任务执行后立即执行下一次任务,相当于连续执行。
  • scheduledWithFixedDelay:每当上次任务执行完毕后,每隔一段时间执行。(可以做每天几点执行任务)
                 
  • newSingleThreadExecutor:单线程线程池,由始至终都由一个线程来执行。所有任务按照FIFO指定顺序执行
                  
4、线程池参数:
  • corePoolSize:核心线程数;
在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
一般需要根据任务的类型来配置线程池大小:
  如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
  如果是IO密集型任务,参考值可以设置为2*NCPU
  • maximumPoolSize:最大线程数,核心线程+线程队列;最大线程数 =(最大任务数-队列容量)/每个线程每秒处理能力
  • keepAliveTime:最大空闲时间,超过空闲时间不工作的线程会被销毁,默认用于非核心线程,通过设置 allowCoreThreadTimeOut(true)后,也会用于核心线程
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
  • unit:最大空闲时间时间单位
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:等待执行的任务队列,如果核心线程没有空闲的,新来的任务会被放到这个等待队列中
    • SynchronousQueue:将任务直接交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图将任务加入队列失败,会构造一个新的线程。要求maximumPoolSize,以避免拒绝新提交的任务。
    • 无界队列:LinkedBlockingQueue,所有corePoolSize线程都在忙时新任务在队列中等待。适用场景:所有任务互不影响,用于处理瞬态突发请求。
    • 有界队列:ArrayBlockingQueue,有助于防止资源耗尽,但是可能较难调整和控制。当ArrayBlockingQueue满时,则又会开启新的线程去执行,直到线程数量达到maximumPoolSize。
    • 延时队列:DelayedWorkQueue,保证添加到队列中的任务,会按照任务的延时排序,延时时间少的任务首先被获取
  • threadFactory:用于实现生成线程的方式,定义线程名格式,是否后台执行等等
    • Spring框架提供的:CustomizableThreadFactory
ThreadFactory springThreadFactory = new CustomizableThreadFactory("springThread-pool-");
  • Google guava工具类提供的:ThreadFactoryBuilder
  • Apache commons-lang3 提供的:BasicThreadFactory
ThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()
          .namingPattern("basicThreadFactory-").build();
  • handler:当线程数达到最大线程数maximumPoolSize,再有新的任务到达时启动的策略。
    • ThreadPoolExecutor.AbortPolicy:直接抛出异常(默认)
    • ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,但不抛出异常
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后将新加入的任务加入等待队列
    • ThreadPoolExecutor.CallerRunsPolicy:由线程池所在的线程处理该任务
5、线程工具类:
  • CountDownLatch:栅栏,一般用于某个线程等待若干个其他线程执行完任务之后,他才执行,不可重用
  • CylicBarrier:  回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行,一组线程互相等待至某个状态,然后这一组线程再同时执行,可重用
  • Semaphore:信号量

part 02:Java锁

1、synchronized和reentrantLock的区别:

  • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
  • Lock等待可中断,而synchronized只会让等待的线程一直等待下去。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。
  • synchronized是非公平锁,reentrantLock可以设置是否为公平锁,通过构造方法new ReentrantLock时传入值进行选择,true为公平锁,false为非公平锁
  • 一个ReentrantLock可以绑定多个Condition对象,结合await()/singal()方法实现线程的精确唤醒,而synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机幻想一个线程要么唤醒全部线程
  • synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
  • 通过Lock可以知道有没有成功获取到锁
大量线程同时竞争,ReentrantLock要远胜于synchronized。
JDK5中,synchronized是性能低效的,因为这是一个重量级操作,对性能的最大影响是阻塞的实现,挂起线程和恢复线程的操作,都需要转入内核态中完成,给并发带来了很大压力。
JDK6中synchronized加入了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等一系列优化,官方也支持synchronized,提倡在synchronized能实现需求的前提下,优先考虑synchronized来进行同步。
2、锁升级过程:
  • 偏向锁:一个线程多次获得同一个锁,不存在锁竞争。
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。(先看对象头中的threadID是否一致,不一致看持有锁的对象是否存活,存活看是否还持有锁,还持有锁就升级为轻量级锁
  • 轻量级锁:竞争锁对象的线程不多,而且线程持有锁的时间不长的情景,其他线程自旋等待锁。
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。 自旋锁简单来说就是让线程2在循环中不断CAS
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
  • 重量级锁:很多线程竞争锁,且锁持有时间长。
3、threadlocal
  • threadlocal主要作用是做数据隔离,填充的数据只属于当前线程,防止当前线程的变量被其他线程篡改
  • Spring采用threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,实现事务隔离级别(threadlocal+AOP实现)
  • 每个Thread都维护了自己的threadLocals变量
  • 使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值
  • ThreadLocal的key被设计成weakReference弱引用,在没有被外部强引用时,发生GC会被回收, 如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。怎么解决:在使用的最后remove把值清空
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值