线程不安全就是说在多线程编程中出现了错误情况,由于系统的线程调度具有一定的随机性,当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。具体原因如下:
1,多个线程同时访问一个数据资源(该资源称为临界资源),形成数据发生不一致和不完整。2,数据的不一致往往是因为一个线程中的多个关联的操作(这几个操作合成原子操作)未全部完成。
关于线程安全问题,有一个经典的情景:银行取钱。代码如下:
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 账户类,封装账户编号、账户余额两个Field */ public class Account { private String accountNo;//账户编号 private double balance;//账户余额 public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } public void setBalance(double balance) { this.balance = balance; } public double getBalance() { return this.balance; } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (this == obj) return true; if (obj != null && obj.getClass() == Account.class) { Account target = (Account) obj; return target.getAccountNo().equals(accountNo); } return false; } }
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 取钱的线程类 */ 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; } // 当多条线程修改同一个共享数据时,将涉及数据安全问题。比如说:2条线程进来取钱了,一条取完钱了但是还没有修改余额,那么另外一条就又进来了,本来是钱不够了,但是他还以为钱够,所以逻辑上就有错了 public void run() { // 账户余额大于取钱数目 if (account.getBalance() >= drawAmount) { System.out.println(getName() + "取钱成功!取出的钱是:" + drawAmount); try { Thread.sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 account.setBalance(account.getBalance() - drawAmount); System.out.println("\t余额为: " + account.getBalance()); } else { System.out.println(getName() + "取钱失败!余额不足!"); } } }
public class DrawTest { public static void main(String[] args) { // 创建一个账户 Account acct = new Account("15158117453", 1000); // 模拟两个线程对同一个账户取钱 new DrawThread("LinkinPark", acct, 800).start(); new DrawThread("NightWish", acct, 800).start(); } }
很明显出错了,正常的应该是LinkinPark可以取钱,取完钱余额变成了200,NightWish就不能再次取钱了。。。
既然如此,那么如何避免以上的问题呢?加锁。具体有2种方式:1,同步代码库 2,同步方法
使用第一种方式解决上面的问题,我们只需要修改取钱的那个线程类,给取钱的那个线程类中的那个账户添加一个同步监视器就可以了。
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁” */ 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作为同步监视器,任何线程进入下面同步代码块之前,必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它 synchronized (account) { // 账户余额大于取钱数目 if (account.getBalance() >= drawAmount) { // 吐出钞票 System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 account.setBalance(account.getBalance() - drawAmount); System.out.println("\t余额为: " + account.getBalance()); } else { System.out.println(getName() + "取钱失败!余额不足!"); } } //同步代码块结束,该线程释放同步锁 } }现在结果正确了。
使用第2种方式解决上面的问题,代码如下:
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 账户类,封装账户编号、账户余额两个Field,另外还有取钱的方法 */ public class Account { // 封装账户编号、账户余额两个Field private String accountNo; private double balance; public Account() { } // 构造器 public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法, public double getBalance() { return this.balance; } // 提供一个线程安全draw()方法来完成取钱操作 public synchronized void draw(double drawAmount) { // 账户余额大于取钱数目 if (balance >= drawAmount) { // 吐出钞票 System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 balance -= drawAmount; System.out.println("\t余额为: " + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!"); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (this == obj) return true; if (obj != null && obj.getClass() == Account.class) { Account target = (Account) obj; return target.getAccountNo().equals(accountNo); } return false; } }
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁” */ 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对象的draw方法来执行取钱 // 同步方法的同步监视器是this,this代表调用draw()方法的对象。 // 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。 account.draw(drawAmount); } }
建议总是 立即实践,使用 lock 块来调用 try,在之前/之后的构造中,最典型的代码如下:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }
死锁就是说当2个线程互相等待对方来释放同步监视器。值得注意的是:死锁不报错的,整个程序不会异常,也不会给出任何的提示,只是所有的线程都处于阻塞状态,无法继续了。下面给出一个例子:
/** * * @version 1L * @author LinkinPark * @since 2015-2-4 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 出现死锁的一个例子 */ class A { public synchronized void foo(B b) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法"); try { Thread.sleep(200); } catch (InterruptedException ex) { ex.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 ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法"); a.last(); } public synchronized void last() { System.out.println("进入了B类的last方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); // 调用a对象的foo方法 a.foo(b); System.out.println("进入了主线程之后"); } public void run() { Thread.currentThread().setName("副线程"); // 调用b对象的bar方法 b.bar(a); System.out.println("进入了副线程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); // 以dl为target启动新线程 new Thread(dl).start(); // 调用init()方法 dl.init(); } }
原文地址:http://blog.csdn.net/u011794238/article/details/43486571