java并发3-Synchronized|ReentrantLock|锁优化

synchronized

1、作用

1)作用于方法时,锁住的是对象的实例(this);
2)当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
3)作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

2、原理

在这里插入图片描述

4、对象头

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。
在这里插入图片描述

Mark Word 平时存储这个对象的 哈希码 、分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、 重量级锁指针 、 线程 ID 等内容。
对象头上的标记位替换,类似于火车硬卧换票,等解锁(下车)时,再把栈帧中的hash码等还原回来。

在这里插入图片描述

5、偏向锁–轻量级锁–重量级锁(锁膨胀)

详细参考
1)场景:
在这里插入图片描述
a、偏向锁:
偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁
意义:通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

b、轻量级锁
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1)线程利用object对象加锁,内部创建锁记录(Lock Record)对象,record对象地址和markword交换,并将对象引用指向锁对象
在这里插入图片描述

2)注意是交替运行,如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有
竞争)
在这里插入图片描述
3)进行锁膨胀,将轻量级锁变为重量级锁。
创建Monitor,并将markword指向它。
将自己放入EntryList,将线程0设为Owner。
在这里插入图片描述
4)线程0退出时按照重量级锁流程解锁,唤醒阻塞线程

C、重量级锁
依赖于操作系统Mutex Lock所实现阻塞。

2)比喻

小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁

小南要使用房间保证计算不被其它人干扰(原子性),最初用的是防盗锁,当上下文切换时,锁住门。是安全的。

每次上锁太麻烦了,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。于是,小南干脆在门上刻上了自己的名字,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
在这里插入图片描述

ReentrantLock

1 特点

可重入:同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量

2 条件变量

synchronized 是那些不满足条件的线程都在一间休息室等消息
ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

3 应用

互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果

4原理

NonfairSync 继承自 AQS;
锁重入时state++
条件变量实现原理:每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

synchronized和lock有什么区别?

1. 原始构成

  • synchronized时关键字属于jvm

    monitorenter,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步或方法中才能掉wait/notify等方法

    monitorexit

  • Lock是具体类,是api层面的锁(java.util.)

2. 使用方法

  • sychronized不需要用户取手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
  • ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成

3. 等待是否可中断

  • synchronized不可中断,除非抛出异常或者正常运行完成
  • ReentrantLock可中断,设置超时方法tryLock(long timeout, TimeUnit unit),或者lockInterruptibly()放代码块中,调用interrupt()方法可中断。

4. 加锁是否公平

  • synchronized非公平锁
  • ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5. 锁绑定多个条件Condition

  • synchronized没有
  • ReentrantLock用来实现分组唤醒需要要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

6、代码示例

package com.jian8.juc.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * synchronized和lock区别
 * <p===lock可绑定多个条件===
 * 对线程之间按顺序调用,实现A>B>C三个线程启动,要求如下:
 * AA打印5次,BB打印10次,CC打印15次
 * 紧接着
 * AA打印5次,BB打印10次,CC打印15次
 * 。。。。
 * 来十轮
 */
public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareData.print5();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareData.print10();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareData.print15();
            }
        }, "C").start();
    }

}

class ShareData {
    private int number = 1;//A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            //判断
            while (number != 1) {
                condition1.await();
            }
            //干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 2;
            condition2.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print10() {
        lock.lock();
        try {
            //判断
            while (number != 2) {
                condition2.await();
            }
            //干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 3;
            condition3.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15() {
        lock.lock();
        try {
            //判断
            while (number != 3) {
                condition3.await();
            }
            //干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 1;
            condition1.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

锁优化

1. 减少上锁时间

同步代码块中尽量短

2. 减少锁的粒度

将一个锁拆分为多个锁提高并发度,例如:
1)LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
2)concurrenthashmap 分段锁,Segment 数组长度为 16不可扩容

3. 锁粗化

多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

4. 锁消除

JVM 会进行代码的逃逸分析
,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。

5. 读写分离

CopyOnWriteArrayList ConyOnWriteSet
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值