JAVA | 线程(三)线程同步(重要)

一、线程安全问题(银行取钱)

问题描述:
当两个人同时对一个账户进行操作取钱的时候,可能会出现线程安全问题

//定义一个用户类
public class Account {
    // 银行账户
    private String accountNo;

    //余额
    private int balance;

    public Account(String accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

  // get()  set()  方法
}
// 取钱的类
public class DrawThread extends Thread {
    // 取钱的账户
    private Account account;

    // 取钱的金额
    private int monny;

    DrawThread(String name,Account account,int monny){
        super(name);
        this.account = account;
        this.monny = monny;
    }

    @Override
    public void run() {
        super.run();

        if (account.getBalance() >= monny){
            Log.e("testthread",getName()+"取钱成功"+monny);
			 // 强制线程调度切换,这样每次两个用户都能取到钱了
             Thread.sleep(1);
                
            account.setBalance(account.getBalance()-monny);

        }else {
            Log.e("testthread","余额不足~~");
        }
    }
}

//取钱
mBntFun5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               Account account = new Account("admin",1000);

               DrawThread thread1 = new DrawThread("用户1",account,800);
               thread1.start();

               DrawThread thread2 = new DrawThread("用户2",account,800);
               thread2.start();
            }
        });

取钱成功取钱失败
上面代码很有可能会执行成功,如上图1,或者如图2;

要每次都出现图1的异常情况,只需要将run()中的 Thread.sleep(1); 打开即可

二、同步代码块

图一是因为run()方法的方法体不具有同步安全性,可以用同步监视器解决这个问题,通用方法就是同步代码块,语法格式如下:

// obj 就是同步监视器
synchronized(obj){
   ...
   // 此处就是同步代码块
}

说明

  • 上面代码的说明:就是在执行同步代码块之前,必须要对同步监视器进行锁定
  • 任何时刻只能有一个线程可以获得同步监视器的锁定,当同步代码块执行完之后,就会释放同步监视器的锁定
  • 同步监视器 一般都是 可能被并发访问的共享资源
  • 一般逻辑如下:
    加锁–>修改–>释放锁

上面的代码进行优化,如下:

@Override
    public void run() {
        super.run();
        // 符合加锁-->修改-->释放锁的逻辑
        synchronized(account){
            if (account.getBalance() >= monny){
                Log.e("testthread",getName()+"取钱成功"+monny);
                try {
                    // 强制线程调度切换,这样每次两个用户都能取到钱了
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance()-monny);
                Log.e("testthread","余额:"+account.getBalance());
            }else {
                Log.e("testthread","余额不足~~");
            }
        }
    }

在这里插入图片描述

三、同步方法

  • 用synchronized关键字修饰的方法就是同步方法,无需显示指定同步监视器,它的同步监视器就是this,也就是该对象本身

  • 线程安全的类具有如下特点:
    (1)该类的对象可以被多个线程同时访问
    (2)每个线程调用该类的方法后返回的都是正确结果
    (3)每个线程调用该对象的方法后,该对象的状态仍然保持合理状态

  • 不要对所有的方法都进行同步,只对共享资源进行同步

  • 如果可变类有两种运行环境:单线程和多线程,则要为它提供两种版本:线程安全版本和线程不安全版本;

    • 单线程中使用线程不安全版本保证性能
    • 多线程中使用线程安全版本
public class Account {
    // 银行账户
    private String accountNo;

    //余额
    private int balance;

    public Account(String accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    
    public synchronized void draw(int monny){
        if (balance >= monny){
            Log.e("testthread",Thread.currentThread().getName()+"取钱成功:"+monny);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setBalance(balance - monny);

        }else {
            Log.e("testthread",Thread.currentThread().getName()+"取钱失败");
        }
        Log.e("testthread","余额:"+getBalance());
    }
}

@Override
    public void run() {
        super.run();
        account.draw(800);
    }

说明

  1. synchronized 要写在返回值的前面
  2. 因为draw()用synchronized修饰了,同步方法的同步监视器总是 this,而this指的是调用这个方法的对象。在上面的代码中,调用draw()的是draw,因此多个线程并发修改account的时候,要先对account对象进行加锁。

四、释放同步监视器的锁定

