多线程基础-02.线程间的通信与线程安全问题

1.专业术语

临界区Critical Section

  • 一个查询运行多个线程本身没有问题
  • 问题在于多个线程访问共享资资源
    • 多个线程读共享资源时也不会发生问题
    • 在多个线程对共享操作发生指令交错时就会发生问题
  • 一段代码如果存在共享资源的多线程读写操作,称这段代码块为临界区

竞态条件Race Condition
多个线程在临界区执行,由于代码的执行顺序不同而导致结果无法预测,称之为竞态条件

2.synchronized解决方案

应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案: synchronized,Lock
非阻塞式的解决方案: 原子变量

synchronized语法:

synchronized(同一对象) // 线程1, 线程2(blocked)
{
 临界区
}

建议锁对象前可以加上final保证锁住的引用对象不会变换

synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断。

优化:

  • 将数据抽象成一个类,并将数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“
  • 将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数
    据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各
    个 Runnable 对象调用外部类的这些方法。
    synchronized加在方法上:
class Test{
   public synchronized void test() {
 
   }
}

//等价于
class Test{
   public void test() {
       synchronized(this) {
 
       }
   }
}
class Test{
   public synchronized static void test() {
   
   }
}
//等价于
class Test{
   public static void test() {
       synchronized(Test.class) {
 
       }
   }
}

3.变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则不一定
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

对象行为不确定的方法,可能导致不安全发生,被称之为外星方法,如果不想向外暴露,可以修饰为private或者final
举例:经典的String类型使用了final修饰,不可变,保证了线程安全

局部变量线程安全分析
链接: JVM内存结构.

// 线程安全
public static  void test1() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    System.out.println(sb.toString());
}
// 线程不安全,引用了外部StringBuilder
public static  void test2(StringBuilder sb) {
    sb.append("a");
    sb.append("b");
    System.out.println(sb.toString());
}
// 线程不安全,返回了一个引用,其他对象可能拿到该引用区修改它
public static  StringBuilder test3() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    return sb;
}

private和final也可以在线程安全中起到作用,防止方法重写,类继承时子类无法重写。(开闭原则CLOSE)

常见的线程安全的类
String、Integer、StringBuffer、Random、Vector、HashTable、java.util.concurrent包下的类等

这里说它们是线程安全的是指:多个线程调用它们的同一个实例中的某个方法是,是线程安全的。
它们的每个方法是原子的,但是多个方法组合在一起就不是原子的了
举例:
在这里插入图片描述
如果有多个需要保护的对象可以锁住类,但是效率不佳

class Account {
    private int money;

    public Account(int money) {this.money = money;}

    public int getMoney() {return this.money;}

    public void setMoney(int money) {this.money = money;}

