线程同步 synchronized 同步代码块 同步方法 同步锁
最近项目中提了资源共享的需求,所以就在网上看了一些多线程的例子,觉得有一个博主的文章比较不错。里面有多线程同步锁的代码详解,开启了我对代码安全更深层次的认识,代码简单易懂。还有一个死锁的demo,现在经过修改后贴在自己园里,供理解其思想。
举一个线程作用的例子: 一个Java虚拟机的实例运行在一个单独的进程中,不同的线程共享Java虚拟机进程所属的堆内存。这也是为什么不同的线程可以访问同一个对象。线程彼此共享堆内存并保有他们自己独自的栈空间。这也是为什么当一个线程调用一个方法时,他的局部变量可以保证线程安全。但堆内存并不是线程安全的,必须通过显示的声明同步来确保线程安全。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
一 同步代码块
1.为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。其语法如下:
synchronized(obj){
//同步代码块
}
其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但 是同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
2.小例子
Account.java
1 public class Account { 2 private String accountNo; 3 private double balance; 4 public Account(String accountNo,double balance){ 5 this.accountNo=accountNo; 6 this.balance=balance; 7 } 8 9 public double getBalance() { 10 return balance; 11 } 12 13 public void setBalance(double balance) { 14 this.balance = balance; 15 } 16 17 public String getAccountNo() { 18 return accountNo; 19 } 20 21 public void setAccountNo(String accountNo) { 22 this.accountNo = accountNo; 23 } 24 25 @Override 26 public boolean equals(Object o) { 27 if (this == o) return true; 28 if (o == null || getClass() != o.getClass()) return false; 29 30 Account account = (Account) o; 31 32 return accountNo.equals(account.accountNo); 33 34 } 35 36 @Override 37 public int hashCode() { 38 return accountNo.hashCode(); 39 } 40 }
DrawThread.java
1 public class DrawThread extends Thread { 2 private Account account; 3 private double drawAmount; 4 5 public DrawThread(String name, Account account, double drawAmount) { 6 super(name); 7 this.account = account; 8 this.drawAmount = drawAmount; 9 } 10 public void run(){ 11 synchronized (account){ 12 if(account.getBalance()>=drawAmount){ 13 System.out.println(getName() + "取钱成功,吐出钞票: " + drawAmount); 14 try{ 15 Thread.sleep(1); 16 }catch(InterruptedException ex){ 17 ex.getStackTrace(); 18 } 19 account.setBalance(account.getBalance()-drawAmount); 20 System.out.println(getName()+"\t余额为:"+account.getBalance()); 21 }else{ 22 System.out.println(getName()+"取钱失败,余额不足"); 23 } 24 } 25 } 26 }
DrawTest.java
1 public class DrawTest { 2 public static void main(String[] args){ 3 Account acct=new Account("1234567",1000); 4 new DrawThread("甲",acct,800).start(); 5 new DrawThread("乙",acct,800).start(); 6 } 7 }
运行结果:
甲取钱成功,吐出钞票: 800.0
余额为:200.0
乙取钱失败,余额不足
3.如果将DrawThread的同步去掉:
1 public class DrawThread extends Thread { 2 private Account account; 3 private double drawAmount; 4 5 public DrawThread(String name, Account account, double drawAmount) { 6 super(name); 7 this.account = account; 8 this.drawAmount = drawAmount; 9 } 10 public void run(){ 11 // synchronized (account){ 12 if(account.getBalance()>=drawAmount){ 13 System.out.println(getName() + "取钱成功,吐出钞票: " + drawAmount); 14 try{ 15 Thread.sleep(1); 16 }catch(InterruptedException ex){ 17 ex.getStackTrace(); 18 } 19 account.setBalance(account.getBalance()-drawAmount); 20 System.out.println("\t余额为:"+account.getBalance()); 21 }else{ 22 System.out.println(getName()+"取钱失败,余额不足"); 23 } 24 // } 25 } 26 }
会出现这些情况的结果:
乙取钱成功,吐出钞票: 800.0
甲取钱成功,吐出钞票: 800.0
余额为:200.0
余额为:-600.0
甲取钱成功,吐出钞票: 800.0
乙取钱成功,吐出钞票: 800.0
余额为:200.0
余额为:200.0
程序使用synchronized将run()方法里的方法体修改成同步代码块,同步监视器就是account对象,这样的做法符合“加锁-修改-释放锁”的逻辑,这样就可以保证并发线程在任一时刻只有一个线程进入修改共享资源的代码区。多次运行,结果只有一个。
二 同步方法
1.同步方法就是使用synchronized关键字修饰某个方法,这个方法就是同步方法。这个同步方法(非static方法)无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。通过同步方法可以非常方便的实现线程安全的类,线程安全的类有如下特征:
该类的对象可以方便的被多个线程安全的访问;
每个线程调用该对象的任意方法之后都能得到正确的结果;
每个线程调用该对象的任意方法之后,该对象状态依然能保持合理状态。
2.不可变类总是线程安全的,因为它的对象状态不可改变可变类需要额外的方法来保证其线程安全,在Account类中我们只需要把balance的方法变成同步方法即可。
Account.java
1 public class Account { 2 private String accountNo; 3 private double balance; 4 public Account(String accountNo,double balance){ 5 this.accountNo=accountNo; 6 this.balance=balance; 7 } 8 9 //因为账户余额不可以随便更改,所以只为balance提供getter方法 10 public double getBalance() { 11 return balance; 12 } 13 14 public String getAccountNo() { 15 return accountNo; 16 } 17 18 public void setAccountNo(String accountNo) { 19 this.accountNo = accountNo; 20 } 21 22 @Override 23 public boolean equals(Object o) { 24 if (this == o) return true; 25 if (o == null || getClass() != o.getClass()) return false; 26 27 Account account = (Account) o; 28 29 return accountNo.equals(account.accountNo); 30 31 } 32 33 @Override 34 public int hashCode() { 35 return accountNo.hashCode(); 36 } 37 38 //提供一个线程安全的draw()方法来完成取钱操作 39 public synchronized void draw(double drawAmount){ 40 if(balance>=drawAmount){ 41 System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount); 42 try{ 43 Thread.sleep(1); 44 }catch (InterruptedException ex){ 45 ex.printStackTrace(); 46 } 47 balance-=drawAmount; 48 System.out.println("\t余额为:"+balance); 49 }else{ 50 System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足"); 51 } 52 } 53 }
DrawThread.java
1 public class DrawThread extends Thread { 2 private Account account; 3 private double drawAmount; 4 5 public DrawThread(String name, Account account, double drawAmount) { 6 super(name); 7 this.account = account; 8 this.drawAmount = drawAmount; 9 } 10 public void run(){ 11 account.draw(drawAmount); 12 } 13 }
DrawTest.java
1 public class DrawTest { 2 public static void main(String[] args){ 3 Account acct=new Account("1234567",1000); 4 new DrawThread("甲",acct,800).start(); 5 new DrawThread("乙",acct,800).start(); 6 } 7 }
注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。在Account类中定义draw()方法,而不是直接在 run()方法中实现取钱逻辑,这种做法更符合面向对象规则。DDD设计方式,即Domain Driven Design(领域驱动设计),认为每个类都应该是完备的领域对象,Account代表用户账户,就应该提供用户账户的相关方法。通过draw()方法来执行取钱操作,而不是直接将setBalance()方法暴露出来任人操作。
但是,可变类的线程安全是以降低程序的运行效率为代价的,不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。同时,可变类有两种运行环境:单线程环境和多线程环境, 则应该为可变类提供两种版本,即线程安全版本和线程不安全版本。如JDK提供的StringBuilder在单线程环境下保证更好的性能,StringBuffer可以保证多线程安全。
三 释放同步监视器的锁定
1.任何线程进入同步代码块,同步方法之前,必须先获得对同步监视器的锁定,那么如何释放对同步监视器的锁定呢,线程会在一下几种情况下释放同步监视器:
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器;
当前线程在同步代码块、同步方法中遇到break,return终止了该代码块、方法的继续执行;
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、方法的异常结束;
当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器;
2.以下几种情况,线程不会释放同步监视器:
线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器;
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器,当然,程序应尽量避免使用suspend()和resume()方法来控制线程。
四 同步锁:
1.Java5开始,Java提供了一种功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。
Lock 对象提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock是控制多个线程对共享资源进行访问的工具。通常, 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java5提供的两个根接口,并为 Lock提供了ReentrantLock实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。在 Java8中提供了新型的StampLock类,在大多数场景下它可以替代传统的ReentrantReadWriteLock。 ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing,ReadingOptimistic,Reading。
2.在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。主要的代码格式如下:
1 class X{ 2 //定义锁对象 3 private final ReentrantLock lock=new ReentrantLock(); 4 //定义需要保证线程安全的方法 5 public void m(){ 6 //加锁 7 lock.lock(); 8 try{ 9 //...method body 10 } 11 //使用finally块来保证释放锁 12 finally{ 13 lock.unlock(); 14 } 15 } 16 }
将Account.java修改为:
1 public class Account { 2 private final ReentrantLock lock=new ReentrantLock(); 3 private String accountNo; 4 private double balance; 5 public Account(String accountNo,double balance){ 6 this.accountNo=accountNo; 7 this.balance=balance; 8 } 9 10 //因为账户余额不可以随便更改,所以只为balance提供getter方法 11 public double getBalance() { 12 return balance; 13 } 14 15 public String getAccountNo() { 16 return accountNo; 17 } 18 19 public void setAccountNo(String accountNo) { 20 this.accountNo = accountNo; 21 } 22 23 @Override 24 public boolean equals(Object o) { 25 if (this == o) return true; 26 if (o == null || getClass() != o.getClass()) return false; 27 28 Account account = (Account) o; 29 30 return accountNo.equals(account.accountNo); 31 32 } 33 34 @Override 35 public int hashCode() { 36 return accountNo.hashCode(); 37 } 38 39 //提供一个线程安全的draw()方法来完成取钱操作 40 public void draw(double drawAmount){ 41 //加锁 42 lock.lock(); 43 try { 44 if (balance >= drawAmount) { 45 System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount); 46 try { 47 Thread.sleep(1); 48 } catch (InterruptedException ex) { 49 ex.printStackTrace(); 50 } 51 balance -= drawAmount; 52 System.out.println("\t余额为:" + balance); 53 } else { 54 System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足"); 55 } 56 }finally { 57 lock.unlock(); 58 } 59 } 60 }
使用Lock与使用同步代码有点相似,只是使用Lock时可以显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。使用 Lock时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一个时刻只能有一个线程进入临界区。Lock提供 了同步方法和同步代码块所没有的其他功能,包括使用非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock可重入锁的意思是,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
五 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。
如DeadLock.java
1 class A{ 2 public synchronized void foo(B b){ 3 System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了A实例的foo()方法"); 4 try{ 5 Thread.sleep(200); 6 }catch(InterruptedException ex){ 7 ex.printStackTrace(); 8 } 9 System.out.println("当前线程名为:"+Thread.currentThread().getName()+"试图调用B实例的last()方法"); 10 b.last(); 11 } 12 public synchronized void last(){ 13 System.out.println("进入了A类的last()方法内部"); 14 } 15 } 16 class B{ 17 public synchronized void bar(A a){ 18 System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了B实例的bar()方法"); 19 try{ 20 Thread.sleep(200); 21 }catch(InterruptedException ex){ 22 ex.printStackTrace(); 23 } 24 System.out.println("当前线程名为:"+Thread.currentThread().getName()+"试图调用A实例的last()方法"); 25 a.last(); 26 } 27 public synchronized void last(){ 28 System.out.println("进入了B类的last()方法内部"); 29 } 30 } 31 public class DeadLock implements Runnable{ 32 A a =new A(); 33 B b=new B(); 34 public void init(){ 35 Thread.currentThread().setName("主线程"); 36 a.foo(b); 37 System.out.println("进入了主线程之后..."); 38 } 39 public void run(){ 40 Thread.currentThread().setName("副线程"); 41 b.bar(a); 42 System.out.println("进入了副线程之后..."); 43 } 44 public static void main(String[] args){ 45 DeadLock d1=new DeadLock(); 46 new Thread(d1).start(); 47 d1.init(); 48 } 49 }
结果:
当前线程名为:主线程进入了A实例的foo()方法
当前线程名为:副线程进入了B实例的bar()方法
当前线程名为:主线程试图调用B实例的last()方法
当前线程名为:副线程试图调用A实例的last()方法