java多线程总结 - 原理篇

synchronized关键字

java关键字,用于并发线程的同步执行。主要用在:
修饰对象:当线程进入该代码块,该线程就会持有对象的锁。 当其他线程准备进入该代码块时,就会被阻塞。线程会进入对象的等待队列,直到上一个线程执行结束,释放锁。示例:synchronized(object){}、synchronized(this){}
修饰非静态方法:和修饰对象类似,获取的是当前对象的锁。示例:public void synchronized methed1{}
修饰类静态方法或类:进入被synchronized修饰的静态方法的线程会持有当前类锁,其他线程无法进入当前类的其他方法。直到上个线程释放类锁。synchronized(A.class){}、public synchronized static void methed1{}

synchronized实现原理

1、如果持有对象的锁:在JVM中,每个对象的头部会有一块记录锁状态的区域,称为Mark Word。当线程进入该代码块时,会判断当前对象的锁状态,根据锁的状态来进行不同的阻塞方案。
具体在JVM中通过ObjectMonitor中的monitorenter和monitorexit字节码实现,如果修饰对象方法,则在方法的标记位上有ACC_SYNCHRONIZED标志
2、如果持有类的锁:修饰静态方法也是通过方法标记位ACC_STATIC、ACC_SYNCHRONIZED,如果是synchronized(A.class),是通过类进行ObjectMonitor中的monitorenter和monitorexit的。
在JDK1.6以前,synchronized关键字被认为是重量级的锁,它通过Mutex Lock实现,线程之间的切换需要从用户态切换到核心态
在1.6及以后,JVM对synchronized做了较大的优化,主要引入了偏向锁、自旋锁、轻量级锁、重量级锁、自适应自旋、锁消除、锁粗化的概念。
偏向锁:当线程进入同步代码块时,对象头就记录了当前线程的ID,这样的好处是,当同一个线程多次进入同步代码块时就可以直接进入,降低了加锁解锁的成本。但当多个线程需要同时获得对象锁时,偏向锁就不适用了,因为对象头只能记录一个线程ID。此时就会升级为轻量级锁,在升级之前,还会进行一次挣扎(考虑到大部分情况下,线程持有cpu的时间是非常短的),所以进行一定的自旋大部分能获取到锁,JVM默认自旋次数为10次。

synchronized与ReentrantLock区别

ReentrantLock可重入锁,是Lock的一个实现类,组合了Sync类,Sync是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer。是一个基于队列的同步控制器。相对于synchronized,ReentrantLock可以说是用户控制的锁,不借助JVM内部就控制了线程的同步机制。它主要借助了CAS(compare and swap比较并交换),它有三个参数,内存位置V,预期原值(expect)和更新值(update),表示当内存位置的值等于expect时,就将其更新为update,否则不更新。这个机制很有效,它模拟了线程获取锁的过程,如果当前锁被其他线程持有,则继续获取,否则就获取锁。
总结一下:
synchronized: java关键字 JVM级别 非公平锁
ReentrantLock:java类 用户级 公平非公平均可
两者都是可重入锁
ReentrantLock可以对获取锁的等待时间进行设置,避免了死锁,同时可以获取锁的各种信息。通过condition可以实现灵活的多路通知。
加锁机制:synchronized操作的是对象的Mark Word,而lock调用的Unsafe的park方法。

JMM内存可见性

内存不可见的原因:CPU存在多级缓存(局部性原理),当线程在执行过程中,大部分数据存在与CPU缓存中,不同的cpu核之间的内存是不可见的。
解决可见性的方案:volitile关键字
语义:1、被修饰的字段修改对其他线程可见 2、禁止指令重排序(通过内存屏障)
原理:写入:当线程更新被volitile修饰的变量时,会让cpu缓存失效,直接更新主内存。
读取:当线程读取被volitile修饰的变量时,使工作内存中失效,会直接从主内存读取
在这里插入图片描述

CAS

CAS(compare and swap比较并交换),它有三个参数,内存位置V,预期原值(expect)和更新值(update),表示当内存位置的值等于expect时,就将其更新为update,否则不更新。
CAS是一种类似乐观锁的实现,在线程竞争不是很激烈的时候,通过CAS能大大提高效率。
缺点:
1、因为CAS通过do-while实现,若循环时间长,cpu开销会比较大
2、只能保证一个共享变量的原子操作,多个共享变量必须使用lock或synchronized
3、ABA问题,可以通过AtomicStampedReference寻求解决

JAVA线程池

当并发请求的数量很多,同时每个线程执行花费的时间很短。如果一个请求创建一个线程再销毁,那样大部分时间和性能会花在创建销毁线程上。如果采用了线程池,可以大大提高线程的复用率,提高执行效率。
之前常用的是JUC包下Executors类创建线程池,但现在不推荐了,原因是封装了线程池的创建过程,开发者会降低关注度。同时它设置的阻塞队列过长,当阻塞线程过多时可能会出现OOM异常。推荐直接采用ThreadPoolExecutor创建线程
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值