本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题。html
一.一个典型的Java线程安全例子java
public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thread myThread1 = new Thread(drawMoneyRunnable); Thread myThread2 = new Thread(drawMoneyRunnable); myThread1.start(); myThread2.start(); } } class DrawMoneyRunnable implements Runnable { private Account account; private double drawAmount; public DrawMoneyRunnable(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } public void run() { if (account.getBalance() >= drawAmount) { //1 System.out.println("取钱成功, 取出钱数为:" + drawAmount); double balance = account.getBalance() - drawAmount; account.setBalance(balance); System.out.println("余额为:" + balance); } } } 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; } }
上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操做的场景。屡次运行此程序,可能具备多个不一样组合的输出结果。其中一种可能的输出为:编程
1 取钱成功, 取出钱数为:700.0 2 余额为:300.0 3 取钱成功, 取出钱数为:700.0 4 余额为:-400.0
也就是说,对于一张只有1000余额的银行卡,大家一共能够取出1400,这显然是有问题的。安全
通过分析,问题在于Java多线程环境下的执行的不肯定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,所以,颇有可能出现以下状况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,而后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。多线程
所以,讲到线程安全问题,实际上是指多线程环境下对共享资源的访问可能会引发此共享资源的不一致性。所以,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。并发
二.同步方法ide
对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。能够简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要得到此同步锁(且同时最多只有一个线程可以得到),只有当线程执行完此同步方法后,才会释放锁对象,其余的线程才有可能获取此同步锁,以此类推...测试
在上例中,共享资源为account对象,当使用同步方法时,能够解决线程安全问题。只需在run()方法前加上synshronized关键字便可。this
public synchronized void run() { // .... }
三.同步代码块spa
正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不肯定性便可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的状况,因而,针对须要同步的代码能够直接另外一种同步方式——同步代码块来解决。
同步代码块的格式为:
synchronized (obj) { //... }
其中,obj为锁对象,所以,选择哪个对象做为锁是相当重要的。通常状况下,都是选择此共享资源对象做为锁对象。
如上例中,最好选用account对象做为锁对象。(固然,选用this也是能够的,那是由于建立线程使用了runnable方式,若是是直接继承Thread方式建立的线程,使用this对象做为同步锁会其实没有起到任何做用,由于是不一样的对象了。所以,选择同步锁时须要格外当心...)
例如:
package test.thread; import java.io.IOException; import org.junit.Test; /* * 测试线程锁 */ public class TestBlock { public static void main(String[] args) { TestBlock test = new TestBlock(); MyTest thread1 = test.new MyTest(test); thread1.setName("1"); MyTest thread2 = test.new MyTest(test); thread2.setName("2"); thread1.start(); thread2.start(); } /* * 测试同步 */ class MyTest extends Thread { private Object o; public MyTest(Object o) { this.o = o; } @Override public void run() { // TODO Auto-generated method stub synchronized (o) { // 这个o是test对象的实例 ,对类对象实例进行加锁,当线程调用一个实例运行的,另外的线程调用这个实例时候阻塞,达到上锁的目的 try { for (int a = 0; a < 10; a++) { System.out.println("线程" + MyTest.currentThread().getName() + "修改a==" + a); // MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } } 返回的结果: · 线程1修改a==0 线程1修改a==1 线程1修改a==2 线程1修改a==3 线程1修改a==4 线程1修改a==5 线程1修改a==6 线程1修改a==7 线程1修改a==8 线程1修改a==9 线程2修改a==0 线程2修改a==1 线程2修改a==2 线程2修改a==3 线程2修改a==4 线程2修改a==5 线程2修改a==6 线程2修改a==7 线程2修改a==8 线程2修改a==9 能够看到当一个线程运行完毕以后才运行第二个线程
package myTest; /* * 测试线程锁 */ public class TestBlock { // 调用类 public static void main(String[] args) { TestBlock test = new TestBlock(); MyTest thread1 = test.new MyTest(test); thread1.setName("1"); MyTest thread2 = test.new MyTest(test); thread2.setName("2"); thread1.start(); thread2.start(); } /* * 测试同步 */ class MyTest extends Thread { private Object o; public MyTest(Object o) { this.o = o; } @Override public void run() { synchronized (this) { // this 指代当时类 也就是MyTest,两个线程同时调用同一个类方法。就是两个线程对两个实例的各自上锁。互相不阻塞 try { for (int a = 0; a < 10; a++) { System.out.println("线程" + MyTest.currentThread().getName() + "修改a==" + a); // MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } } 返回的结果: 线程1修改a==0 线程1修改a==1 线程2修改a==0 线程2修改a==1 线程2修改a==2 线程2修改a==3 线程1修改a==2 线程1修改a==3 线程1修改a==4 线程1修改a==5 线程1修改a==6 线程1修改a==7 线程1修改a==8 线程1修改a==9 线程2修改a==4 线程2修改a==5 线程2修改a==6 线程2修改a==7 线程2修改a==8 线程2修改a==9 能够看到:两个线程互不阻塞,锁住的this是不一样的对象
利用实现了Runnable接口的类建立线程,也是同样
public class ThreadTest { public static void main(String[] args) { ThreadTest test = new ThreadTest(); //这里只能new 一个Runnable接口的实现类,若是两个线程使用不一样的Runnable做为入参,那么同步代码块中锁住的this会是不一样的对象 Runnable runnable = test.new MyTest(test); // Runnable runnable2 = test.new MyTest(test); Thread thread1 = new Thread(runnable); // 将myRunnable做为Thread target建立新的线程 thread1.setName("1"); Thread thread2 = new Thread(runnable); // Thread thread2 = new Thread(runnable2); thread2.setName("2"); /*若是直接调用run()方法,调用的是覆盖了Runnable接口的run方法,此时并无启动线程,能够发现打印的结果为: * 线程main修改a==0 线程main修改a==1 线程main修改a==2 线程main修改a==0 线程main修改a==1 线程main修改a==2 thread1.run(); thread2.run(); */ /*调用start()方法才能正确的启动线程,而且锁住的this是同一个对象,同步锁有效,输出结果为: * 线程1修改a==0 线程1修改a==1 线程1修改a==2 线程2修改a==0 线程2修改a==1 线程2修改a==2 */ thread1.start(); thread2.start(); } /* * 测试同步 */ class MyTest implements Runnable{ private Object o; public MyTest(Object o){ this.o=o; } @Override public void run() { // TODO Auto-generated method stub synchronized (this) { //这个o是test对象的实例 ,对类对象实例进行加锁,当线程调用一个实例运行的,另外的线程调用这个实例时候阻塞,达到上锁的目的 try { for(int a=0;a<3;a++){ System.out.println("线程"+Thread.currentThread().getName()+"修改a=="+a); //MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } }
输出结果:
线程1修改a==0 线程1修改a==1 线程1修改a==2 线程2修改a==0 线程2修改a==1 线程2修改a==2
四.Lock对象同步锁
上面咱们能够看出,正由于对同步锁对象的选择须要如此当心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。当使用Lock来保证线程同步时,需使用Condition对象来使线程保持协调。Condition实例被绑定在一个Lock的对象上,使用Lock对象的方法newCondition()获取Condition的实例。Condition提供了下面三种方法,来协调不一样线程的同步:
一、await():致使当前线程等待,直到其余线程调用该Condition的signal()或signalAll()方法唤醒该线程。
二、signal():唤醒在此Lock对象上等待的单个线程。
三、signalAll():唤醒在此Lock对象上等待的全部线程。
使用Lock对象同步锁能够方便的解决此问题,惟一须要注意的一点是Lock对象须要与资源对象一样具备一对一的关系。Lock对象同步锁通常格式为:
class X { // 显示定义Lock同步锁对象,此对象与共享资源具备一对一关系 private final Lock lock = new ReentrantLock(); public void m(){ // 加锁 lock.lock(); //... 须要进行线程安全同步的代码 // 释放Lock锁 lock.unlock(); } }
五.wait()/notify()/notifyAll()线程通讯
在博文《Java总结篇系列:java.lang.Object》中有说起到这三个方法,虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。所以,理论上,任何Object对象均可以做为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通讯。
wait():致使当前线程等待并使其进入到等待阻塞状态。直到其余线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
notify():唤醒在此同步锁对象上等待的单个线程,若是有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操做,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
notifyAll():唤醒在此同步锁对象上等待的全部线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
package myTest; public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 0); Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700); Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700); drawMoneyThread.start(); depositeMoneyThread.start(); } } class DrawMoneyThread extends Thread { private Account account; private double amount; public DrawMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.draw(amount, i); } } } class DepositeMoneyThread extends Thread { private Account account; private double amount; public DepositeMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.deposite(amount, i); } } } 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 void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } /** * 存钱 * * @param depositeAmount */ public synchronized void deposite(double depositeAmount, int i) { if (flag) { // 帐户中已有人存钱进去,此时当前线程须要等待阻塞 try { System.out.println(Thread.currentThread().getName() + " 开始要执行wait操做" + " -- i=" + i); wait(); // 1 System.out.println(Thread.currentThread().getName() + " 执行了wait操做" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 开始存钱 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i); setBalance(balance + depositeAmount); flag = true; // 唤醒其余线程 notifyAll(); // 2 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i); } } /** * 取钱 * * @param drawAmount */ public synchronized void draw(double drawAmount, int i) { if (!flag) { // 帐户中还没人存钱进去,此时当前线程须要等待阻塞 try { System.out.println(Thread.currentThread().getName() + " 开始要执行wait操做" + " 执行了wait操做" + " -- i=" + i); wait(); System.out.println(Thread.currentThread().getName() + " 执行了wait操做" + " 执行了wait操做" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 开始取钱 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i); setBalance(getBalance() - drawAmount); flag = false; // 唤醒其余线程 notifyAll(); System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); // 3 } } }
上面的例子演示了wait()/notify()/notifyAll()的用法。部分输出结果为:
1 取钱线程 开始要执行wait操做 执行了wait操做 -- i=0 2 存钱线程 存款:700.0 -- i=0 3 存钱线程-- 存钱 -- 执行完毕 -- i=0 4 存钱线程 开始要执行wait操做 -- i=1 5 取钱线程 执行了wait操做 执行了wait操做 -- i=0 6 取钱线程 取钱:700.0 -- i=1 7 取钱线程-- 取钱 -- 执行完毕 -- i=1 8 取钱线程 开始要执行wait操做 执行了wait操做 -- i=2 9 存钱线程 执行了wait操做 -- i=1 10 存钱线程 存款:700.0 -- i=2 11 存钱线程-- 存钱 -- 执行完毕 -- i=2 12 取钱线程 执行了wait操做 执行了wait操做 -- i=2 13 取钱线程 取钱:700.0 -- i=3 14 取钱线程-- 取钱 -- 执行完毕 -- i=3 15 取钱线程 开始要执行wait操做 执行了wait操做 -- i=4 16 存钱线程 存款:700.0 -- i=3 17 存钱线程-- 存钱 -- 执行完毕 -- i=3 18 存钱线程 开始要执行wait操做 -- i=4 19 取钱线程 执行了wait操做 执行了wait操做 -- i=4 20 取钱线程 取钱:700.0 -- i=5 21 取钱线程-- 取钱 -- 执行完毕 -- i=5 22 取钱线程 开始要执行wait操做 执行了wait操做 -- i=6 23 存钱线程 执行了wait操做 -- i=4 24 存钱线程 存款:700.0 -- i=5 25 存钱线程-- 存钱 -- 执行完毕 -- i=5 26 存钱线程 开始要执行wait操做 -- i=6 27 取钱线程 执行了wait操做 执行了wait操做 -- i=6 28 取钱线程 取钱:700.0 -- i=7 29 取钱线程-- 取钱 -- 执行完毕 -- i=7 30 取钱线程 开始要执行wait操做 执行了wait操做 -- i=8 31 存钱线程 执行了wait操做 -- i=6 32 存钱线程 存款:700.0 -- i=7
由此,咱们须要注意以下几点:
1.wait()方法执行后,当前线程当即进入到等待阻塞状态,wait()方法会当即使当前线程暂停执行并释放对象锁标示.其后面的代码将不会执行;运行的线程执行wait()方法,JVM会把该线程放入等待池中(wait会释放持有的锁)。wait是指在一个已经进入了同步锁的线程内,让本身暂时让出同步锁,以便其余正在等待此锁的线程能够获得同步锁并运行,只有其余线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程能够去参与得到锁的竞争了,但不是立刻获得锁,由于锁还在别人手里,别人还没释放,若是notify()/notifyAll()后面还有代码,还会继续进行,直到当前线程执行完毕才会释放同步锁对象,即当前线程原来要怎么执行,调用了nodity()以后还的怎么执行,对它自己的代码的执行没有影响,并不会释放锁),调用wait方法的一个或多个线程就会解除wait状态,从新参与竞争对象锁,线程若是再次获得锁,才能够继续向下运行。当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,(这种阻塞是经过提早释放synchronized锁,从新去请求锁致使的阻塞,这种请求必须有其余线程经过notify()或者notifyAll()唤醒从新竞争得到锁)
2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/全部-notifyAll())线程对象,可是,此时还并无释放同步锁对象,也就是说,若是notify()/notifyAll()后面还有代码,还会继续进行,直到当前线程执行完毕才会释放同步锁对象;当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程可以获取锁标志;若是锁标志等待池中没有线程,则notify()不起做用。notifyAll()则从对象等待池中移走全部等待那个对象的线程并放到锁标志等待池中。调用某个对象的notify()方法可以唤醒一个正在等待这个对象的monitor(即锁)的线程,若是有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程; notify()或者notifyAll()方法并非真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)。调用notifyAll()方法可以唤醒全部正在等待这个对象的monitor的线程,唤醒的线程得到锁的几率是随机的,取决于cpu调度。
3.notify()/notifyAll()执行后,若是后面有sleep()方法,则会使当前线程进入到阻塞状态,可是sleep()方法并不会释放同步锁,同步锁依然本身保留,那么必定在sleep的时间后会继续执行此线程,接下来同2;
4.wait()/notify()/nitifyAll()完成线程间的通讯或协做都是基于不一样对象锁的,所以,若是是不一样的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;这三个方法都是java.lang.Object的方法。
5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
6.Thread.yield()方法做用是:yield()方法是中止当前线程,让同等优先权的线程运行,并执行其余线程,如果没有同等优先权的线程,那么yield()方法将不会起做用。yield()应该作的是让当前运行线程回到可运行状态,以容许具备相同优先级的其余线程得到运行机会。所以,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。可是,实际中没法保证yield()必定会达到让步的目的,由于让步的线程还有可能被线程调度程序再次选中。yield()会使当前线程从执行状态(运行状态)变为可执行态(就绪状态),此时cpu会从众多的可执行状态的线程里中选择,也就是说,当前也就是刚刚的那个线程仍是有可能会被再次执行到的,并非说必定会执行其余线程而该线程在下一次中不会执行到了。使用了yield方法后,该线程就会把CPU时间让掉,让其余或者本身的线程执行(也就是谁先抢到谁执行)。
结论:yield()从未致使线程转到等待/睡眠/阻塞状态。在大多数状况下,yield()将致使线程从运行状态转到可运行状态,但有可能没有效果。yield()暂停当前正在执行的线程对象,并执行其余线程。
yield和sleep的区别:
① sleep()方法给其余线程运行机会时不考虑线程的优先级,所以会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操做系统CPU调度相关)具备更好的可移植性。sleep(0)和yield()我以为效果差很少,都是从新进行cpu的从新调度,此时cpu会从众多的可执行状态的线程里中选择
7.join()方法的做用:join()方法可使得一个线程在另外一个线程结束后再执行。若是join()方法在一个线程实例上调用,那么当前运行着的线程将阻塞直到调用了join()方法的线程实例完成了以后才能继续执行。主线程等待 调用了join()方法的线程完成以后才会被唤醒,以前一直是 wait 状态(即在子线程线程执行完以前,主线程一直是wait状态,直到子线程执行完,才被唤醒,继续执行主线程)
join()方法的底层是利用wait()方法实现的。能够看出,join方法是一个synchronized 包裹的同步方法,假设主线程调用线程threadA.join()方法时,由于join()被synchronized 修饰,因此主线程先得到了threadA的锁,随后进入join方法,在join方法中调用了threadA的wait()方法,使主线程进入了threadA对象的等待池,此时,threadA线程则还在执行,只有等到threadA线程执行完毕以后,才会释放threadA锁,此时主线程被唤醒,主线程得以继续执行,threadA的join()方法只会影响到主线程和threadA之间的执行顺序,并不影响同一时刻处在运行状态的其余线程。其余线程原来是怎么运行的,如今仍是怎么运行,丝毫没有收到threadA.join()方法的影响。
8.执行sleep()的线程在指定的时间内确定不会被执行;sleep()方法只让出了CPU,而并不会释放同步资源锁。sleep()方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,而yield()方法会使当前线程让出 CPU 占有权,只是让出的时间是不可设定的。实际上,yield()方法对应了以下操做:先检测当前是否有相同优先级的线程处于同可运行状态,若有,则把 CPU 的占有权交给此线程,不然,继续运行原来的线程。因此yield()方法称为“退让”,它把运行机会让给了同等优先级的其余线程,可是该线程自己仍是会可能被再次调度。 另外,sleep()方法容许较低优先级的线程得到运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,因此,不可能让出较低优先级的线程些时得到 CPU 占有权。
问:Java多线程运行环境中,在哪些状况下会使对象锁释放? 答:因为等待一个锁的线程只有在得到这把锁以后,才能恢复运行,因此让持有锁的线程在再也不须要锁的时候及时释放锁是很重要的。在如下状况下,持有锁的线程会释放锁: (1)执行完同步代码块,就会释放锁。(synchronized) (2)在执行同步代码块的过程当中,遇到异常而致使线程终止,锁也会被释放。(exception) (3)在执行同步代码块的过程当中,执行了锁所属对象的wait()方法,这个线程会释放锁,进 入对象的等待池。(wait) 除了以上状况之外,只要持有锁的线程尚未执行完同步代码块,就不会释放锁。 在下面状况下,线程是不会释放锁的: (1)执行同步代码块的过程当中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。 (2)在执行同步代码块的过程当中,执行了Thread.yield()方法,当前线程放弃CPU,此时当前线程会加入抢夺锁的行列,当前线程有也可能会从新得到锁,获得执行的机会,但不会释放锁。 (3)在执行同步代码块的过程当中,其余线程执行了当前线程对象的suspend()方法,当前线程被暂停,但不会释放锁。