分类锁Condition: 用于替代wait/notify方法,wait和notify方法是结合synchronized一起使用,可以令线程等待和唤醒等待线程的集合,而Condition是结合Lock一起使用,能够提供多个等待集合,更精准地控制线程的等待和唤醒(底层使用LockSupport的park和unpark机制)
// 使用线程通信方式
final Condition full = reentrantLock.newCondition();
final Condition empty = reentrantLock.newCondition();
final int size = 10;
final ArrayDeque<String> container = new ArrayDeque<>();
Thread producer = new Thread(){
@Override
public void run() {
reentrantLock.lock();
try{
// 不断生产数据
while (true){
// 数据已经满了
while (size == container.size()){
// 不再进行生产数据
full.await();
}
// 生产数据
container.push(new String("xxx"));
System.out.println(Thread.currentThread().getName() + " produce ...");
empty.signalAll();// 通知消费进行消费
}
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
};
Thread consumer = new Thread(){
@Override
public void run() {
reentrantLock.lock();
try{
while (true){
// 判断数据是否为空
while(0 == container.size()){
empty.await();
}
String str = container.pop();
System.out.println(Thread.currentThread().getName() + " consumer data for :" + str);
full.signalAll(); // 通知生产者生产数据
}
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
};
producer.start();
TimeUnit.SECONDS.sleep(2L);
consumer.start();
无锁: 正常的java对象,属于默认的初始化的锁状态
偏向锁: 对象头markword中的hashcode通过CAS替换为当前的ThreadId,同时对象的偏向锁标志为1
轻量级锁: 达到一定的并发量,JVM撤销偏向锁,锁升级为轻量级锁,即在对象头markword中的bitfields通过CAS替换为当前的执行线程栈帧的引用地址,同时栈帧中存储对象头的markword信息
重量级锁(mutex lock): 并发竞争激烈,升级为重量级锁,通过将对象头markword中的bitfields通过CAS替换为当前对象的引用地址,通过JVM优化会通过走“捷径”方式来进行加锁,锁无法降级
仅在程序出现线程安全的情况下进行加锁
代码演示减少锁持有时间
run(){
// query from user
// query from order
try{
// 保证一致性, 思考: 这里用事务也可以实现同样的效果,事务与锁有什么关系?
lock.lock();
// create order items
// create orders
// create outbound orders
}finally{
lock.unlock();
}
}
关于事务与锁的联系与区别的参考文档
参考mysql文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html
参考美团技术文档: https://tech.meituan.com/2014/08/20/innodb-lock.html
在并发条件下,程序获取锁的成本十分昂贵,如果在一块代码中对同一个锁不断地获取和释放,容易导致CPU系统资源被消耗殆尽,严重影响程序在操作系统中的执行效率,也就是说为了保证在并发多线程环境下,要求每个线程尽可能持有锁的时间片段尽可能少,同时能够在完成同步代码之后释放共享资源以便其他线程能够获取锁进行相应的操作
程序手动优化方式
// 锁粗化伪代码示例
increase(){
synchronized(this){
this.productNum -- ;
}
// send message to mq notice product num have been decreased
synchronized(this){
this.orderItem ++ ;
}
// shopping cart have add new one product one
}
take(){
for(int index=0; index < LIMIT_SIZE; index++){
synchronized(queue){
queue.pop();
}
}
}
// 从上述看到,其实锁粗化出现的情况很多时候是我们是在指定的业务顺序逻辑进行编写代码造成的,一般情况下对代码的优化我们也需要考虑到两个同步代码中间的其他代码是否不影响执行程序代码的执行结果来进行优化
increase(){
// 减少锁的获取
synchronized(this){
this.productNum -- ;
this.orderItem ++ ;
}
// send message to mq notice product num have been decreased
// shopping cart have add new one product one
}
take(){
synchronized(queue){
for(...){
// ...
}
}
}
JIT编译器优化:如果发现调用的次数过多,JIT会在编译的时候会进行锁粗化的优化
int i = 0;
test1(){
synchronized(this){
i++;
}
synchronized(this){
i++;
}
}
// main函数
for (int i = 0; i < 1000000; i++) {
new LockOptmistic().test1()
}
// 分别截图JIT编译 i < 10000 & i < 100000 & i < 1000000 对应的效果
也就是在JIT在编译阶段发生对于非共享资源在程序代码进行加锁的处理,那么这个时候JIT识别是非共享资源,这个时候将直接跳过加锁的方式运行程序代码
// 锁消除的伪代码
test2(){
// 线程封闭,属于线程私有的变量,此时非共享资源,加锁没有意义,JIT编译器会自动将锁进行消除
Object object = new Object();
int count = 0;
synchronized(object){
count++;
}
}
// 出现上述的原因很多时候是由于工程师本身在编译不规范造成的,这种并非业务性逻辑错误,可以避免的
JIT Watch查看结果
分片加锁其实将锁的粒度缩小范围,尤其是在并发情况下,需要对存储容器不断进行读写操作,如果每次都对容器进行加锁,那么锁范围十分大,会大大降低程序执行的效率,因此分片加锁就是针对容器进行划分为若干等分(可以片段)进行加锁,也就是说针对容器某一个等分进行读写操作的时候只针对该部分进行加锁操作,其他部分仍保持无锁的状态,可以大大提升程序CPU利用率,加快程序的执行
分片加锁技术
java中使用ConcurrentHashMap针对Segment片段进行加锁,每一份Segment存储key-value,通过对Segment进行加锁方式(进一步优化可以处理为读写锁方式)来保证线程安全
在数据库中,可以将一系列的update/delete操作进行行级别加锁,相比表级别的加锁方式,可以提升并发执行效率
可以按照读写功能进行划分为读写锁,即写写互斥,读写互斥,读读共享,也可以按照指定的业务场景来对相应的程序代码设置对应的加锁方式,有效地提升并发执行的处理能力,降低锁之间的竞争,比如读写锁ReadWriteLock
基于UnSafe实现的CAS机制,其原理可以参考先前的CAS机制
基于java并发包下的(AQS)实现的锁方式
本文分享自微信公众号 - 疾风先生(Gale2Writing)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。