线程安全(内部锁与显式锁)

线程安全问题的产生?

多个线程并发访问共享数据


锁(Lock)的概述 


   线程安全问题是因为多个线程并发访问共享数据。于是很容易我们就想到将多个线程对共享数据的访问转换为串行访问。同一时刻只能有一个线程来访问共享数据。锁就是利用这种思路来保障线程安全的一种同步机制 
锁可以理解为对共享数据保证的许可证。一个线程只有在持有许可证的情况下才能访问共享数据,在结束共享数据的访问之后必须释放许可证,以便其它线程也能够获取到许可证对共享数据进行访问。


临界区

    

在线程获取锁和释放锁的这段时间内所执行的代码。所以共享数据只允许在临界区
    内进行访问,临界区一次只能被一个线程访问。



锁的排他性(Exclusive)

     

一个锁同一时刻只能被一个线程持有,这种锁成为排他锁或者互斥锁(Metux)


è¿éåå¾çæè¿°
 


Java虚拟机对锁的实现划分

  1. 内部锁:Synchronized
  2. 显示锁:Lock接口,默认的实现ReentrantLock


Synchronized的使用

synchronized (metux) {
//对共享数据的访问和修改   
}

//并且synchronized内部锁释放是由Java虚拟机来管理,不会出现内存泄漏问题
同步代码块包含两个部分:一是作为锁的对象引用,一个是由该锁保护的代码块。
同步方法:当使用Synchronized代码块来修饰方法的时候。
非静态方法的锁就是方法调用所在的对象
静态方法的锁就是Class对象
每个Java对象都可以用作同步代码块的锁。线程在进入同步代码块的之前先获取锁,
退出同步代码块自动释放锁。不管是正常途径的退出还是发生异常。在这个过程中,
线程获取锁和释放锁都是由Java虚拟机来负责实施,这也正是synchronized
被称为内部锁的原因。



作为同步代码块锁的Java对象我们采用final修饰。如果对象发生改变,可能会导致多个线程执行同步代码块实际上使用的是不同的锁。 
锁的重入:

          当一个线程请求另一个线程持有的锁时,会发生阻塞。但是内置锁是可重入的。因此如果某个线程视图获得一个已经由它持有的锁,会成功。



重入的实现方法:

          为每一个锁关联一个获取计数器和持有者线程。当计数器为0时,这个锁认为没有被任何线程持有。当线程请求一个未被持有的锁时,JVM为记录锁的持有者。并且将计数器值加1.如果一个线程再次获取这个锁,计数器的值递增。线程在退出同步代码块的时候计数器相应递减,当计数器再次递减为0,锁被释放。


 

 

锁重入演示

public class UseReentryLock {
    public void method1() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "进入...");
            //在锁释放前调用method2的,进入method2同步代码块执行
            method2();
        }
        System.out.println(Thread.currentThread().getName() + "退出释放锁...");
    }
    public void method2(){
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "进入...");
        }
    }
    public static void main(String[] args){
        UseReentryLock reentryLock = new UseReentryLock();
        new Thread(reentryLock::method1, "reentryLock").start();
    }
}
/**
控制台输出
reentryLock进入...
reentryLock进入...
reentryLock退出释放锁...
可以看到锁是可重入的
*/



显示锁:Lock和ReentrantLock

JDK1.5开始引入的显示锁
显示锁是Lock接口的实例。该接口对显示锁进行了抽象,ReentrantLock是
Lock接口的默认实现。


Lock接口的API  :

è¿éåå¾çæè¿°

 

使用Lock的正确形式:

private final Lock lock = new ReentrantLock();//创建Lock接口实例
lock.lock();//申请锁
    try{
        //操作共享数据
    } finally{
        //总是在finally中释放锁
        lock.unlock();
}



创建Lock接口实例,使用默认的实现ReentrantLock作为显示锁。
从字面意思可以看出,ReentrantLock是可重入锁。
在访问共享数据前获取锁lock.lock
在临界区访问共享数据,lock.lock和lock.unlock中间的代码区域。一般情况下就是
try包含的代码块。
在共享数据访问结束后释放锁unlock。必须在finally中释放。

显示锁和内部锁的比较

ReentrantLock不是用来代替内部锁的,各自适用场景不同。
内部锁基于代码块,不够灵活。显示锁基于对象,可以充分发挥面向对象的灵活性
内部锁比较简单,不会出现锁泄露。显示锁必须在finally中释放锁
内部锁持有的线程如果一直不释放锁,其它同步在该锁上的所有线程就会一直被暂停
导致任务无法进展。显示锁可以使用tryLock来避免。
ReentrantLock的性能比较好


tryLock来避免死锁

     锁在获取时还没有被任何线程持有,如果获取的时候被线程持有,则不会去试图获取锁
在获取锁时,锁没有被其它线程持有,是空闲的,返回true并得到锁。
如果锁不可用,则返回false;

 

