JAVA之Synchronized和Lock

目录

什么是线程安全,如何保证线程安全

synchronized的三种应用方式

Java对象头与Monitor

锁升级的过程

monitorenter/monitorexit实现原理

1、公平锁

2、非公平锁

3、ReentrantLock

4、synchronized

5、可重入锁(又名递归锁)

6、独占锁(写锁)、共享锁(读锁)、互斥锁

7、自旋锁

问题:synchronized和lock有什么区别?用新的Lock有什么好处?你举例说说

 

 

 


什么是线程安全,如何保证线程安全

  • 线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
  • 如何保证
    • 使用线程安全的类;
    • 使用synchronized同步代码块,或者用Lock锁;
    • 多线程并发情况下,线程共享的变量改为方法局部级变量;

 

synchronized的三种应用方式

synchronized关键字最主要有以下3种应用方式,下面分别介绍

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
     

Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

  1. 对象头:它实现synchronized的锁对象的基础,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度)
  2. 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  3. 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

锁升级的过程

  • 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。其中识别是不是同一个线程一只获取锁的标志是在上面提到的对象头Mark Word(标记字段)中存储的。
  • 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。这时候也就成为了原始的Synchronized的实现。

 

monitorenter/monitorexit实现原理

我们先看一下JVM规范是怎么定义monitorenter和monitorexit的
(1) monitorenter:
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

(2) monitorexit:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

 

1、公平锁

是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

2、非公平锁

是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

两者区别:

公平锁就是很公平,在并发环境中,每个线程在获取锁时会险查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁。否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁比较粗鲁,上来就直接尝试占有锁。如果尝试失败,就才哟个类似公平锁的那种方式。

3、ReentrantLock

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。非公平锁有点在于吞吐量比公平锁大。

1、问题:是否能多次 lock 和 unlock?

回答:允许,只要成对出现,程序可以正常运行。

2、问题:1次lock,2次unlock会报错吗?

答案:报错,IllegalMonitorStateException

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.example.demo.Phone.get(ReenterLockDemo.java:35)
	at com.example.demo.Phone.run(ReenterLockDemo.java:25)
	at java.lang.Thread.run(Thread.java:748)

3、问题:2次lock,1次unlock会报错吗?

答案:不会,但是死锁

4、synchronized

synchronized而言,也是一种非公平锁。

5、可重入锁(又名递归锁)

指的是同一线程外层函数获得锁后,内层递归函数仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,进入内层方法会自动获取锁。

也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

ReentrantLock和synchronized默认是非公平的可重入锁。

可重入锁最大的作用就是避免死锁。

例子:证明synchronized是可重入锁

public class ReenterLockDemo {

    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "T1").start();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "T2").start();

    }
}

class Phone implements Runnable{

    public synchronized void sendSMS()throws Exception{

        System.out.println(Thread.currentThread().getId() + "\tinvoked sendSMS()");
        this.sendEmain();
    }

    public synchronized void sendEmain()throws Exception{
        System.out.println(Thread.currentThread().getId() + "\tinvoked sendEmain()");
    }
}

例子:证明ReentrantLock是可重入锁

 

6、独占锁(写锁)、共享锁(读锁)、互斥锁

多个线程同时读一个资源类没有任何问题,所以为了满足并发性量,读取共享资源应该可以同时进行,但是,如果有一个线程想去写共享资源类,就不应该再有其他线程可以对该资源类进行读或写。

小总结:

读-读能共享

读-写不能共享

写-写不能共享

写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断。

 

例子:请手写一个读写锁

class MyCache{
    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value){
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key){
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在读取" + key);
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t读取完成" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void clearMap(){
        map.clear();
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for(int i = 1; i <=5; i++){
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(""+ tempInt, "" + tempInt);
            }, "" + i).start();
        }

        for(int i = 1; i <=5; i++){
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(""+ tempInt);
            }, "" + i).start();
        }
    }
}

7、自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

例子:请手写一个自旋锁

public class SpinLockDemo {
    // 原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\tcome on");
        while (!atomicReference.compareAndSet(null, thread)){
//            System.out.println(Thread.currentThread().getName() + "\t获取锁失败");
        }
        System.out.println(Thread.currentThread().getName() + "\t获取锁成功");
    }

    public void myUnLock(){
        Thread  thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\tinvoked myUnLock()");
    }

    public static void main(String[] args) {
        SpinLockDemo demo = new SpinLockDemo();
        new Thread(() -> {
            demo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            demo.myUnLock();
        }, "AA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            demo.myLock();
            demo.myUnLock();
        }, "BB").start();
    }
}

 

问题:synchronized和lock有什么区别?用新的Lock有什么好处?你举例说说

1、原始构成

synchronized是关键字属于JVM层面。

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

Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

2、使用方法

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

3、等待是否可中断

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

4、加锁是否公平

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

5、锁绑定多个条件Condition

  • synchronized没有
  • ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个,要么全部唤醒
     
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值