• Java多线程:线程安全问题


    一、线程安全问题案例

    模拟银行账户存取款

    1、账户类

    package com.lyq.java.threadsafe;
    /*
    银行账户
        不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
     */
    public class Account {
        // 账号
        private String actno;
        // 余额
        private double balance;
    
        public Account() {
        }
    
        public Account(String actno, double balance) {
            this.actno = actno;
            this.balance = balance;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        //取款的方法
        public void withdraw(double money){
            // t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
            // 取款之前的余额
            double before = this.getBalance(); // 10000
            // 取款之后的余额
            double after = before - money;
    
            // 在这里模拟一下网络延迟,100%会出现问题
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // 更新余额
            // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
            this.setBalance(after);
        }
    }

     2、实现类

    package com.lyq.java.threadsafe;
    
    public class AccountThread extends Thread {
    
        // 两个线程必须共享同一个账户对象。
        private Account act;
    
        // 通过构造方法传递过来账户对象
        public AccountThread(Account act) {
            this.act = act;
        }
    
        public void run(){
            // run方法的执行表示取款操作。
            // 假设取款5000
            double money = 5000;
            // 取款
            // 多线程并发执行这个方法。
            act.withdraw(money);
    
            System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
        }
    }

    3、测试

    package com.lyq.java.threadsafe;
    
    public class Test {
        public static void main(String[] args) {
            // 创建账户对象(只创建1个)
            Account act = new Account("act-001", 10000);
            // 创建两个线程
            Thread t1 = new AccountThread(act);
            Thread t2 = new AccountThread(act);
            // 设置name
            t1.setName("t1");
            t2.setName("t2");
            // 启动线程取款
            t1.start();
            t2.start();
        }
    }

    4、结果

    二、使用synchronized同步代码块

    1、账户类及线程实现类

    package com.bjpowernode.java.threadsafe2;
    /*
    银行账户
        使用线程同步机制,解决线程安全问题。
     */
    public class Account {
        // 账号
        private String actno;
        // 余额
        private double balance; //实例变量。
    
        //对象
        Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)
    
        public Account() {
        }
    
        public Account(String actno, double balance) {
            this.actno = actno;
            this.balance = balance;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        //取款的方法
        public void withdraw(double money){
    
            //int i = 100;
            //i = 101;
    
            // 以下这几行代码必须是线程排队的,不能并发。
            // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
            /*
            线程同步机制的语法是:
                synchronized(){
                    // 线程同步代码块。
                }
                synchronized后面小括号中传的这个“数据”是相当关键的。
                这个数据必须是多线程共享的数据。才能达到多线程排队。
    
                ()中写什么?
                    那要看你想让哪些线程同步。
                    假设t1、t2、t3、t4、t5,有5个线程,
                    你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
                    你一定要在()中写一个t1 t2 t3共享的对象。而这个
                    对象对于t4 t5来说不是共享的。
    
                这里的共享对象是:账户对象。
                账户对象是共享的,那么this就是账户对象吧!!!
                不一定是this,这里只要是多线程共享的那个对象就行。
    
                在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
                100个对象,100把锁。1个对象1把锁。
    
                以下代码的执行原理?
                    1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
                    2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
                    找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
                    占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
                    3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
                    共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
                    直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
                    t2占有这把锁之后,进入同步代码块执行程序。
    
                    这样就达到了线程排队执行。
                    这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
                    执行的这些线程对象所共享的。
             */
            //Object obj2 = new Object();
            //synchronized (this){
            //synchronized (obj) {
            //synchronized ("abc") { // "abc"在字符串常量池当中。
            //synchronized (null) { // 报错:空指针。
            //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
                double before = this.getBalance();
                double after = before - money;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.setBalance(after);
            //}
        }
    }
    package com.lyq.java.threadsafe2;
    
    public class AccountThread extends Thread {
    
        // 两个线程必须共享同一个账户对象。
        private Account act;
    
        // 通过构造方法传递过来账户对象
        public AccountThread(Account act) {
            this.act = act;
        }
    
        public void run(){
            // run方法的执行表示取款操作。
            // 假设取款5000
            double money = 5000;
            // 取款
            // 多线程并发执行这个方法。
            //synchronized (this) { //这里的this是AccountThread对象,这个对象不共享!
            synchronized (act) { // 这种方式也可以,只不过扩大了同步的范围,效率更低了。
                act.withdraw(money);
            }
    
            System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
        }
    }

     2、测试

    package com.lyq.java.threadsafe2;
    
    public class Test {
        public static void main(String[] args) {
            // 创建账户对象(只创建1个)
            Account act = new Account("act-001", 10000);
            // 创建两个线程
            Thread t1 = new AccountThread(act);
            Thread t2 = new AccountThread(act);
    
            // 设置name
            t1.setName("t1");
            t2.setName("t2");
            // 启动线程取款
            t1.start();
            t2.start();
        }
    }

     3、结果

    三、实例方法上可以使用synchronized

    1、账户类

    package com.lyq.java.threadsafe3;
    
    public class Account {
        // 账号
        private String actno;
        // 余额
        private double balance;
    
        public Account() {
        }
    
        public Account(String actno, double balance) {
            this.actno = actno;
            this.balance = balance;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        //取款的方法
        /*
        在实例方法上可以使用synchronized吗?可以的。
            synchronized出现在实例方法上,一定锁的是this。
            没得挑。只能是this。不能是其他的对象了。
            所以这种方式不灵活。
    
            另外还有一个缺点:synchronized出现在实例方法上,
            表示整个方法体都需要同步,可能会无故扩大同步的
            范围,导致程序的执行效率降低。所以这种方式不常用。
    
            synchronized使用在实例方法上有什么优点?
                代码写的少了。节俭了。
    
            如果共享的对象就是this,并且需要同步的代码块是整个方法体,
            建议使用这种方式。
         */
        public synchronized void withdraw(double money){
            double before = this.getBalance(); // 10000
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }

     2、实现类

    package com.lyq.java.threadsafe3;
    
    public class AccountThread extends Thread {
    
        // 两个线程必须共享同一个账户对象。
        private Account act;
    
        // 通过构造方法传递过来账户对象
        public AccountThread(Account act) {
            this.act = act;
        }
    
        public void run(){
            // run方法的执行表示取款操作。
            // 假设取款5000
            double money = 5000;
            // 取款
            // 多线程并发执行这个方法。
            act.withdraw(money);
    
            System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
        }
    }

     3、测试

    package com.lyq.java.threadsafe3;
    
    public class Test {
        public static void main(String[] args) {
            // 创建账户对象(只创建1个)
            Account act = new Account("act-001", 10000);
            // 创建两个线程
            Thread t1 = new AccountThread(act);
            Thread t2 = new AccountThread(act);
            // 设置name
            t1.setName("t1");
            t2.setName("t2");
            // 启动线程取款
            t1.start();
            t2.start();
        }
    }

     4、结果

  • 相关阅读:
    解决Driver/library version mismatch
    ubuntu 16.04 nfs服务的搭建
    samba on ubuntu
    SSH连接下复制远程linux服务器文件到本地的命令(zz)
    R 语言入门(Ubuntu)
    new repository
    vlc play函数跟踪
    SSH连接下复制远程linux服务器文件到本地的命令(zz)
    c# HttpServer 的使用
    异步Udp监听关闭 出现异常,访问已释放的资源或者其他错误的解决方法
  • 原文地址:https://www.cnblogs.com/omgliyq/p/14428099.html
Copyright © 2020-2023  润新知