一、线程安全问题(银行取钱)
问题描述:
当两个人同时对一个账户进行操作取钱的时候,可能会出现线程安全问题
//定义一个用户类
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);
}
说明
- synchronized 要写在返回值的前面
- 因为draw()用synchronized修饰了,同步方法的同步监视器总是 this,而this指的是调用这个方法的对象。在上面的代码中,调用draw()的是draw,因此多个线程并发修改account的时候,要先对account对象进行加锁。
四、释放同步监视器的锁定
- 以下几种情况会释放同步监视器:
- 当同步代码块或同步方法执行完了,会释放;
- 当同步代码块或同步方法中遇到了break、return 进行终止,会释放
- 当同步代码块或同步方法中有未处理的Error或Exception,导致了异常退出,会释放
- 当执行了同步监视器的ewait()方法,当前线程会暂停,并释放
- 以下几种情况不会释放:
- 在同步代码块或同步方法执行的时候,程序调用了Thread.sleep()或Thread.yield()来暂停当前线程,则不会释放
- 在同步代码块或同步方法执行的时候,程序调用了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很容易造成死锁,尽量不要使用它来暂停线程