关于锁的一些知识
乐观锁和悲观锁
1、 悲观锁 (synchronized 关键字和 Lock 的实现类都是悲观锁)
- 什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- 适合写操作多的场景,先加锁可以保证写操作时数据正确 (写操作包括增删改)、显式的锁定之后再操作同步资源
- synchronized 关键字和 Lock 的实现类都是悲观锁
2、 乐观锁
- 概念:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁在 Java 中通过使用无锁编程来实现,最常采用的时 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的
- 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升
- 乐观锁一般有两种实现方式 (采用版本号机制、CAS 算法实现)
- //悲观锁的调用方式
- public synchronized void m1(){
- //加锁后的业务逻辑
- }
- //保证多个线程使用的是同一个lock对象的前提下
- ReetrantLock lock=new ReentrantLock();
- public void m2(){
- lock.lock();
- try{
- //操作同步资源
- }finally{
- lock.unlock();
- }
- }
- //乐观锁的调用方式
- //保证多个线程使用的是同一个AtomicInteger
- private AtomicInteger atomicIntege=new AtomicInteger();
- atomicIntege.incrementAndGet();
对象锁 / 类锁
【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体﹔能用对象锁,就不要用类锁。
说明︰尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
作用于代码块,对括号里配置的对象加锁。
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- phone.sendSMS( );
- // phone.hello();
- // phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
* 一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调归其中的一个 synchronized 方法了,
* 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些 synchronized 方法
* 锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- // phone.sendSMS( );
- phone.hello();
- // phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----hello
- -----sendEmail
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- // phone.sendSMS( );
- // phone.hello();
- phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----sendSMS
- -----sendEmail
* 加个普通方法后发现和同步锁无关
* 换成两个对象后,不是同一把锁了,情况立刻变化。
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- phone.sendSMS( );
- // phone.hello();
- // phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public static synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public static synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----sendEmail
- -----sendSMS
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- // phone.sendSMS( );
- // phone.hello();
- phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public static synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public static synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----sendEmail
- -----sendSMS
** 都换成静态同步方法后,情况又变化三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指 this, 具体的一部部手机,所有的普通同步方法用的都是同一把锁 —> 实例对象本身,
* 对于静态同步方法,锁的是当前类的 Class 对象,如 Phone.class 唯一的一个模板。
- Phone phone = new Phone();
- Phone phone2 = new Phone();
-
- //对于静态同步方法 看的是当前的类 比竟:static关键字修饰的方法,又叫类方法.属于类的,不属于对象, 在实例化对象之前就可以通过类名.方法名调用静态方法。
- public static synchronized void sendEmail(){};
- //static synchronized 是属于类锁
- public synchronized void sendsMs(){};
- //synchronize 是属于对象锁
-
- //静态方法static(类方法)和 非静态方法(实例方法)的区别:
-
- //方法我们主要分为三种:
- //1.构造方法
- //2.非静态方法(普通方法/实例方法)
- //3.静态方法(类方法)
-
- //一、静态方法和非静态方法的区别(调用对象、引用变量不同)
- //静态方法:是使用static关键字修饰的方法,又叫类方法.属于类的,不属于对象, 在实例化对象之前就可以通过类名.方法名调用静态方法。 (静态属性,静态方法都是属于类的,可以直接通过类名调用)。
- //A.在静态方法中,可以调用静态方法。
- //B.在静态方法中,不能调用非静态方法。
- //C.在静态方法中,可以引用类变量(即,static修饰的变量)。
- //D.在静态方法中,不能引用成员变量(即,没有static修饰的变量)。
- //E.在静态方法中,不能使用super和this关键字
-
- //非静态方法:是不含有static关键字修饰的普通方法,又称为实例方法,成员方法。属于对象的,不属于类的。(成员属性,成员方法是属于对象的,必须通过new关键字创建对象后,再通过对象调用)。
- //A.在普通方法中,可以调用普通方法。
- //B.在普通方法中,可以调用静态方法
- //C.在普通方法中,可以引用类变量和成员变量
- //D.在普通方法中,可以使用super和this关键字
-
- //二、静态方法和非静态方法的区别(调用方法不同)
-
-
- //静态方法可以直接调用,类名调用和对象调用。(类名.方法名 / 对象名.方法名)
- //但是非静态方法只能通过对象调用。(对象名.方法名)
-
- //三、静态方法和非静态方法的区别(生命周期不同)
-
-
- //静态方法的生命周期跟相应的类一样长,静态方法和静态变量会随着类的定义而被分配和装载入内存中。一直到线程结束,静态属性和方法才会被销毁。(也就是静态方法属于类)
- //非静态方法的生命周期和类的实例化对象一样长,只有当类实例化了一个对象,非静态方法才会被创建,而当这个对象被销毁时,非静态方法也马上被销毁。(也就是非静态方法属于对象)
-
- //总结:类方法可以直接通过类名调用,实例方法必需先实例化类,再初始化对象,然后通过类的实例对象才能调用
* 对于同步方法块,锁的是 synchronized 括号内的对象
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- phone.sendSMS( );
- // phone.hello();
- // phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public static synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----sendSMS
- -----sendEmail
- //入口
- public static void main(string[] args){
- Phone phone = new Phone();
- Phone phone2 = new Phone();
- new Thread(() -> {
- phone. sendEmail();
- }, name: "a" ).start();
- //暂停毫秒,保证a线程先启动
- try {
- TimeUnit.ILLISECONDs.sleep( timeout 200);
- } catch (InterruptedException e) {
- e.printstackTrace();
- }
- new Thread(() -> {
- // phone.sendSMS( );
- // phone.hello();
- phone2.sendSMS();
- }, name: "b" ).start();
- }
-
- class Phone{
- public static synchronized void sendEmail(){
- try {
- TimeUnit.SECONDs.sleep( timeout: 3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- system.out.println( "-----sendEmail" );
- }
- public synchronized void sendsMs(){
- system.out.println( "-----sendSMS" );
- }
- public void hello(){
- system.out.println( "-----hello" );
- }
- }
-
- -----sendSMS
- -----sendEmail
** 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* 所有的普通同步方法用的都是同一把锁 — 实例对象本身,就是 new 出来的具体实例对象本身,本类 this
* 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* 所有的静态同步方法用的也是同一把锁 — 类对象本身,就是我们说过的唯一模板 class
* 具体实例对象 this 和唯一模板 class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* 但是一旦一个静态同步方获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到
非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象
注意:synchronized 和 ReentrantLock 默认是非公平锁
为什么会有公平锁和非公平锁
1
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从 CPU 的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用 CPU 的时间片,尽量减少 CPU 空闲状态时间。
2
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当 1 个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁 (前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
显示锁 / 隐式锁
隐式锁(即 synchronized 关键字使用的锁)默认是可重入锁
显式锁(即 Lock)也有 ReentrantLock 这样的可重入锁。
- new Thread(-> {
- lock.lock();
- try{
- system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
- lock.lock();
- try{
- system.out.println(Thread.currentThread().getName()+"\t ---come in内层调用");
- }finally {
- lock.unLock( );
- }
- }finally {
- lock. unlock();
- }
- },name: "t1").start();
-
- //加锁和释放锁次数一致,程序运行正常!
- new Thread(-> {
- lock.lock();
- try{
- system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
- lock.lock();
- try{
- system.out.println(Thread.currentThread().getName()+"\t ---come in内层调用");
- }finally {
- lock.unLock( );
- }
- }finally {
- //由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
- //lock. unlock();
- }
- },name: "t1").start();
-
- new Thread(() -> {
- lock. lock();
- try{
- system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
- }finally {
- lock.unlock();
- }
- }, name: "t2" ).start();
-
- //加锁和释放锁次数不一致,线程t2不会执行,程序一直等待!
死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源
请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
- public class DeadLockDemo {
- private static Object resource1 = new Object();//资源 1
- private static Object resource2 = new Object();//资源 2
-
- public static void main(String[] args) {
- new Thread(() -> {
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource2");
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- }
- }
- }, "线程 1").start();
-
- new Thread(() -> {
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource1");
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- }
- }
- }, "线程 2").start();
- }
- }
小结
指针指向 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,当一个 monior 被某个线程持有
后,它便处于锁定状态。在 Java 虚拟机 (HotSpot) 中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下〈位于 HotSpot 虚拟机源
码 ObjectMonitor.hpp 文件,C++ 实现的)