JUC编程之锁详解

1. 概述

Juc简称是并发包的简写。在jdk1.5之后,并发包新增了Lock接口,以及很多Lock锁的实现类用来实现锁的功能,Lock锁提供了和Synchroized类似的功能,但是Lock锁需要手动加锁和释放锁。
Lock锁的实现:
JUC编程之锁详解_第1张图片

2.锁的可重入性

锁的可重入是指,当一个线程获得一个对象锁后,再次请求该对象锁时依旧可以获得锁。

public class Ticket {
    private synchronized void print1(){
        System.out.println("进入同步方法1...");
        print2();
    }

    private synchronized void print2(){
        print3();
        System.out.println("进入同步方法2...");
    }

    private synchronized void print3(){
        System.out.println("进入同步方法3...")
    }
}

public class TicketMain{

    public static void main(String[] args){
        //创建资源类
        Ticket t = new Ticket();
        //创建线程
    	new Thread(() ->{
            t.ptint1();
        },"TicketThreadA").start();
    }
}

预计执行结果:
线程执行print1()方法,默认this作为锁的对象,在print1()方法 中调用print()2方法,需要的注意的是锁依旧是当前this对象,这里可以得出sychronized锁是可重入性的,如果不是,可能会导致死锁

3. ReentrantLock可重入锁

调用 lock()方法获得锁,调用 unlock()释放锁,同样可以重入锁。
ReentrantLock锁有两个模式:
公平锁 ----> new ReentrantLock(true)
解释:可以先来先到
非公平锁 ----> new ReentrantLock() 默认是非公平锁
解释:可以插队

原理图:
JUC编程之锁详解_第2张图片

下面我们看一个ReentrantLock可重入锁的模版代码

    //默认是非公平锁
    Lock lock = new ReentrantLock();
    //不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
    lock.lock();
    try{
        //执行业务逻辑
    }finally{
        //一定要在try的finally内释放锁
        lock.unlock();
    } 

ReentrantLock可重入锁的案例代码:

public class Ticket{
    private int number = 30;

    Lock lock = new ReentrantLock();

