JavaThread 11 线程同步 → 同步方法及同步块

5.2 线程同步 → 同步方法及同步块


5.2.1 同步方法

  • 由于我们可以 通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制 就是 synchronized 关键字,它包括两种 用法:synchronized 方法 和 synchronized 块。
同步方法:
public synchronized void method(int args){}

意思就是,你如果 想要保证 数据的安全,那么就 先要 做最基本的安全措施, private 修饰一下!否则 肯定不安全。。然后呢,利用 锁 来对 方法 进行一下 针对!锁是 在用 synchronized 之后 设定的同步监视器 提供的!

synchronized 同步方法 会默认的 把调用该方法的 this 对象 设定为 同步监视器!让 this 对象来提供锁。或者说 被 synchronized 同步方法 绑定的对象,都会 给这个 对象提供个同步锁。

  • synchronized 方法控制 对 “对象” 的访问,每个对象对应一把锁,每个 synchronized() 方法都必须获得 该对象的锁 (获取的方法很简单,让这个对象 作为 同步监视器就完事了),否则线程会进入 阻塞状态,方法一旦被线程执行,线程就会独占该锁(这个锁一旦被线程独占,就被叫做 排它锁)。直到 synchronized 的代码块里正常执行完毕后 才会释放该锁,后面 被阻塞的线程 才能获得 再次操作该方法的权利,该对象也会继续提供给线程锁,然后再执行,然后再释放锁。

缺陷:若将一个大的方法申明为 synchronized 将会影响效率!(这是由于 排它锁和释放锁 上下文频繁切换导致的)

在这里插入图片描述
我们一般 多线程去处理同一个事务的时候,只读部分不会加锁进行 线程同步。只有 修改部分的代码 才会 加锁 进行 线程同步。

package www.muquanyu.syn;

//不安全的买票 案例
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();

        new Thread(station,"黄牛").start();
        new Thread(station,"小明").start();
        new Thread(station,"老师").start();


    }

}

class BuyTicket implements Runnable {
    //票
    private int ticket = 10;

//哈哈,就加了个 修饰的关键字,synchronized 同步,会用到线程身上的的锁。
    @Override
    public synchronized void run() {
        //买票
        while(ticket > 0)
        {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->买到了第"+(ticket--)+"张票");
        }
    }
}
  • 请看 输出结果,你会非常的惊讶!(这么严重的问题居然这么轻松就解决了。)

黄牛–>买到了第10张票
黄牛–>买到了第9张票
黄牛–>买到了第8张票
黄牛–>买到了第7张票
黄牛–>买到了第6张票
黄牛–>买到了第5张票
黄牛–>买到了第4张票
黄牛–>买到了第3张票
黄牛–>买到了第2张票
黄牛–>买到了第1张票
在这里插入图片描述

但是 我们说 为什么 只有 黄牛 这一个 线程 拿到 资源了呢?这其实 并不是 表面看到的那样。其实 是 每次 黄牛释放锁后,大家 三个线程 都同时 再次 抢夺 这个 资源。只不过 恰好 又被 黄牛抢到了而已。

  • 两人取钱就不可以这样方便的解决了!
package www.muquanyu.syn;

public class UnsafeBank {
    public static void main(String[] args) {
        //账户创建
        Account account = new Account(100,"结婚基金");

        Drawing you = new Drawing(account,50,"你");
        Drawing grilFriend = new Drawing(account,100,"你的女朋友");

        you.start();
        grilFriend.start();

    }
}