  • 以下几种情况会释放同步监视器:
  1. 当同步代码块或同步方法执行完了,会释放;
  2. 当同步代码块或同步方法中遇到了break、return 进行终止,会释放
  3. 当同步代码块或同步方法中有未处理的Error或Exception,导致了异常退出,会释放
  4. 当执行了同步监视器的ewait()方法,当前线程会暂停,并释放
  • 以下几种情况不会释放:
  1. 在同步代码块或同步方法执行的时候,程序调用了Thread.sleep()或Thread.yield()来暂停当前线程,则不会释放
  2. 在同步代码块或同步方法执行的时候,程序调用了suspend使线程挂起了,则不会释放。应该尽量避免使用suspend和resume来控制线程

五、同步锁(Lock)

  • Lock 是sychronized的升级版,有跟广泛的锁定操作
  • Lock是控制多个线程对共享资源进行访问的工具,提供了对共享资源的独占访问
  • java提供了两个根接口:
    • Lock---->实现类:ReentranLock(可重入锁)
    • ReadWriteLock—>实现类:ReentrantReadWiteLock

可重用性
一个线程可以对已经加锁的ReentranLock再次加锁,线程每次调用lock()加锁后,都必须显示调用unlock()释放锁

使用格式:

class A{
ReentrantLock lock = new ReentrantLock();
    
    void fun(){
        //加锁
        lock.lock();
        
        try {
            //需要保证线程安全的代码
            // .....
        }
        finally {
            // 释放锁
            lock.unlock();
        }
    }
}

以上取钱的代码进行优化:

public class Account {
    // 银行账户
    private String accountNo;

    //余额
    private int balance;

    ReentrantLock lock = new ReentrantLock();

    public void drawmonny(int monny){
        // 加锁
        lock.lock();

        try {
            if (balance >= monny){
                Log.e("testthread",Thread.currentThread().getName()+"取钱成功:"+monny);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                setBalance(balance - monny);

            }else {
                Log.e("testthread",Thread.currentThread().getName()+"取钱失败");
            }

            Log.e("testthread","余额:"+getBalance());
        }
        finally {
            // 释放锁
            lock.unlock();
        }
    }
}

六、死锁(互相等待释放同步监视器)

当两个线程相互等待对方释放同步监视器的时候就会发生死锁,死锁发生的时候既不会发生异常,也不会给任何提示,所以要尽量避免死锁。当系统中有多个同步监视器的时候就很容易发生死锁。

public class A{
        public synchronized void funA(B b){
            Log.e("testthread",Thread.currentThread().getName()+"进入A的fun方法");

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.e("testthread",Thread.currentThread().getName()+"想进B的last方法");
            b.lashB();
        }

        public synchronized void lastA(){
            Log.e("testthread",Thread.currentThread().getName()+"进入A的last方法");
        }
    }

    public class B{
        public synchronized void funB(A a){
            Log.e("testthread",Thread.currentThread().getName()+"进入B的fun方法");

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Log.e("testthread",Thread.currentThread().getName()+"想进A的last方法");
            a.lastA();
        }

        public synchronized void lashB(){
            Log.e("testthread",Thread.currentThread().getName()+"进入B的last方法");
        }
    }

    public class DeadLock implements Runnable{
        A a = new A();
        B b = new B();

        void init(){
            Thread.currentThread().setName("主线程");
            a.funA(b);
        }

        @Override
        public void run() {
            b.funB(a);
        }
    }
mBntFun5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DeadLock lock = new DeadLock();
                new Thread(lock,"子线程").start();
                lock.init();
            }
        });

死锁代码解释:
因为funA()和funB()都是同步方法,当a和b调用他们的时候,就要对a和b加锁。而A、B中各自sleep(200)后,开始继续执行,这是A中要调用B的lastB()方法,这时候就要对B加锁,但是这时候B的锁并没有释放,同理B中也是这样的情况,所以双方就一直在等待,造成了死锁。

说明
suspend很容易造成死锁,尽量不要使用它来暂停线程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值