阅读 2145

线程同步和锁 | Java多线程(五)

这是我参与更文挑战的第29天,活动详情查看: 更文挑战


相关文章

Java多线程汇总:Java多线程


前言

前面的文章介绍了并发的情况下会有数据错误的现象出现。

  • 并发:同一个对象被多个线程同时操作,也就是不同线程同时操作同一个资源地址,造成数据紊乱。
  • 同步:多个需要同时访问资源的线程进入对象的等待池,等待前面线程使用完毕。
  • 锁:每个对象都有把锁,当获取对象时,独占资源,其他线程必须等待,使用结束后才释放
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

一、同步方法

首先得介绍一下 synchronized 关键字。

public synchronized void method(int args){}
复制代码
  • synchronized:

    • 默认锁的是他自身的对象。
    • 要是跨对象,通常使用同步块。
    • 即锁共享资源所在的对象。
  • 代码示例:

/**
 * 手机抢购案例
 */
public class PhoneSnapUp implements Runnable {

    private Integer inventory = 10;//手机库存
    private boolean flag = true;

    public static void main(String[] args) {
        PhoneSnapUp phoneSnapUp = new PhoneSnapUp();
        //模拟5人同时抢购,即同时开启5个线程
        new Thread(phoneSnapUp, "丁大大1号").start();
        new Thread(phoneSnapUp, "丁大大2号").start();
        new Thread(phoneSnapUp, "丁大大3号").start();
        new Thread(phoneSnapUp, "丁大大4号").start();
        new Thread(phoneSnapUp, "丁大大5号").start();
    }

    @Override
    public synchronized void run() {
        while (flag){
            try {
//                synchronized (this) {
                    buy();
//                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //购票方法提出来
    private void buy() throws Exception {
        //当库存为0时,抢购结束
        if (inventory <= 0) {
            flag = false;
            return;
        }
        //模拟延迟,否则结果不容易看出来
        Thread.sleep(500);
        //每次抢购完,库存减1
        System.out.println("恭喜!!" + Thread.currentThread().getName() + "--抢到了一台小米12!库存还剩:" + --inventory + "台");
    }
}

复制代码
  • 执行结果如下:

在这里插入图片描述

  • 总结:同步方法可以做到保证结果不会出错。
    • 但是因为是方法,一加上synchronized 整个方法内的所有内容就相当于被加上了锁,可能会引起效率问题,导致程序阻塞~
    • 所以一般我们都是使用同步代码块来实现加锁的操作。

二、同步代码块

synchronized(Obj){}
复制代码
  • 代码案例:其他代码和同步方法一致,不在重复粘贴,这里只介绍run方法里的代码。
@Override
    public void run() {
        while (flag){
            try {
                synchronized (this) {
                    buy();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
复制代码
  • 执行结果:

在这里插入图片描述

  • 总结:

    • 结果和同步方法是一致的,证明两种方法都可以保证结果的准确性。

    • 同步代码块是指定锁住固定的东西,在方法里其他的内容不受影响。

    • 举个粗暴的例子,你拉屎去,同步方法相当于直接把厕所大门锁住,三个坑位你只使用了一个,典型的浪费资源。而同步代码块,你上了哪个坑位就锁住坑位的门,其他的人可以正常使用其他坑位,哈哈 形象~

  • 注意点:

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身或者是class
  • 执行过程:同步监视器的执行过程:

    • 第一个线程访问,锁定同步监视器,执行其中代码
    • 第二个线程访问 ,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕 ,解锁同步监视器.
    • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

三、死锁

上面一直在讲 ,那么如果两个线程同时拿到某一个共享资源时,都无法释放时,就会造成死锁现象

  • 简单粗暴的理解:

    • 两个资源 A和B
    • 丁一 拿着A资源的锁,执行完想继续拿资源B的锁
    • 丁二 拿着B资源的锁,执行完想继续拿资源A的锁
    • 两个人都没有执行完,也就是互相没有释放锁,两个人就会打起来,谁都执行不了
    • 导致死锁
  • 文字总是理解不清晰,跟着代码去走一圈肯定可以理解的:

/**
 * 模拟多线程死锁的情况
 */
public class Demo {
    public static void main(String[] args) {
        DeadLockThread deadLockThread = new DeadLockThread();
        new Thread(deadLockThread,"丁一+").start();
        new Thread(deadLockThread,"丁二+").start();
    }
}

class DeadLockThread implements Runnable {
    A a = new A();
    B b = new B();

    @SneakyThrows
    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("丁一")){
            synchronized (this){
                System.out.println(Thread.currentThread().getName());
                a.soutA();
                Thread.sleep(2000);
                b.soutB();
            }
        }else {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName());
                b.soutB();
                Thread.sleep(2000);
                a.soutA();
            }
        }
    }
}


class A {
    public void soutA(){
        System.out.println("我是A");
    }
}

class B {
    public void soutB(){
        System.out.println("我是B");
    }
}

复制代码
  • 执行结果如下:

在这里插入图片描述

  • 总结:产生死锁的四个必要条件:

    • 互斥条件: 一个资源每次只能被一个进程使用
    • 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺
    • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
  • 只要破除任意一个就能避免死锁

四、Lock锁

  • 通过显示定义同步锁对象(Lock)来实现同步

  • ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

  • 代码案例:

/**
 * 使用Lock锁
 */
public class TestLock {

    public static void main(String[] args) {
        TestLock2 testLock = new TestLock2();

        new Thread(testLock, "A").start();
        new Thread(testLock, "B").start();
        new Thread(testLock, "C").start();
    }

}

class TestLock2 implements Runnable{
    int count = 1000;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while(true)
        {
            try {
                //进入加锁状态
                lock.lock();
                if(count > 0)
                    System.out.println(Thread.currentThread().getName() + "---" +count--);
                else
                    break;
            }
            finally {
                //解锁
                lock.unlock();
            }
        }
    }
}
复制代码
  • 执行结果:

在这里插入图片描述

  • 总结:
    • 在不加锁的情况下,ABC可能会同时操作到count,导致数据紊乱
    • 在加了锁之后,ABC排队操作

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

文章分类
后端
文章标签