    // 模拟转账
    public void transfer(Account target, int amount) {
        synchronized (Account.class) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

4.Minitor

4.1.Java对象头

Java对象通常由两部分组成,对象头和成员变量。
以32位虚拟机为例:
普通对象
在这里插入图片描述
数组对象
在这里插入图片描述
MarkWord结构
在这里插入图片描述

4.2.Monitor(监视器锁)

Monitor 被翻译为监视器锁管程,每个Java对象都可以关联一个 Monitor对象,如果使用 sychronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置为指向Monitor对象。
Minitor结构:
在这里插入图片描述

  • 刚开始是Moinitor为NULL
  • 当Thread执行到synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Moinitor只能有一个所有者
  • 在Thread上锁过程中,如果Thread-3、Thread-4、Thread-5也执行到synchronized(obj),就会进入到EntryList BLOCKED
  • Thread-2同步代码块执行完毕,就会唤醒EntryList中等待线程来竞争锁,竞争时是非公平的
  • 图中WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足而放弃锁进入WAITING等待状态的线程(详细见本篇后文)

注意:

  • synchronized 必须是进入同一个对象的 Monitor 才会有以上效果
  • 不加 sychronized 对象不会关联 Monitor,不尊遵从以上规则

字节码角度理解synchronized加锁原理:
在这里插入图片描述
对应字节码为:
在这里插入图片描述

5.sychronized锁优化

链接: JVM内存模型.

6.wait与notify

为什么需要wait ?
某个线程由于条件不满足不能继续进行运算,但是却又不能一直占用着锁,其他线程一直阻塞,影响效率,当满足条件后调用notify方法重新唤醒线程,进入等待队列。

6.1 wait/notify原理

在这里插入图片描述

  • Owner线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU
  • BLOCKED线程会在Owener线程释放锁时被唤醒
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后进入EntryList重新竞争

6.2.API介绍

  • obj.wait() 让进入object监视器的线程到waitSet等待
  • obj.wait(long timeout) 让进入object监视器的线程到waitSet等待一定时间
  • obj.notify() 在object上正在waitSet等待的某个线程唤醒(随机)
  • obj.notifyAll() 让obj上正在waitSet等待的线程全部唤醒
    它们都是线程间进行协作通信的方法,都属于Object的方法,必须获得此对象的锁才能调用这几个方法,否则会抛出异常

6.3.正确的使用wait notify

sleep与wait的区别:

  • sleep是Thread的static方法,wait是Object的方法
  • sleep不需要强制和synchronized配合使用,但wait需要
  • sleep在睡眠时不会释放锁对象,但wait会释放锁对象

多个线程时notify可能导致唤醒错误(唤醒的对象不对(虚假唤醒))
notifyAll 只能解决部分虚假唤醒问题(全部唤醒,但有对象条件仍不满足,导致无法继续执行逻辑)

正确方法:

synchronized(lock) {
	while(条件不成立) {
		lock.wait();
	}
	doSomething();
}

//另一个线程
synchronized(lock) {
	while(条件不成立) {
		lock.notifyAll();
	}
}

6.4.设计模式保护性暂停

场景:用于一个线程等待另一个线程的执行结果
要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程,不能使用此模式,可以使用消息队列
  • JDK中,join的实现、Future的实现,采用的就是此模式
  • 因为要等到另一方的结果,因此归类到同步模式

在这里插入图片描述
示例:
一个线程等待另一个线程给某个对象赋值
在这里插入图片描述
增加超时效果:
在这里插入图片描述
保护性暂停应用之join源码:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

6.5.park与unpark

Park与UnPark:
它们是LockSupport类中的方法

// 暂停当前线程
LookSupport.patk();

//恢复线程的运行
LockSuport.unpark(暂停线程对象);
public static void main(String[] args) {

        Thread t0 = new Thread(new Runnable() {

            @Override
            public void run() {
                Thread current = Thread.currentThread();
                log.info("{},开始执行!",current.getName());
                for(;;){//spin 自旋
                    log.info("准备park住当前线程:{}....",current.getName());
                    LockSupport.park();
                    log.info("当前线程{}已经被唤醒....",current.getName());
                }
            }

        },"t0");

        t0.start();

        try {
            Thread.sleep(5000);
            log.info("准备唤醒{}线程!",t0.getName());
            LockSupport.unpark(t0);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
-----------------------------------------------------------------------
执行结果
21:55:14.621 [t0] INFO com.zhe.lock.Thread_LockSupport - t0,开始执行!
21:55:14.637 [t0] INFO com.zhe.lock.Thread_LockSupport - 准备park住当前线程:t0....
21:55:19.636 [main] INFO com.zhe.lock.Thread_LockSupport - 准备唤醒t0线程!
21:55:19.636 [t0] INFO com.zhe.lock.Thread_LockSupport - 当前线程t0已经被唤醒....
21:55:19.636 [t0] INFO com.zhe.lock.Thread_LockSupport - 准备park住当前线程:t0....

在这里插入图片描述
在这里插入图片描述

特点:
与Object的wait & notify相比

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
  • park和unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个,notifyAll只能唤醒所有,不能【精确】唤醒
  • park和unpark可以先unpark而wait和notifyl不能先notify,先unpark也能恢复线程的运行

原理:
每个线程都有自己的Parker对象,由三部分组成_counter,_cond和_mutex。

线程就像一个旅人,Parker就是他随身携带的背包,
条件变量_cond就好比背包中的帐篷。_counter就好比背
包中的备用干粮(0为耗尽,1为充足)。
调用park就是看需不需要停下来休息
如果备用干粮耗尽,你们钻进帐篷休息
如果备用干粮充足,你们不需要停留,继续其前进
调用unpark就好比干粮充足
如果这时还在帐篷,就唤醒他继续前进
如果这时线程还在运行,你们他下次调用park时,仅是
消耗掉备用干粮,不需要停留,继续前进
因为背包空间有限,多次调用unpark仅会补充一份备用
干粮

调用park:
在这里插入图片描述
1.当前线程调用UnSafe.park()方法
2.检查counter,本情况为0,这时获得_mutex互斥锁
3.线程进入_cond条件变量阻塞
4.设置_counter = 0

先调用park后调用unpark:
在这里插入图片描述
1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2.唤醒_cond条件变量中的Thread_0
3.Thread_0恢复运行
4.设置_counter为0

先调用unpark后调用park:
在这里插入图片描述
1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2.当前线程调用Unsafe.park()方法
3.检查_counter,本情况为1,这时线程无需阻塞,继续运行
4.设置_counter为0

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
嗨!很高兴回答你关于Python游戏脚本入门的问题。在Python中,多线程是一种并发处理的技术,它允许程序同时执行多个线程,从而提高程序的运行效率和响应能力。在游戏开发中,多线程可以用于处理游戏中的多个任务或实现并行计算。 要在Python中使用多线程,可以使用内置的`threading`模块。下面是一个简单的示例,演示如何在Python中创建和启动多个线程: ```python import threading def task(): # 这里是线程要执行的任务 print("Hello, I'm running in a thread!") # 创建线程对象 thread1 = threading.Thread(target=task) thread2 = threading.Thread(target=task) # 启动线程 thread1.start() thread2.start() ``` 在上面的示例中,我们首先定义了一个`task`函数,这是线程要执行的具体任务。然后,我们使用`threading.Thread`类创建了两个线程对象`thread1`和`thread2`,并将`task`函数作为参数传递给它们。最后,我们调用`start`方法来启动这两个线程多线程的执行是并发的,所以你可能会看到输出信息交替出现。在实际的游戏开发中,你可以利用多线程来处理不同的游戏逻辑、计算复杂的物理模拟或者处理网络通信等任务,从而提升游戏的性能和玩家体验。 但是需要注意的是,多线程编程需要注意线程的同步和资源竞争问题。在游戏开发中,你可能需要使用锁和同步原语来确保线程的安全操作。 希望这个简单的介绍对你有所帮助!如果你有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值