//账户(Java 版本的结构体)
class Account{
    int money;//余额
    String name;//卡名
    public Account( int money,String name)
    {
        this.money = money;
        this.name = name;
    }
    public Account(String name)
    {
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends  Thread{
    Account account;//账户(类的组合)

    //取了多少钱
    int drawingMoney;

    //现在手里有多少钱
    int nowMoney;

    public Drawing(Account account, int drawingMoney, String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public synchronized void run() {
        //判断有没有钱
        if(account.money - drawingMoney < 0)
        {
            System.out.println(Thread.currentThread().getName()+"-->抱歉你的钱不够!");
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money -= drawingMoney;

        //你手里的钱
        nowMoney += drawingMoney;

        System.out.println(account.name+"余额为:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);

    }
}

在这里插入图片描述
你会发现数据还是出现了错误,并发问题依然发生了,这是为什么呢?因为你 锁的东西不对!根据我们的代码,你锁的是 当前 这个 class 创建的 对象,分别是 you 和 grilFriend,相当于 我们只实现了有顺序的进入银行,但是呢 ~ 你并未 有顺序的 取款呀。你应该锁的是那个 账户,而不是 两个线程对象 ~ ~


5.2.2 同步块

  • 同步块:synchronized(Obj/* 待锁的对象 */){处理事务的代码}
  • Obj 称之为 同步监视器

1.** Obj 可以是任何对象,但是推荐 使用共享的那块儿资源作为同步监视器**

  1. 同步方法中 无需指定同步监视器,这是因为同步方法的同步监视器 默认 指向的就是 this 它自己(对象)本身。或者是 class [反射中讲解]
  • 同步监视器的执行过程
  1. 第一个线程访问,会获取同步监视器的锁,执行其中代码。

  2. 第二个线程访问,发现同步监视器被锁定,无法访问。进入阻塞状态。

  3. 第一个线程访问完毕,解除了同步监视器的锁,但是锁依然还在!可以供给第二个线程用。

  4. 第二个 线程访问,发现同步监视器上面有 释放锁,然后 就把释放锁变成了排它锁,把自己和同步监视器 锁上了,并且执行 需要执行的代码。

  • 两人取钱 可以用 同步块来解决
package www.muquanyu.syn;

public class UnsafeBank {
    public static void main(String[] args) {
        //账户创建
        Account account = new Account(100,"结婚基金");

        Drawing you = new Drawing(account,50,"你");
        Drawing grilFriend = new Drawing(account,100,"你的女朋友");

        you.start();
        grilFriend.start();

    }
}

//账户(Java 版本的结构体)
class Account{
    int money;//余额
    String name;//卡名
    public Account( int money,String name)
    {
        this.money = money;
        this.name = name;
    }
    public Account(String name)
    {
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends  Thread{
    Account account;//账户(类的组合)

    //取了多少钱
    int drawingMoney;

    //现在手里有多少钱
    int nowMoney;

    public Drawing(Account account, int drawingMoney, String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        synchronized(account){
            //判断有没有钱
            if(account.money - drawingMoney < 0)
            {
                System.out.println(Thread.currentThread().getName()+"-->抱歉你的钱不够!");
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money -= drawingMoney;

            //你手里的钱
            nowMoney += drawingMoney;

            System.out.println(account.name+"余额为:"+account.money);
            System.out.println(this.getName()+"手里的钱:"+nowMoney);
        }
    }
}

在这里插入图片描述
正常情况,它会判断出 你取 50 万,你女朋友 取 100 万,那么 当你 取50 万的时候,你女朋友 肯定就 取不出来了。

那么如何 让 女朋友 先取呢?

答:设置优先级,然后 按照优先级的 顺序 写启动代码。

 grilFriend.setPriority(2);
        you.setPriority(1);
        
        grilFriend.start();
        you.start();

在这里插入图片描述

  • 集合 问题 可以用 同步块解决

  • 错误解决:

package www.muquanyu.syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        synchronized (list){
            for(int i = 0;i < 10000;++i)
            {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            System.out.println(list.size());
        }
        
    }
}

在这里插入图片描述
原因是:你虽然把 list 对象设定为 同步监视器了。但是只有 主线程进入了队列。剩下的 所有 子线程 都没有 进入队列!这就导致了,你在做一个 无用功 …

  • 正确解决:
package www.muquanyu.syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        for(int i = 0;i < 10000;++i)
        {
            new Thread(()->{
                synchronized (list){list.add(Thread.currentThread().getName());
                    }
            }).start();
        }
        System.out.println(list.size());

    }
}

但是我们发现 最后输出的 还不是 10000,这是 为什么呢?难道出错了吗? 答案是:没出错!

原因是:主线程 执行的比较快,子线程在 刚要执行完的那一瞬间,主线程 就把 这个结果 输出了。这也是 多线程的魅力。如果多线程 无法 完成这种 多个人 同时 工作的 假象,那么 也就没有任何 意义了!!!

  • 验证 其正确性(如果我们说是因为主线程提前输出了,那么我们 把输出 放在 所有的子线程里面不就完事了吗? 这不是更准确吗?而且也验证了 同步锁 解决 并发问题的正确性。
package www.muquanyu.syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        for(int i = 0;i < 10000;++i)
        {
            new Thread(()->{
                synchronized (list){list.add(Thread.currentThread().getName());
                    }
            }).start();
        }
        System.out.println(list.size());

    }
}

在这里插入图片描述

  • 小结
  1. 同步块 比 同步方法 更加的灵活 和 好用!

  2. 锁 是从哪来的 ? 是设定 同步监视器 而来的。

  3. 如何 让 某个线程 先 被锁 ? 设置优先级 !

  4. 一定要注意 是否 你需要的线程 会进来 锁住自己呢 ? 怎么 万无一失的保证这一点 ? 必须 把 同步块 写在 线程的 事务(方法)里面! 这样就可以完美的保证 每个需要被锁的 线程 贱贱的 ~自动来找 同步块 获取到锁,把自己锁住 嘿嘿()。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值