Java多线程学习之路(三)---解决共享资源问题线程同步和锁

Java多线程学习之路(三)—解决共享资源问题线程同步(更新中)

多线程不仅会带来各种好处,但是也对程序员提出了更高的要求,其中最重要的就是如何解决多个线程与共享资源的关系.多个线程经常会彼此干涉从而造成麻烦.比如 一个多线程的银行系统,一个用户的账户余额现在是500元.现在用户在商场购物,刷卡付费300元(一个线程),于此同时他的儿子偷偷的用他的账户给网游充值400(另一个线程).两个线程同时访问该数据,而且当时访问的数据都是账户余额500元,如果没有同步机制.那么这两个操作都会成功.显然这会给银行系统造成巨大损失.

3.1 多线程问题模拟

下面是代码模拟上文场景.多线程如果不进行同步可能会造成的巨大问题.

背景:一个银行账号有1000的余额,然后多个用户同时去消费100元.

信用卡账户类

public class CreditCard implements Runnable
{
    
    
    private Double balance=1000D;
    private Double spendMoney;
    public CreditCard(Double spendMoney)
    {
    
    
        this.spendMoney = spendMoney;
    }
    public void run()
    {
    
    
        try
        {
    
    
            Thread.sleep(1000);//通过sleep方法增加多个线程同时访问变量的可能性
        } catch (InterruptedException e)
        {
    
    
            e.printStackTrace();
        }
        if (balance>spendMoney)
        {
    
    
            balance-=spendMoney;
            System.out.println(Thread.currentThread().getName()+" : "+balance);
        }
    }
}

主函数所在类:多个线程

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main
{
    
    
    public static void main(String[] args)
    {
    
    
        CreditCard creditCard = new CreditCard(100D);
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++)
        {
    
    
            executorService.execute(creditCard);
        }
    }
}

运行结果: 可以看到如果按照逻辑,这样的线程任务只能有完成10个(余额为1000,每个线程执行一个任务,对余额扣款100),但是结果出乎意料20个线程任务都完成了.如果转换为现实场景的话,那就是1张卡,20个用户在极短的时间段内对账号进行访问并消费100元,但是全部都消费成功了.可是余额只有1000.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l2acufvg-1604740641886)(C:\Users\暗谷幽兰\AppData\Roaming\Typora\typora-user-images\image-20201104223323637.png)]

那么如何防止此类事情发生呢? 那就是同步,将可能产生问题的代码进行同步处理,即当一个线程在访问可能会产生问题的代码的时候,阻止别的线程对其进行访问.

3.2 synchronized同步代码块

用synchronize的关键字将代码"关"起来,并且给它一个对象锁,这个锁可以是任意对象,这样不管是哪个线程想要访问这个代码段都必须获得这个锁.当一个线程获取到这个锁的时候(把门关上),其他的线程只能等该线程运行完成(把门打开),才能获取到这个锁.

更改后的代码:

public class CreditCard implements Runnable
{
    
    
    private Double balance=1000D;
    private Double spendMoney;
    Object object=new Object();
    public CreditCard(Double spendMoney)
    {
    
    
        this.spendMoney = spendMoney;
    }
    public void run()
    {
    
    
        synchronized (object)
        {
    
    
            try
            {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e)
            {
    
    
                e.printStackTrace();
            }
            if (balance>=spendMoney)
            {
    
    
                balance-=spendMoney;
                System.out.println(Thread.currentThread().getName()+" : "+balance);
            }
        }
    }
}

运行结果:这个时候就恢复到正常了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGAb5oAQ-1604740641888)(C:\Users\暗谷幽兰\AppData\Roaming\Typora\typora-user-images\image-20201104225956046.png)]

3.3synchronized同步方法

与同步代码段相似,同步方法就是在访问共享共享资源的方法前面加上synchronized 声明,并且不需要显式的声明其对象锁,因为它的对象锁就是调用该方法的对象本身,如果该方法还是static,也就是说它不属于某一特定的对象,那么它的对象所就是该类的字节码文件 如本文的 CreditCard.class

public class CreditCard implements Runnable
{
    
    
    private Double balance = 1000D;
    private Double spendMoney;

    public CreditCard(Double spendMoney)
    {
    
    
        this.spendMoney = spendMoney;
    }

    public  void run()
    {
    
    
        spendMoney();
    }

    public synchronized void spendMoney()
    {
    
    
        try
        {
    
    
            Thread.sleep(200);
        } catch (InterruptedException e)
        {
    
    
            e.printStackTrace();
        }
        if (balance >= spendMoney)
        {
    
    
            balance -= spendMoney;
            System.out.println(Thread.currentThread().getName() + " : " + balance);
        }
    }
}

3.4 使用显式的Lock类来进行同步.

Lock的用法也十分的简单,只需要创建一个Lock的对象,并且在需要的代码段前后先后调用其 lock() 和 unlock() 方法,但是需要注意的事,使用的lock()方法,就一定要unlock(),所以使用try finally 是必须的.因为如果没有 unlock(),锁对象就永远不会释放.

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

public class CreditCard implements Runnable
{
    
    
    private Double balance = 1000D;
    private Double spendMoney;
    private Lock lock=new ReentrantLock();

    public CreditCard(Double spendMoney)
    {
    
    
        this.spendMoney = spendMoney;
    }

    public  void run()
    {
    
    
        spendMoney();
    }

    public synchronized void spendMoney()
    {
    
    
        lock.lock();
        try
        {
    
    
            try
            {
    
    
                Thread.sleep(200);
            } catch (InterruptedException e)
            {
    
    
                e.printStackTrace();
            }
            if (balance >= spendMoney)
            {
    
    
                balance -= spendMoney;
                System.out.println(Thread.currentThread().getName() + " : " + balance);
            }
        } finally
        {
    
    
            lock.unlock();
        }
    }
}

乐观锁(Optimistic Lock)和悲观锁(Pessimistic Lock)

1.乐观锁

乐观锁就是在并发过程中对数据进行读写的时候采取乐观的态度,当多个线程同时申请一个共享资源的时候,都可以访问到共享资源,但是当它们提交对共享资源的更新的时候会进行数据冲突检查,如果产生了冲突,则会拒绝这次提交,并将错误信息返回给用户.可以想象,如果多个线程只对数据进行读操作,那么乐观锁会非常有用,因为大家很简单地就访问到了数据.**所以乐观锁适用于读操作很多的场景,**只有少量的写操作需要进行检查.

在这里插入图片描述

在这里插入图片描述

2.悲观锁

与乐观锁相反,悲观锁采用的是一种相对保守,悲观的行为.当多个线程对共享资源进行访问的时候,只能有一个线程获取到共享资源的对象锁,其他线程进入到Blocked状态.直到该线程完成对对象锁的释放.
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44823898/article/details/109549685