当前位置: 代码迷 >> 综合 >> JavaThread 11 线程同步 → 同步方法及同步块
  详细解决方案

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

热度:68   发布时间:2023-11-24 13:48:43.0

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