我们来模拟一个存取款操作,来看看线程的安全问题
首先,建一个Account类:
package bank;
public class Account {
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo,double balance) {
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;
}
}
再设置一个取钱类:
package bank;
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取得钱数
private double drawAmount;
public DrawThread (String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多个线程开始取钱时,是否安全?
public void run() {
if (account.getBalance() >=drawAmount){
System.out.println(getName()+"取钱成功!吐出钞票"+ drawAmount );
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t 余额为:"+account.getBalance());
}
else {
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
测试取钱是否安全?
package bank;
public class Test {
public static void main(String[] args) {
//存钱
Account account=new Account("张三",1000);
//取钱
DrawThread t1=new DrawThread("李四",account,800);
DrawThread t2=new DrawThread("王五",account,800);
//启动线程
t1.start();
t2.start();
}
}
运行结果如下:
王五取钱成功!吐出钞票800.0
李四取钱成功!吐出钞票800.0
余额为:200.0
余额为:-600.0
显然,取钱出错了,账户余额为负,出错原因是当线程一检测到余额不为负时,进入if判断,在还没有完成取钱操作时,线程二也检测到余额不为负,也进入到了if判断,可是账户里只有一千块,两个线程各取走了八百,所以最终余额为-600;
这就是多线程的不安全性,那么怎么来解决这个问题呢?Java引入了同步监视器,synchronized(obj),线程开始执行前,必须先获得对同步监视器的锁定。提供以下几种方法来控制线程同步:
一、同步代码块:
package bank;
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取得钱数
private double drawAmount;
public DrawThread (String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多个线程开始取钱时,是否安全?
public void run() {
//将account对象加上同步锁
synchronized(account) {
if (account.getBalance() >=drawAmount){
System.out.println(getName()+"取钱成功!吐出钞票"+ drawAmount );
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t 余额为:"+account.getBalance());
}
else {
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
}
运行结果:
李四取钱成功!吐出钞票800.0
余额为:200.0
王五取钱失败!余额不足!
在加上同步锁以后,线程便安全了。这是为什么呢?是因为将共享资源作为同步监视器后,就能阻止两个线程对同一个共享资源进行并发访问,其逻辑为:加锁->修改->释放锁。
二、同步方法:
同步方法就是使用synchronized关键字对方法修饰,修饰后的方法被称为同步方法,这种方法无需显式指定同步监视器,同步方法的同步监视器就是this, 也就是该对象本身。
将Account类加一个取钱的同步方法,然后在run方法里调用,如下:
public synchronized void draw(double drawAmount) {
if (getBalance() >=drawAmount){
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票"+ drawAmount );
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
//修改余额
balance=getBalance()-drawAmount;
System.out.println("\t 余额为:"+getBalance());
}
else {
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
run方法调用:
public void run(){
account.draw(drawAmount);
}
运行结果:
王五取钱成功!吐出钞票800.0
余额为:200.0
李四取钱失败!余额不足!
第二种方法更适合与面向对象的思想,一个类应该包含它所有的操作,而不是将操作交给其他类实现。
可变类的线程安全是以运行效率作为代价的,所以我们为减少线程安全所带来的负面影响,应该采用以下策略:
1、不要对线程安全类里所有方法进行线程同步,只对共享资源的方法进行同步。
2、如果可变类有两种运行环境:单线程环境和多线程环境。应为其提供两种版本,即线程安全版本,和线程不安全版本。线程安全版本保证多线程环境下的安全性,不安全版本将使用在单线程环境中,以保证性能。
以下四种情况会释放同步监视器的锁定:
1、当前线程的同步方法,同步代码块执行完成后,当前线程即释放同步监视器。
2、当同步方法、同步代码块中遇到break return 中止了该方法,该代码块的执行,将释放同步监视器。
3、当前线程在同步代码块、同步方法中遇到了未处理的Error或Exception,则释放同步监视器。
4、当前线程在执行同步代码块、同步方法时调用了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
以下两种情况不会释放同步监视器的锁定:
1、线程执行的同步代码块、同步方法中,调用了Thread.sleep(),Thread.yield()来暂停当前线程的执行,将不会释放同步监视器。
2、线程执行同步代码块时,其他线程调用了该线程的suspend()将该线程挂起,将不会释放同步监视器。
同步锁(Lock):
Lock是控制多线程对共享资源访问的一种工具。该工具比synchronized方法更加灵活,每次只能有一个线程对Lock加锁,线程访问共享资源前,应该先获得Lock对象。
package bank;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final ReentrantLock lock =new ReentrantLock();
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo,double balance) {
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
//加锁
lock.lock();
try {
if (getBalance() >=drawAmount){
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票"+ drawAmount );
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
//修改余额
balance=getBalance()-drawAmount;
System.out.println("\t 余额为:"+getBalance());
}
else {
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
finally {
//修改完成释放锁
lock.unlock();
}
}
}
在Account类中定义锁对象,在draw()方法中显式的加锁,最后在finally块中确保释放锁。
死锁:
当两个线程相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有监测,也没有采取措施来解决死锁,所以这就靠编程人员来避免死锁问题
下面程序便展示了死锁:
package bank;
class A{
public synchronized void foo(B b) {
System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了A类的foo方法");
try {
Thread.sleep(200);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名为:"+Thread.currentThread().getName()+"企图调用B类的last方法内部");
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B{
public synchronized void bar(A a) {
System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了B类的bar方法");
try {
Thread.sleep(200);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名为:"+Thread.currentThread().getName()+"企图调用A类的last方法内部");
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class MoreThread implements Runnable{
A a =new A();
B b =new B();
public void init() {
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入了主线程之后");
}
public static void main(String[] args) {
MoreThread d1=new MoreThread();
//以d1为target启动新线程
new Thread(d1).start();
d1.init();
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.println("进入了副线程之后");
}
}
运行结果:
当前线程名为:主线程进入了A类的foo方法
当前线程名为:副线程进入了B类的bar方法
当前线程名为:主线程企图调用B类的last方法内部
当前线程名为:副线程企图调用A类的last方法内部
当主线程在持有a对象的锁在等待b对象的锁,但是副线程持有b对象的锁,但却在等待a对象的锁,这就形成了死锁。
线程通信:
在系统中,线程调度具有一定的透明性,程序无法准确控制线程的轮换执行,所以我们通过一些机制来保证线程的协调运行。
假设有两个线程,分别代表存款者和取款者,要求这存款者在存入钱后,取款者立即取出,并且重复这种操作。
可以通过设置旗标的方式解决。下面提供两种方法:
1、与synchronized配套的的线程通信:
Account类设置旗标:
package bank;
public class Account {
private String accountNo;
private double balance;
private boolean flag=false;
public Account() {
}
public Account(String accountNo,double balance) {
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
public synchronized void draw(double drawAmount) throws InterruptedException {
//如果flag为假,表明账户还没有人存钱进去,取钱方法阻塞
if(!flag) {
wait();
}else {
System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"取钱:"+drawAmount);
//修改余额
balance-=drawAmount;
System.out.println("\t 余额为:"+getBalance());
//取钱后设置flag为false
flag=false;
//唤醒其他线程
notifyAll();
}
}
public synchronized void depoist(double depositAmount) throws InterruptedException {
//如果flag为真,表明账户还没有人取走钱,存钱方法阻塞
if(flag) {
wait();
}else {
System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"存钱:"+depositAmount);
//修改余额
balance +=depositAmount;
System.out.println("\t 余额为:"+getBalance());
//存钱钱后设置flag为true
flag=true;
//唤醒其他线程
notifyAll();
}
}
}
DrawThread类:
package bank;
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取得钱数
private double drawAmount;
public DrawThread (String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
public void run() {
for(int i=0;i<100;i++) {
try {
account.draw(drawAmount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
DepositThread类:
package bank;
public class DepositThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取得钱数
private double depositAmount;
public DepositThread (String name,Account account,double depositAmount) {
super(name);
this.account=account;
this.depositAmount=depositAmount;
}
public void run() {
for(int i=0;i<100;i++) {
try {
account.depoist(depositAmount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行部分结果:
当前取钱的线程为:张三存钱:800.0
余额为:800.0
当前取钱的线程为:李四取钱:800.0
余额为:0.0
当前取钱的线程为:赵六存钱:800.0
余额为:800.0
当前取钱的线程为:李四取钱:800.0
余额为:0.0
当前取钱的线程为:张三存钱:800.0
余额为:800.0
2、与Lock配套的线程通信:
只是Account类发生变化,所以只附上Account类的代码:
package bank;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
//显式定义Lock对象
private final ReentrantLock lock =new ReentrantLock();
//获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
private String accountNo;
private double balance;
private boolean flag=false;
public Account() {
}
public Account(String accountNo,double balance) {
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
//加锁
lock.lock();
try {
//如果flag为假,表明账户还没有人存钱进去,取钱方法阻塞
if(!flag) {
cond.await();
}else {
System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"取钱:"+drawAmount);
//修改余额
balance-=drawAmount;
System.out.println("\t 余额为:"+getBalance());
//取钱后设置flag为false
flag=false;
//唤醒其他线程
cond.signalAll();
}
}
catch(InterruptedException e) {
e.printStackTrace();
}
//使用finally释放锁
finally {
lock.unlock();
}
}
public void depoist(double depositAmount) throws InterruptedException {
//加锁
lock.lock();
try {
//如果flag为真,表明账户还没有人取走钱,存钱方法阻塞
if(flag) {
cond.await();
}else {
System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"存钱:"+depositAmount);
//修改余额
balance +=depositAmount;
System.out.println("\t 余额为:"+getBalance());
//存钱钱后设置flag为true
flag=true;
//唤醒其他线程
cond.signalAll();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}