Java锁机制,synchronized和lock详解。

Java锁机制详解

1.java各种锁详解

1.1 公平锁 vs 非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,假设有一个线程此时加锁后正准备释放,这时候刚好又有一个线程进来获取锁,则有可能改线程就能获取该锁。(跟插队一样),如果没有刚好线程释放锁的话,则需要乖乖往后面排队,先来后到。就变成公平锁了。(synchronized和lock默认是非公平锁)

lock非公平锁设置:Lock lock=new ReentrantLock(false)。 默认是false,设置为true则为公平锁。

1.2乐观锁 VS 悲观锁

  • 悲观锁是一种悲观思想,它总认为自己在使用数据的时候一定有别的线程来修改,所以悲观锁在持有数据的时候总会把资源或数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**悲观锁的实现往往依靠数据库本身的锁功能实现。

  • Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。

  • 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

  • 例如Java中的validate

比较

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

1.3可重入锁

就是加锁的次数和释放锁的次数要一样,可重入锁的意义在于防止死锁。

synchronized 和 ReentrantLock 都是可重入锁。

1.4读写锁

读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。

读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 JVM 自己控制的。(只要有写锁就需要加锁)

1.5轻量级锁

轻量级锁在没有多线程竞争的前提下,把整个同步都消除掉,连CAS(Compare And Swap)操作都不去做了,优于轻量级锁。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁

1.6重量级锁

同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁

1.7自旋锁

为了优化重量级锁,特意引入了自旋锁。自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。

1.8偏向锁(当前场景不太适应)

这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。

然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做。

2.synchronized

synchronized 实现原理

synchronized 是悲观锁,在字节码层被映射成两个指令:monitorenter 和 monitorexit,当一个线程遇到 monitorenter 指令时,会尝试去获取锁,如果获取成功,锁的数量 +1,(因为synchronized是一个可重入锁,需要使用锁计数来判断锁的情况),如果没有获取到锁,就会阻塞;当线程遇到 monitorexit 指令时,锁计数 -1,当计数器为 0 时,线程释放锁;如果线程遇到异常,也会释放锁。

例如查看下面代码的字节码:

public class APP {
    void test() {
        synchronized (this) {
            System.out.println("hello world");
        }
    }
}

首先 cd 到文件目录,然后执行 javac APP.java,可得到字节码文件:APP.class,再执行 javap -verbose APP.class,可看到字节码内容。

Java锁机制,synchronized和lock详解。_第1张图片

2.1synchronized修饰普通方法,锁对象默认为this

package com.hjt.synchronizedDemo;



