一、乐观锁和悲观锁

1、悲观锁

  • 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的数据修改
  • synchronized关键字和Lock的实现类都是悲观锁
  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确
  • 显式的锁定之后再操作同步资源 54.png
// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
    // 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
    lock.lock();
    // 操作同步资源
    lock.unlock();
}

2、乐观锁

  • 认为自己在使用数据时,不会有别的线程修改数据或者资源,所以不会添加锁
  • 在java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
    • 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
  • 判断规则:
    • 版本号机制Version
    • 最长采用的是CAS算法,java原子类中的递增操作就通过CAS自旋实现的

55.png

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

二、synchronized 锁的八种案例演示

加锁原则: 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体。能用对象锁,就不用类锁。 使用加锁的代码块尽可能的要小,避免在锁代码块中调用RPC方法

1、例一

标准访问有a,b两个线程,请问先打印邮件还是短信?邮件

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest1 {

    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
             phone.sendSMS();
        },"b").start();
    }
}


class Phone{
    public  synchronized void sendEmail(){
    
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public   synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }

}

56.png

2、例二

sendEmail方法中加入暂停3s,先打印邮件还是短信?邮件

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest2 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone.sendSMS();
        },"b").start();
    }
}

class Phone{
    public  synchronized void sendEmail(){
        try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public   synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }




}

57.png

3、例三

添加一个普通的hello方法(不加锁),先打印邮件还是hello?hello

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone.hello();
        },"b").start();
    }
}



class Phone{
    public  synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

   public void hello(){
       System.out.println("==============hello");
   }
}

58.png

4、例四

两部手机,先打印邮件还是短信?短信

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone1.sendSMS();
        },"b").start();
    }
}



class Phone{
    public  synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public  synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }



}


59.png

5、例五

有两个静态同步方法,一部手机,先打印邮件还是短信?邮件

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone.sendSMS();
        },"b").start();
    }
}



class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public static   synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }



}


60.png

6、例六

两部手机,两个静态同步方法,先打印邮件还是短信?邮件

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone2.sendSMS();
        },"b").start();
    }
}



class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public static   synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }
}

61.png

7、例七

有一个静态同步方法,有一个普通同步方法,一部手机,先打印邮件还是短信?短信

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone.sendSMS();
        },"b").start();
    }
}



class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public    synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }
}

62.png

8、例八

有一个静态同步方法,有一个普通同步方法,有2部手机,先打印邮件还是短信?短信

package com.lori.juc2023.juc2;

import java.util.concurrent.TimeUnit;

public class LockTest3 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{

            phone.sendEmail();
        },"a").start();


        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(()->{
            phone2.sendSMS();
        },"b").start();
    }
}



class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("++++++++++sendEmail+++++++++++++++");
    }

    public    synchronized void sendSMS(){
        System.out.println("++++++++++sendSMS+++++++++++++++");
    }
}

63.png

9、总结

  • 1、一个对象里面如果有多个synchronized 方法,某一个时刻内,只要有一个线程去调用其中一个synchronized 修饰的方法了,其他线程都只能等待,换句话来说,某一时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他线程都不能进入到当前对象的其他synchronized 方法。

  • 2、同一个类里面,如果有一个线程去调用了synchronized 修饰的方法,其他线程调用非synchronized 方法不受影响,各自执行各自的方法,无需等到synchronized 方法执行完后再执行普通方法。

  • 3、new两个不同的对象进入phone资源,两部手机分别调用手机功能,是没有影响的,不需要synchronized 释放锁,因为是两个phone资源

  • 4、上面的可以证明synchronized 锁的是对象

  • 5、三种synchronized 锁的内容有一些差别

    • 对于synchronized ,锁的是当前实例对象,通常指的是this,具体的某一部手机,所有synchronized 方法用的都是同一把锁。
    • 对于static synchronized ,锁的是当前类的Class对象,相当于Phone.class唯一的一个模板
    • 对于同步方法块,锁的是synchronized 括号内的对象
  • 6、类锁和对象锁互不干扰,自己执行自己的。一旦一个static synchronized方法获取同步锁以后,其他static synchronized都必须等待锁释放后才能获得锁,但static synchronized与synchronized之间不会有竞争条件。

10、以上八种锁的案例体现为三个方面

  • 1、作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁
  • 2、作用于代码块,对括号里配置的对象加锁
  • 3、作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

三、公平锁和非公平锁

1、公平锁

定义: 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

package com.lori.juc2023.juc3;

import java.util.concurrent.locks.ReentrantLock;

public class SaleTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"a").start();

        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"b").start();

        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"c").start();
    }
}


class Ticket{
    //资源类,模拟3个售票员卖完50张票
    private int number = 50;
    //公平锁
    ReentrantLock lock = new ReentrantLock(true);

