Java多线程详解(3)线程同步和锁

队列

处理多线程问题时,多个线程访问一个对象并修改数据库时,可能破坏事务的四大特性(原子性、一致性、隔离性、持久性),因此我们要采取队列和锁(缺一不可),就好像上图厕所排队,请问你怎么才能安全和安心的上一个厕所?这时候首先得有序排队(队列)避免插队冲突,第二 人进厕所得上锁(加锁)避免在你未完成的情况下别人进去干扰你


线程同步(保证线程安全)

当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用完成后释放锁即可,但会引起以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致性能慢

同步方法

  • 我们通过Private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提供机制,也就是synchronized 关键字,它包括两种用法:synchronized方法和synchronized块
  • synchronized方法控制对 对象 的访问,每一个对象对应一把锁,每一个synchronized方法都必须获得调用该方法对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

不安全案例代码

public class TestLock {

    public static void main(String[] args) {
        User user = new User(1000,"小明");
        Bank bank = new Bank(user,300);
        //四个人去银行取钱
        new Thread(bank,"小明").start();
        new Thread(bank,"小明老婆").start();
        new Thread(bank,"小明妈妈").start();
        new Thread(bank,"小明爸爸").start();

    }
}


//个人账户
class User{
    //账户的钱和名字
    int totalMoney;
    String name;

    public User(int totalMoney, String name){
        this.totalMoney = totalMoney;
        this.name = name;
    }

}

//银行取款
class Bank implements Runnable{
    User user;
    //要取的钱和个人拥有的现金
    int getMoney;
    int money;

    public Bank(User user,int getMoney){
        this.user = user;
        this.getMoney = getMoney;
    }

    @Override
    public void run() {
        if(getMoney> user.totalMoney){
            System.out.println("卡里余额不足");
            return;
        }
        //设置延时是为了满足四个人在知道卡里有钱的情况下同时取钱
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        money = getMoney;
        user.totalMoney = user.totalMoney-getMoney;
        System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
    }
}

运行结果:

小明老婆取了300
小明的卡余额为:400
小明取了300
小明的卡余额为:400
小明妈妈取了300
小明的卡余额为:400
小明爸爸取了300
小明的卡余额为:400

!结果很明显是银行亏大了

1.synchronized方法保证线程安全

  • 同步方法无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,也可以或者是class(反射)
    @Override
    public synchronized void run() {
        if(getMoney> user.totalMoney){
            System.out.println("卡里余额不足");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        money = getMoney;
        user.totalMoney = user.totalMoney-getMoney;
        System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
    }

2.synchronized块保证线程安全

  • 同步块 synchronized(Obj){…}
  • Obj可以称为同步监视器
    1.Obj可以是任何对象,推荐使用共享资源作为同步监视器
  • 同步监视器的执行过程
    1.第一个线程访问,锁定同步监视器,执行其中代码
    2.第二线程访问,发现同步监视器被锁定,无法访问
    3.第一个线程访问结束,解锁同步监视器
    4.第二个线程访问,发现同步监视器没有锁,然后锁定访问
    @Override
    public void run() {
        synchronized (user){
            if(getMoney> user.totalMoney){
                System.out.println("卡里余额不足");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money = getMoney;
            user.totalMoney = user.totalMoney-getMoney;
            System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
        }
    }

如果Bank继承的是Thread类,采用synchronized方法(在修饰run方法加synchronized)是不能保证线程安全的,因为创建多线程时,synchronized方法锁的是Bank,而操作对象增删改的是User,synchronized方法锁的是当前实例(this)。而对于上述实现Runnable接口的Bank类,采取synchronized方法锁的虽然是Bank,但不会出现问题,因为代理对象操作的同一资源(进同一银行得排队),没有代理对象的话是多个银行取同一资源(账户的钱),锁Bank是解决不了问题的,因为其他银行操作不需要另一个银行的锁,只需要User的锁


死锁问题的导致

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

案例代码

public class TestDeadLock {
    public static void main(String[] args) {
        PlayGame playGame = new PlayGame();
        new Thread(playGame,"a").start();
        new Thread(playGame,"b").start();
    }
}

class Lol{}

class Dnf{}

class PlayGame implements Runnable{
    //可以理解为资源只有一份(只有一个游戏账户)
    static Lol lol = new Lol();
    static Dnf dnf = new Dnf();

    @Override
    public void run() {
        try {
            playGame();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    private void playGame() throws InterruptedException {
        if(Thread.currentThread().getName()=="a"){
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
                Thread.sleep(1000);
                synchronized (dnf){
                    System.out.println(Thread.currentThread().getName()+"同时获得DNF的锁");
                }
            }
        }else {
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
                Thread.sleep(2000);
                synchronized (lol){
                    System.out.println(Thread.currentThread().getName()+"同时获得LOL的锁");
                }
            }
        }
    }
}

a线程访问锁定 lol 同步监视器,b线程访问锁定 dnf 同步监视器,接下来a没执行完synchronized代码块(相当于lol没下线还玩着)还想同时玩 dnf,但dnf 同步监视器已经被 b 线程锁定了(导致无法登录),于是等待b释放锁。但b也没执行完synchronized代码块(相当于dnf没下线还玩着)还想同时玩 lol,但lol同步监视器已经被 a 线程锁定了(导致无法登录),于是双方等待对方释放锁资源(但对方都很倔强,如果a没等到b释放dnf的锁死都不会释放lol的,b也和a有着一样的想法),最终宇宙毁灭那时候双方都只玩过其中一个游戏,这就是死锁问题的发生。

死锁问题的解决

    private void playGame() throws InterruptedException {
        if(Thread.currentThread().getName()=="a"){
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
                Thread.sleep(1000);
            }
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
            }
        }else {
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
                Thread.sleep(2000);
            }
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
            }
        }
    }

不要锁中加锁,a释放 lol 同步资源器的条件是你已经玩完了,而不是要等 dnf 的同步资源器可以访问才释放lol的锁,b的思想也一样。


ReentrantLock(可重入锁)

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

不安全案例代码

public class TestReentrantLock {

    public static void main(String[] args) {
        SaleTicket saleTicket = new SaleTicket();
        new Thread(saleTicket,"小明").start();
        new Thread(saleTicket,"小胖").start();
    }
}

class SaleTicket implements Runnable{
    private int ticket = 10;

    @Override
    public void run() {
        try {
            while (true) {
                if(ticket>0){
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"买了第"+ticket--+"票");
                }else{
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ReentrantLock(可重入锁)解决方法

class SaleTicket implements Runnable {
    private int ticket = 10;

    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        try {
            while (true) {
                reentrantLock.lock();//加锁
                if (ticket > 0) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "票");
                } else {
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();//解锁
        }
    }
}

这时候就是小明买票没完成,;另外一个是不能买票的,在实际的业务代码里面,小明不可能一直买票直到票没有,买了一张就释放锁了,有兴趣的朋友可以自己写代码测试

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值