public class UseTryLock {
    //tryLock()在锁空闲的时候才获取该锁
    //锁在获取时还没有被任何线程持有,如果获取的时候被线程持有,则不会去试图获取锁
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();
    public void method1(){
        if(lock1.tryLock()){
            try{
                System.out.println(Thread.currentThread().getName() + "获取锁lock1等待锁lock2...");
                try {
                //获取到lock1,休眠10毫秒   
                TimeUnit.MICROSECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(lock2.tryLock()){
                    try{
                        System.out.println(Thread.currentThread().getName() + "获取锁lock2...");
                    }finally{
                        lock2.unlock();
                    }
                }
            }finally{
                lock1.unlock();
            }
        }
    }
    public void method2(){
        if(lock2.tryLock()){
            try{
                System.out.println(Thread.currentThread().getName() + "获取锁lock2等待锁lock1...");
                if(lock1.tryLock()){
                    try{
                        System.out.println(Thread.currentThread().getName() + "获取锁lock1...");
                    }finally{
                        lock1.unlock();
                    }
                }
            }finally{
                lock2.unlock();
            }
        }
    }
    public static void main(String[] args){
        final UseTryLock tryLock = new UseTryLock();
        new Thread(tryLock::method1,"线程1").start();
        new Thread(tryLock::method2,"线程2").start();
    }
}
/*
 * 控制台输出
 * 线程1获取锁lock1等待锁lock2...
 * 线程2获取锁lock2等待锁lock1...
 * 线程1获取锁lock2..
 */
 分析:线程1获取锁lock1休眠10毫秒等待锁lock2... 
 在线程1休眠10毫秒间,线程2执行,线程2获取锁lock2等待锁lock1...
 然后lock1.tryLock()尝试获取lock1的锁,返回false,执行结束释放lock2
 线程1休眠10毫秒后执行,发现lock2释放,线程1获取锁lock2..

 



读写锁ReadWriteLock

对于同步在同一把锁上的线程,对共享变量仅进行读取而没有更新的线程为只读线程,
简称读线程。对共享变量进行更新(包括先读取后更新)的线程成为写线程。
ReentrantLock实现了一种标准的互斥锁:每次最多有一个线程持有ReentrantLock。
这是一种强硬的加锁规则,因此就限制了一定的并发性。在避免写-写冲突和读写冲突
的时候也避免了读-读冲突。
但是在许多情况下,很多操作都是读操作,在这种情况下可以使用读/写锁:
共享数据可以被多个线程读访问,或者被一个写操作访问,但不能同时进行。


ReadWriteLock的API 
 è¿éåå¾çæè¿°

两个方法都返回Lock,这并不表示是两把锁。而是代表着两个角色 
读写锁的两种角色(读锁和写锁)


读写锁的适用场景

只读操作比写操作频繁的多
读线程持有锁的时间长

读写锁演示

public class UseReadWriteLock {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();
    public void reader(){
        readLock.lock();
        try{
            //读取共享数据
            System.out.println(Thread.currentThread().getName() + "进入...");
            TimeUnit.MILLISECONDS.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "退出...");
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            readLock.unlock();
        }
    }
    public void writter() {
        writeLock.lock();
        try{
            //读取共享数据
            System.out.println(Thread.currentThread().getName() + "进入...");
            TimeUnit.MILLISECONDS.sleep(3000);
            System.out.println(Thread.currentThread().getName() + "退出...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            writeLock.unlock();
        }
    }
    public static void main(String[] args) {
        final UseReadWriteLock useReadWriteLock = new UseReadWriteLock();
        new Thread(useReadWriteLock::reader,"读线程1").start();
        //new Thread(useReadWriteLock::reader,"读线程2").start();
        new Thread(useReadWriteLock::writter,"写线程").start();
    }
    /**
     *只开启两个读线程,两个读线程可以同时进入,获取到读锁 
     * 读线程2进入...
     * 读线程1进入...
     * 读线程2退出...
     * 读线程1退出..
     * 分别开启一个读写线程,不能同时进行
     * 写线程进入...
     * 写线程提出..
     * 读线程1进入..
     * 读线程1退出..
     */
}



Java虚拟机对内部锁的优化

锁消除

 在动态编译同步代码块时,编译器借助一种被称为逃逸分析的技术来判断
 同步代码块所使用的锁是否只能被一个线程访问。如果分析证实只能被一
 哥线程访问,那么编译器在编译同步代码块的时候不生成锁申请和释放对
 应的机器码,消除锁的开销。
1
2
3
4
开启逃逸分析的虚拟机参数:”+XX:+DoEscapeAnalysis” 
开启逃逸分析的虚拟机参数:”-XX:+DoEscapeAnalysis” 

è¿éåå¾çæè¿°

锁粗化

对于相邻的同步代码块,如果这些代码块使用的是同一把锁,则编译器会这些
代码块合并为一个大的同步代码块,避免反复的申请和释放锁
锁粗化可能会导致一个线程持续持有锁的时间变长。从而使得其他线程
在申请锁时等待时间变长。

è¿éåå¾çæè¿° 

猜你喜欢

转载自blog.csdn.net/qq_37807989/article/details/93380526