• Java多线程


    一、线程同步

    1)模拟多个用户同时从银行账户里面取钱

    ● Account 类:银行账户类,里面有一些账户的基本信息,以及操作账户信息的方法

    // 模拟银行账户
    class Account {
    
        private String accountNo;// 账号
        private double balance;// 余额
    
        public Account() {
        }
    
        public Account(String accountNo, double balance) {
            super();
            this.accountNo = accountNo;
            this.balance = balance;
        }
    
        public String getAccountNo() {
            return accountNo;
        }
    
        public void setAccountNo(String accountNo) {
            this.accountNo = accountNo;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    }
    Account.java

    ● DrawThread 类继承了Thread,是一个多线程类,用于模拟多个用户操作同一个账户的信息

    class DrawThread extends Thread {
    
        private Account account;// 银行账户
        private double money;// 操作金额
    
        public DrawThread(String name, Account account, double money) {
            super(name);
            this.account = account;
            this.money = money;
        }
    
        // 多个线程修改同一个共享数据,可能发生线程安全问题
        @Override
        public void run() {
            if (account.getBalance() > money) {
                System.out.println("【" + getName() + "】取钱:" + " " + money);
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance() - money);
                System.out.println("	账户余额为" + " " + account.getBalance());
            } else {
                System.out.println("【" + getName() + "】取钱:余额不足");
            }
        }
    }

    ● 测试类

    public class Test {
        public static void main(String[] args) {
            // 初始账户
            Account account = new Account("88888888", 1000);
            // 模拟两个线程同时操作账号
            new DrawThread("甲", account, 800).start();
            new DrawThread("乙", account, 800).start();
        }
    }

    我们现在希望实现的操作是模拟多个用户同时从银行账户里面取钱,如果用户取钱数小于等于当前账户余额,则提示取款成功,并将余额减去取款钱数,如果余额不足,则提示余额不足,取款失败。

     - 运行程序可能会看到如下运行结果:

    同一账户同时操作,造成数据异常。因为线程调度的不确定性,所以出现了线程安全问题。

    2)线程安全问题产生的原因

    ① 多个线程在操作共享数据
    ② 操作共享数据的线程代码有多条
    当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

    3)解决思路

    ① 将多条操作的共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其它线程时不可以参与运算。
    ② 必须要当前线程把这些代码执行完毕后,其它线程才可以参与运算。

    4)解决线程安全问题

    在java中,使用同步解决这个问题

    ● 同步的好处:解决了线程安全的问题

    ● 同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁

    ● 同步的前提:同步中必须有多个线程并使用同一个锁

    ① 同步代码块 - 使用的锁可以是任意对象 - 通常在继承Thread中使用

    synchronized(obj){
        // 需要被同步的代码;
    }

    ② 同步方法synchronized作为函数修饰符

    普通方法同步:使用的锁是 当前对象 固定的this

    静态方法同步:使用的锁是 当前类的Class对象,可以用getClass()方法获取,也可以用当前 类名.class表示

    public synchronized void method(){
      // 需要被同步的代码;
    }

    ③  同步锁 - 通常在实现Runnable中使用

    从Java5开始,Java提供了一种功能更强大的线程同步机制 —— 通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。

    Lock提供了比 synchronized 方法和 synchronized代码块 更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的 Condition 对象。

    在实现线程安全的控制中,比较常用的是 ReentrantLock(可重入锁)。使用该Lock对象可以显式加锁、释放锁。

    ● Lock接口(互斥锁):替代了同步代码块或同步函数,将同步的隐式锁操作变成了实现锁操作。同时更为灵活,可以一个锁使用多组监视器。

    lock():获取锁

    unlock():释放锁,通常需要定义在finally代码块中。

    ● Condition接口(监视器):将这些监视器方法单独进行了封装,变成了Condition监视器对象。可以任意锁进行组合

    await():替代了Object中的 wait 方法。

    signal():替代了Object中的 notify 方法。

    signalAll():替代了Object中的 notifyAll 方法。

    class D {
        // 定义锁对象
        private final ReentrantLock lock = new ReentrantLock();
        // ...
    
        // 定义需要保护线程安全的方法
        public void method() {
            // 加锁
            lock.lock();
            try {
                // 需要保证线程安全的代码
                // ...method body
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }

    5)将上面银行中DrawThread类作如下修改

    class DrawThread extends Thread {
    
        private Account account;// 模拟用户账户
        private double money;// 操作金额
    
        public DrawThread(String name, Account account, double money) {
            super(name);
            this.account = account;
            this.money = money;
        }
    
        // 多个线程修改同一个共享数据,可能发生线程安全问题
        @Override
        public void run() {
            
            // 使用account作为同步监视器,任何线程在进入下面同步代码块之前
            // 必须先获得account账户的锁定,其他线程无法获得锁,也就无法修改它
            // 这种做法符合:"加锁-修改-释放锁"的逻辑
            synchronized (account) {
                if (account.getBalance() > money) {
                    System.out.println("【" + getName() + "】取钱:" + " " + money);
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    account.setBalance(account.getBalance() - money);
                    System.out.println("	账户余额为" + " " + account.getBalance());
                } else {
                    System.out.println("【" + getName() + "】取钱:余额不足");
                }
            }
        }
    }
    
    public class Test {
    
        public static void main(String[] args) {
            // 初始账户
            Account account = new Account("88888888", 1000);
            // 模拟两个线程同时操作账号
            new DrawThread("甲", account, 800).start();
            new DrawThread("乙", account, 800).start();
        }
    }

    再次运行的结果:

    二、线程死锁(应避免发生)

    当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁岀现

    一旦岀现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

    死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁

    class Mylock// 建立静态锁对象
    {
        public static Object locka = new Object();
        public static Object lockb = new Object();
    }
    
    class Test implements Runnable {
        private boolean flag;
    
        Test(boolean flag) {
            this.flag = flag;
        }
    
        public void run() {
            if (!flag) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "...run...");
                    synchronized (Mylock.locka)// 线程0持a锁进来时if,
                    {
                        System.out.println(Thread.currentThread().getName() + "...if...locka");
                        synchronized (Mylock.lockb)// 线程0持b锁进来时if,线程1持有锁b锁还没有释放,进入阻塞状态
                        {
                            System.out.println(Thread.currentThread().getName() + "...if...lockb");
                        }
                    }
                }
            } else {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "...run...");
                    synchronized (Mylock.lockb)// 线程1持b锁进入else,
                    {
                        System.out.println(Thread.currentThread().getName() + "...elae...lockb");
                        synchronized (Mylock.locka)/// 线程1持a锁进来时else,线程0持有锁a锁还没有释放,进入阻塞状态
                        {
                            System.out.println(Thread.currentThread().getName() + "...elae...locka");
                        }
                    }
                }
            }
        }
    }
    
    class DeadLockTest {
        public static void main(String[] args) {
            new Thread(new Test(false), "A线程").start();
            new Thread(new Test(true), "B线程").start();
        }
    }

    运行结果:

    从运行结果可看到,线程A拿到了a锁,并尝试去获取b锁,与此同时线程B拿到了b锁并尝试去获取a锁,此时线程A和线程B就陷入了无限的等待,形成死锁。

    当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。

    1)预防死锁

    下面介绍几个常见方法:

    避免一个线程同时获取多个锁

    避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源

    尝试使用定时锁,使用 lock.tryLock 来代替使用内置锁

  • 相关阅读:
    JS判断鼠标从什么方向进入一个容器
    jQuery最核心的基础设施之一——数据缓存模块进化史
    判定模块加载时是否存在循环依赖
    mass Framework fx模块 v4
    一个简单的加载系统
    MVC历史演化
    Mozilla Firefox 17 正式发布
    javascript 堆栈与列队
    被迫才是进步的原动力(转)
    jquery1.83 之前所有与异步列队相关的模块的演变回顾
  • 原文地址:https://www.cnblogs.com/Dm920/p/12486423.html
Copyright © 2020-2023  润新知