同步代码块
1.为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,其语法如下:
synchronized(obj){
//同步代码块
}
其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步代码块的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但是同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
2.小例子
Account.java
public class Account {
private String accountNo ;
private double 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;
}
public Account(String accountNo, double balance)
{
super();
this.accountNo = accountNo;
this.balance = balance;
}
}
DrawThread.java
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() {
synchronized (account)
{
if(account.getBalance() >= drawAmount){
System.out.println(getName()+"取钱成功,吐出钞票:"+ drawAmount);
try
{
Thread.sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+"取钱失败,余额不足");
}
}
}
}
DrawTest.java
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("12345",1000);
new DrawThread("甲", acct, 600).start();
new DrawThread("乙", acct, 600).start();;
}
}
运行出现的结果:
甲取钱成功,吐出钞票:600.0
余额为:400.0
乙取钱失败,余额不足
3.如果将DrawThread的同步去掉:
public void run() {
// synchronized (account)
// {
if(account.getBalance() >= drawAmount){
System.out.println(getName()+"取钱成功,吐出钞票:"+ drawAmount);
try
{
Thread.sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+"取钱失败,余额不足");
}
// }
}
会出现的情况有三种:
第一种:
甲取钱成功,吐出钞票:600.0
乙取钱成功,吐出钞票:600.0
余额为:-200.0
余额为:-200.0
第二种:
乙取钱成功,吐出钞票:600.0
甲取钱成功,吐出钞票:600.0
余额为:400.0
余额为:-200.0
第三种:甲取钱成功,吐出钞票:600.0
乙取钱成功,吐出钞票:600.0
余额为:400.0
余额为:400.0
程序使用synchronized将run()方法里的方法修改成同步代码块,同步监视器就是account对象,这样的做法符合“加锁-修改-释放锁”的逻辑,这样就可以保证并发线程在任一时刻只有一个线程进入修改共享资源的代码区。多次运行,结果只有一个。同步 方法
1.同步方法就是使用synchronized关键字修饰某个方法,这个方法就是同步方法,这个同步方法(非static方法)无须显示指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。通过同步方法可以非常方便的实现线程安全的类,线程安全的类有如下特征:
该类的对象可以方便被多个线程安全的访问;
每个线程调用该对象的任意方法之后都得到正确的结果;
每个线程调用该对象的任意方法之后;该对象状态依然能保持合理状态。
2.不可变类总是线程安全的,因为它的对象状态不可改变可变类需要额外的方法来保证其线程安全,在Account类中我们只需要balance的方法变成同步方法即可。
Account.java
public class Account {
private String accountNo ;
private double 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;
}
public Account(String accountNo, double balance)
{
super();
this.accountNo = accountNo;
this.balance = 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()+"取钱失败,余额不足");
}
}
}
DrawThread.java
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(drawAmount);
}
}
DrawTest.java
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("12345",1000);
new DrawThread("甲", acct, 600).start();
new DrawThread("乙", acct, 600).start();;
}
}
注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。在Account类中定义draw()方法,而不是直接在run()方法中实现取钱逻辑,这种做法更符合面向对象规则。DDD设计方式,即Domain Driven Design(领域驱动设计),认为每个类都应该是完备的领域对象,Account代表用户账户,就应该提供用户的相关方法,通过draw()方法执行取钱操作,而不是直接将setBalance方法暴露出来任人操作。
但是可变类的线程安全是以减低程序的运行效率为代价,不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。同时可变类有两种运行环境:单线程环境和多线程环境,则应该为可变类提供两个版本,即线程安全版本和线程不安全版本。如jdk提供的StringBuilder在单线程环境下保证更好的性能,StringBuffer可以保证多线程安全。
释放同步监视器的锁定
1.任何线程进入同步代码块,同步方法之前,必须先获得对同步监视器的锁定,那么如何释放对同步监视器的锁定呢,线程会在以下几种情况下释放同步监视器:
1)当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器;
2)当前线程在同步代码块、同步方法中遇到break,return终止了该代码块、方法的继续执行;
3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、方法的异常结束;
4)当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
2.以下几种情况,线程不会释放同步监视器:
1)线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器;
2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器,当然,程序应尽量避免使用suspend()和resume()方法来控制线程。
同步锁
1.java提供了一种功能更为强大的线程同步机制,通过显示定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。
Lock对象提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock是控制多个线程对资源共享进行访问的工具,通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java5提供的两个接口,并为Lock提供了Reentrant实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。在Java8中提供了新型的StampLock类,在大多数场景下它可以代替传统的ReentrantReadWriteLock。ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing,ReadingOptimistic,Reading.
2.在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。主要的代码格式如下:
public class X {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保护线程安全的方法
public void m(){
//加锁
lock.lock();
try
{
//method body
} catch (Exception e)
{
e.getStackTrace();
}
finally {
lock.unlock();
}
}
}
将Account.java修改为:
public class Account {
private final ReentrantLock lock = new ReentrantLock();
private String accountNo ;
private double 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;
}
public Account(String accountNo, double balance)
{
super();
this.accountNo = accountNo;
this.balance = balance;
}
public void draw(double drawAmount)
{
lock.lock();
try
{
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()+"取钱失败,余额不足");
}
} finally {
lock.unlock();
}
}
}
使用Lock与使用同步代码块有点类似,只是使用Lock时可以显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。使用Lock时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一个线程进入临界区。Lock提供了同步方法和同步代码块所没有的其他功能,包括使用非块状结构的tryLock()方法,以及试图获得可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock可重入锁的意思是,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
死锁
当两个线程互相等待对方释放同步监视器就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁的情况,所以多线程程序应该采取措施避免死锁出现,一旦出现死锁,程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。
如DeadLock.java所示:
A.java
public 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()方法内部");
}
}
B.java
public 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()方法内部");
}
}
DeadLock.java
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public static void main(String[] args) {
DeadLock dLock = new DeadLock();
new Thread(dLock).start();
dLock.init();
}
public void init(){
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入了主线程之后...");
}
public void run() {
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.println("进入了副线程之后...");
}
}
结果有:(四种情况之一)
当前线程名为:副线程进入了B实例的bar()方法
当前线程名为:主线程进入了A实例的foo()方法
当前线程名为:主线程试图调用B实例的last()方法
当前线程名为:副线程试图调用A实例的last()方法