public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
        synchronized (this) {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

Java锁机制,synchronized和lock详解。_第2张图片

2.2 两把不同的锁

package com.hjt.synchronizedDemo;



public class SynchronizedObjectLock1 implements Runnable {
    static SynchronizedObjectLock1 instence = new SynchronizedObjectLock1();
    // 创建2把锁
    Object block1 = new Object();
    Object block2 = new Object();

    @Override
    public void run() {
        // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
        synchronized (block1) {
            System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
        }

        synchronized (block2) {
            System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

输出结果:

Java锁机制,synchronized和lock详解。_第3张图片

2.3synchronize修饰静态方法

package com.hjt.synchronizedDemo;

public class SynchronizedObjectLock2 implements Runnable {
    static SynchronizedObjectLock2 instence1 = new SynchronizedObjectLock2();
    static SynchronizedObjectLock2 instence2 = new SynchronizedObjectLock2();

    @Override
    public void run() {
        method();
    }

    // synchronized用在普通方法上,默认的锁就是this,当前实例
    public static  synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }

    public static void main(String[] args) {
        // t1和t2对应的this是两个不同的实例,所以代码不会串行
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

输出结果:

Java锁机制,synchronized和lock详解。_第4张图片

2.4synchronized指定锁对象为Class对象

package com.hjt.synchronizedDemo;


public class SynchronizedObjectLock3 implements Runnable {
    static SynchronizedObjectLock3 instence1 = new SynchronizedObjectLock3();
    static SynchronizedObjectLock3 instence2 = new SynchronizedObjectLock3();

    @Override
    public void run() {
        // 所有线程需要的锁都是同一把
        synchronized(SynchronizedObjectLock3.class){
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

运行结果:

Java锁机制,synchronized和lock详解。_第5张图片

3.lock

  • lock(): 加锁
  • unlock(): 解锁
  • tryLock(): 尝试获取锁,返回一个boolean值
  • tryLock(long,TimeUtil): 尝试获取锁,可以设置超时

Lock一般使用的例子,注意ReentrantLock是Lock接口的实现。

package com.hjt.lock;


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

public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
        try {
            lock.lock();
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("线程名"+thread.getName() + "释放了锁");

        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
//执行情况:线程名t1获得了锁
//         线程名t1释放了锁
//         线程名t2获得了锁
//         线程名t2释放了锁

Java锁机制,synchronized和lock详解。_第6张图片

4.Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  1. Lock需要手动获取锁和释放锁。

  2. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
    synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致 死锁现象发生,而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放 锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  4. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

5.ReentrantLock锁和ReentrantReadwriteLock 读写锁锁

5.1ReentrantLock详解

  • ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。并且也是公平锁。

    公平锁实例:

    package com.hjt.reentrantLock;
    
    /***
     * @author hjt
     * AbstractQueuedSynchonizer 抽象队列同步器 简称AQS
     */
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class MyThread extends Thread {
        private Lock lock;
        public MyThread(String name, Lock lock) {
            super(name);
            this.lock = lock;
        }
    
        public void run () {
            lock.lock();
            try {
                System.out.println(Thread.currentThread() + " running");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        }
    }
    
    public class AbstractQueuedSynchonizerDemo {
        public static void main(String[] args) throws InterruptedException {
            Lock lock = new ReentrantLock(true);
    
            MyThread t1 = new MyThread("t1", lock);
            MyThread t2 = new MyThread("t2", lock);
            MyThread t3 = new MyThread("t3", lock);
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    运行结果:

    Java锁机制,synchronized和lock详解。_第7张图片

Java锁机制,synchronized和lock详解。_第8张图片

5.2ReentrantReadwriteLock 读写锁(常用)

JUC的就 是java并发编程工具包

** 读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞**

package com.hjt.reentrantReadwriteLock;


import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadThread extends Thread {
    private ReentrantReadWriteLock rrwLock;

    public ReadThread(String name, ReentrantReadWriteLock rrwLock) {
        super(name);
        this.rrwLock = rrwLock;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " trying to lock");
        try {
            rrwLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " lock successfully");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rrwLock.readLock().unlock();
            System.out.println(Thread.currentThread().getName() + " unlock successfully");
        }
    }
}

class WriteThread extends Thread {
    private ReentrantReadWriteLock rrwLock;

    public WriteThread(String name, ReentrantReadWriteLock rrwLock) {
        super(name);
        this.rrwLock = rrwLock;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " trying to lock");
        try {
            rrwLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName() + " lock successfully");
        } finally {
            rrwLock.writeLock().unlock();
            System.out.println(Thread.currentThread().getName() + " unlock successfully");
        }
    }
}

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock rrwLock = new ReentrantReadWriteLock();
        ReadThread rt1 = new ReadThread("rt1", rrwLock);
        ReadThread rt2 = new ReadThread("rt2", rrwLock);
        WriteThread wt1 = new WriteThread("wt1", rrwLock);
        rt1.start();
        rt2.start();
        wt1.start();
    }
}

运行结果,先后顺序可能不一样。

Java锁机制,synchronized和lock详解。_第9张图片

6.volatile 关键字

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

写一段简单的 Java 代码,声明一个 volatile 变量,并赋值。

public class Test {
    private volatile int a;
    public void update() {
        a = 1;
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.update();
    }
}
  
  
    

通过 hsdis 和 jitwatch 工具可以得到编译后的汇编代码:

......
  0x0000000002951563: and    $0xffffffffffffff87,%rdi
  0x0000000002951567: je     0x00000000029515f8
  0x000000000295156d: test   $0x7,%rdi
  0x0000000002951574: jne    0x00000000029515bd
  0x0000000002951576: test   $0x300,%rdi
  0x000000000295157d: jne    0x000000000295159c
  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi
......

lock 前缀的指令在多核处理器下会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。

所有多核处理器下还会完成:当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。

volatile 的应用场景

使用 volatile 必须具备的条件

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。
  • 只有在状态真正独立于程序内其他内容时才能使用 volatile。

附加问题1: i++为什么不能保证原子性?

对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

7. 关键字: final详解

7.1修饰方法

常规的使用就不说了,这里说下:

  • private 方法是隐式的final
  • final方法是可以被重载的

¶ private final

类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。看下下面的例子:

public class Base {
    private void test() {
    }
}

public class Son extends Base{
    public void test() {
    }
    public static void main(String[] args) {
        Son son = new Son();
        Base father = son;
        //father.test();
    }
}
  

    

Base和Son都有方法test(),但是这并不是一种覆盖,因为private所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是father.test()是不可执行的,因为Base中的test方法是private的,无法被访问到。

7.2final方法是可以被重载的

我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。

public class FinalExampleParent {
    public final void test() {
    }

    public final void test(String str) {
    }
}

代码地址:https://github.com/hongjiatao/spring-boot-anyDemo
欢迎互相交流

你可能感兴趣的:(Java专栏,java,jvm,开发语言)