    public void sale(){
        lock.lock();
        try {
            if(number>0){
                System.out.println(Thread.currentThread().getName()+"卖出第:"+"\t"+(number--)+"\t 还剩下"+number);
            }
        } finally {
            lock.unlock();
        }
    }
}

65.png

2、非公平锁

定义: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。 优点: 可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。 缺点: 你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

package com.lori.juc2023.juc3;

import java.util.concurrent.locks.ReentrantLock;

public class SaleTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"a").start();

        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"b").start();

        new Thread(()->{
            for (int i = 0; i <55 ; i++) {
                ticket.sale();
            }
        },"c").start();
    }
}


class Ticket{
    //资源类,模拟3个售票员卖完50张票
    private int number = 50;
    //非公平锁
    ReentrantLock lock = new ReentrantLock();

    public void sale(){
        lock.lock();
        try {
            if(number>0){
                System.out.println(Thread.currentThread().getName()+"卖出第:"+"\t"+(number--)+"\t 还剩下"+number);
            }
        } finally {
            lock.unlock();
        }
    }
}

64.png

3、为什么会有公平锁和非公平锁

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销线程的开销

4、什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用

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

1、概述

  • 定义: 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞
  • 如果是1个有synchronized修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
  • 以Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁 可重入锁这四个字分开解释
  • 可: 可以
  • 重: 再次
  • 入: 进入
  • 锁: 同步锁
  • 进入什么:进入同步域(即同步代码块、方法或显示锁锁定的代码) 同步块

2、可重入锁的种类

1、隐式锁

  • Synchronized关键字默认的是可重入锁
  • 可重复递归调用的锁,在外层使用锁之后,内层仍然可以使用,并且不发生死锁
  • 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
package com.lori.juc2023.juc3;

public class ReEntryLockDemo {
    public static void main(String[] args) {
        final Object o = new Object();
        new Thread(()->{
           synchronized (o){
               System.out.println(Thread.currentThread().getName()+"\t 外层调用");
               synchronized (o) {
                   System.out.println(Thread.currentThread().getName() + "\t 中层调用");
                   synchronized (o){
                       System.out.println(Thread.currentThread().getName()+"\t 内层调用");
                   }
               }
           }
        }).start();
    }
}
package com.lori.juc2023.juc3;


//将代码抽取成方法:Ctrl+alt+M
public class ReEntryLockDemo {

    public synchronized void  m1(){
        System.out.println(Thread.currentThread().getName()+"=====come in m1");
        m2();
        System.out.println(Thread.currentThread().getName()+"=====end m1");

    }

    public synchronized void  m2(){
        System.out.println(Thread.currentThread().getName()+"=====come in m2");
        m3();
        System.out.println(Thread.currentThread().getName()+"=====end m2");
    }
    public synchronized void  m3(){
        System.out.println(Thread.currentThread().getName()+"=====come in m3");

        System.out.println(Thread.currentThread().getName()+"===== m3 end");

    }
    public static void main(String[] args) {
        ReEntryLockDemo entryLockDemo = new ReEntryLockDemo();

        new Thread(()->{
            entryLockDemo.m1();
        },"t1").start();
    }

}

synchronized 的重入实现原理(为什么任何一个对象都可以成为一个锁)

  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
  • 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
  • 当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放 66.png

2、显式锁(锁了几次就需要释放几次)

  • ReentrantLock这样的可重入锁
  • lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程
package com.lori.juc2023.juc3;

import java.util.concurrent.locks.ReentrantLock;

public class ReEntryLockDemo1 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        new Thread(()->{
            reentrantLock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"外层锁");
                try {
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName()+"中层锁");
                } finally {
                    reentrantLock.unlock();
                }
            } finally {
                reentrantLock.unlock();
            }
        },"t1").start();


        new Thread(()->{
            reentrantLock.lock();

            try {
                System.out.println(Thread.currentThread().getName()+"外层锁");


                try {
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName()+"中层锁");
                } finally {
                    reentrantLock.unlock();
                }
            } finally {
                reentrantLock.unlock();
            }
        },"t2").start();
    }
}

五、死锁及排查

1、死锁是什么

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁 67.png

package com.lori.juc2023.juc3;

import java.util.concurrent.TimeUnit;

public class DeadLockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();
    public static void main(String[] args){
        Thread a = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");

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

                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                }
            }
        }, "a");
        a.start();

        new Thread(() -> {
            synchronized (lockB){

                System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");

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

                synchronized (lockA){

                    System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");
                }
            }
        },"b").start();


    }
}

68.png

2、产生死锁的原因

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 资源分配不当

3、如何排查死锁

PS D:\javaProject\JUC2023> jps -l
10272 com.lori.juc2023.juc3.DeadLockDemo
23408 org.jetbrains.jps.cmdline.Launcher
24440 sun.tools.jps.Jps
38700 
PS D:\javaProject\JUC2023> jstack 10272

69.png