    public void sale(){
        lock.lock();
        try{
            //票数大于0继续卖
            if(number>0){
         		System.out.println(Thread.currentThread().getName()+"卖出了"+ (number--)+"票,剩余:"+number);
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
}


public class Ticket Main {

    public static void main(String[] args){
        
    }
}

4.Synchronized 和 Lock 区别

  • synchronized是一个Java关键字,Lock是一个类。
  • synchronized无法判断获取锁的状态,Lock可以判断获取锁的状态。
  • synchronized会自动释放锁,Lock需要自己手动释放锁,如果不手动释放锁,会导致死锁。
  • synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去。
  • synchroized是可重入锁,不可以中断且非公平锁。Lock锁,可重入锁,可判断锁,默认非公平锁,可以自己设置为公平锁。

5. 生产者和消费者问题

5.1 synchronized版本的生产者消费者

package cn.bugoverflow.thread.pc;

/**
 *
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒 
 * 线程交替执行 A B 操作同一个变量 num = 0 
 * A num+1 
 * B num-1
 * Description: 生产者消费者问题
 * 
* ProducerConsumerDemo * * @author laiql * @date 2022/10/17 14:22 */
public class ProducerConsumerDemo { public static void main(String[] args) { Consumer consumer = new Consumer(); consumer.listener(new Producer()); } } class Producer { private int number = 0; /** * 递增 */ public synchronized void increment() throws InterruptedException { if (number != 0) { //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName() + "-->" + number); //通知其他线程 this.notifyAll(); } /** * 递减 */ public synchronized void decrement() throws InterruptedException { if (number == 0) { //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName() + "-->" + number); //通知其他线程 this.notifyAll(); } } class Consumer { /** * 消费者监听器 * * @param producer 生产者 */ public void listener(Producer producer) { new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.increment(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.decrement(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "B").start(); } }

存在问题,ABCD四个线程,虚假唤醒

JUC编程之锁详解_第3张图片

我们根据官方文档的提示将if判断改成while循环

package cn.bugoverflow.thread.pc;

/**
 *
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒 
 * 线程交替执行 A B 操作同一个变量 num = 0 
 * A num+1 
 * B num-1
 * Description: 生产者消费者问题
 * 
* ProducerConsumerDemo * * @author laiql * @date 2022/10/17 14:22 */
public class ProducerConsumerDemo { public static void main(String[] args) { Consumer consumer = new Consumer(); consumer.listener(new Producer()); } } class Producer { private int number = 0; /** * 递增 */ public synchronized void increment() throws InterruptedException { while(number !=0){ //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName() + "-->" + number); //通知其他线程 this.notifyAll(); } /** * 递减 */ public synchronized void decrement() throws InterruptedException { while(number == 0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName() + "-->" + number); //通知其他线程 this.notifyAll(); } } class Consumer { /** * 消费者监听器 * * @param producer 生产者 */ public void listener(Producer producer) { new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.increment(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.decrement(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.increment(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "C").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.decrement(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "D").start(); } }

5.2 JUC版本的生产者消费者

JUC编程之锁详解_第4张图片

通过Lock 找到 Condition
JUC编程之锁详解_第5张图片

测试代码:

package cn.bugoverflow.thread.lock;

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

public class Data {

    //公平锁
    private Lock lock = new ReentrantLock();
    //监视器1
    private Condition condition1 = lock.newCondition();
    //监视器2
    private Condition condition2 = lock.newCondition();
    //监视器3
    private Condition condition3 = lock.newCondition();

    //number=1唤醒A
    //number=2唤醒B
    //number=3唤醒C
    private int number = 1;

    public void printA() {
        lock.lock();
        try {
            while (number != 1) {
                //等待单个
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAA");
            //唤醒B
            number = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (number != 2) {
                //等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBBB");
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }


    public void printC() {
        lock.lock();
        try {
            while (number != 3) {
                //等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>CCCC");
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}


public class DataThreadMain {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        }, "A").start();


        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        }, "C").start();
    }
}

6. 八锁现象

分析:

  • 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
  • 加个普通方法后发现和同步锁无关
  • 换成两个对象后,不是同一把锁了,情况立刻变化。
  • synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
    • 具体表现为以下3种形式。
    • 对于普通同步方法,锁是当前实例对象。
    • 对于静态同步方法,锁是当前类的Class对象。
    • 对于同步方法块,锁是Synchonized括号里配置的对象
  • 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
  • 所有的静态同步方法用的也是同一把锁——类对象本身,
  • 这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

7.死锁

什么是死锁:
JUC编程之锁详解_第6张图片

如何避免死锁
(1)保持加锁顺序:当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序枷锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
(2)获取锁添加时限:上述死锁代码情况二就是因为出现了获取锁失败无限等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;
(3)进行死锁检测:我们可以通过一些手段检查代码并预防其出现死锁。
死锁检测
Java中死锁检测手段最多的就是使用JDK带有的jstack和JConsole工具了。下面我们以jstack为例来进行死锁的检测。
(1)先运行我们的代码程序。
(2)使用JDK的工具JPS查看运行的进程信息。
(3) 使用jps查看到的进程ID对其进行jstack 进程分析。
(4) 根据关键词"Found one Java-level deadlock"找到死锁。

Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x00007f9b1000a000 (object 0x0000000787ede4e8, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007f9b2800ff00 (object 0x0000000787ede4d8, a java.lang.Object),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================

8.自旋锁

概述:
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的持有者已经释放了锁,"自旋"一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者,也就说,在任何时刻最多只能有一个执行单元获得锁。

JUC里面的自旋锁:
AtomicInteger
JUC编程之锁详解_第7张图片

package cn.bugoverflow.thread.lock;

import java.util.concurrent.TimeUnit;

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();
        //底层使用的自旋锁CAS
        SpinlockDemo lock = new SpinlockDemo();
        new Thread(() -> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T2").start();
    }
}

package cn.bugoverflow.thread.lock;

import java.util.concurrent.atomic.AtomicReference;

/*** 自旋锁 */
public class SpinlockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    /**
     * 加锁
     */
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> mylock");
        //自旋锁
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }


    /**
     * 解锁
     */
    public void myUnLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> myUnlock");
        atomicReference.compareAndSet(thread, null);
    }
}

你可能感兴趣的:(多线程,服务端,java)