Java多线程(三)—线程同步

Java多线程—线程同步

当多个线程访问一个数据时,很容易出现线程安全问题,很大原因是线程不同步造成的。

同步代码块

为了解决这个问题,Java使用同步监视器来解决,方法就是使用同步代码块

synchronized(obj){//obj同步监视器
	//同步代码块
}

线程开始执行同步代码块前,必须获得对同步监视器的锁定,任何时刻只能有一个可以获得对同步监视器的锁定,执行完后将会释放。例如使用synchronizedrun()的方法体修改成同步代码块,任何线程在获取资源前对其加锁,保证并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(临界区),所以同一时刻只有一个线程处于临界区

同步方法

多线程还提供了同步方法,同步方法就是用synchronized修饰的方法。对于修饰的实例方法,无需显式指定同步监视器,因为监视器就是this,即调用该同步方法的对象。提供同步方法可以实现安全的线程类,线程安全类的特点是:

  • 该类的对象可以被多个线程访问
  • 每个线程调用该对象的任意方法后都能得到正确结果,并且该对象状态保持合理状态

synchronized关键字可以修饰方法代码块,不能修饰构造器成员变量

对象的状态不可改变,比如不可变类,它的线程总是安全的,而可变类的线程需要其他方法保证其线程安全,例如在可变类中,把修改其成员变量的方法变成同步方法,因此在线程执行体中可变类的对象调用同步方法则必须对该对象加锁。

上述是在可变类中定义该同步方法的,这很符合面向对象的做法规则,例如DDD(Domain Driven Design)领域驱动设计,这种方式认为每个类都应该是完备的领域对象,有完备的方法保证对象的完整性、一致性。

同时可变类的线程安全以降低程序运行效率为代价,因此不要对线程安全的类的所有方法进行同步,只对那些会改变共享资源的方法同步。如果单线程和多线程可选,则在单线程中使用不安全线程保证性能,而在多线程中使用安全的线程。例如JDK提供的StringBuilder,StringBuffer类,单线程环境下可使用StringBuilder保证性能,多线程环境下可以使用StringBuffer来保证性能。

释放同步监视器

线程进入同步代码块或同步方法前会对同步监视器进行锁定,何时释放同步监视器呢?总结了以下情况会释放:

  • 线程的同步方法和同步代码块执行结束。
  • 在同步代码块、同步方法中Break,return中止继续执行。
  • 在同步代码块、同步方法中出现未处理的异常ExceptionError
  • 在执行同步代码块、同步方法时,程序执行了同步监视器对象的wait()方法,当前线程暂停并释放同步监视器。

而以下情况则不会释放同步监视器:

  • 调用了Thread.sleep()Thread.yield()方法暂停线程执行。
  • 其他线程调用该线程的suspend()方法将该线程挂起也不会释放。
同步锁

Java不止如此还提供了更强大的线程同步机制,通过显式定义同步锁对象Lock对象来实现同步。Lock是控制多个线程对共享资源访问的工具,是Java5提供的其中一个根接口。Lock提供了比synchronized同步方法和代码块更多的操作,支持多个相关的condition对象。可以提供对共享资源的独占访问,每次只有一个线程对象对Lock对象加锁,访问资源前要先获得Lock对象

还有一个根接口ReadWriteLock 读写锁,允许对共享资源进行并发访问。而Java为ReadWriteLock 读写锁提供了ReentrantReadWriteLock可重入读写锁实现类,为Lock提供了ReentrantLock可重入锁实现类。可重入读写锁ReentrantReadWriteLock为读写提供WritingReadingOptimisticReading三种锁模式。在Java8新增了StampedLock类,通常可代替过去的ReentrantReadWriteLock

而比较常用的是ReentrantLock可重入锁,使用该Lock对象可显式加锁、释放锁,建议使用finally确保最终能释放锁。可重入锁具有可重入性,就是线程可以对已被加锁的ReentrantLock再次加锁,可重入锁对象有计数器来追踪lock()方法的嵌套使用,线程在调用lock()加锁后,必须显式使用unlock()释放锁。

class x{
	//定义锁对象
    private  final ReentrantLock lock = new ReentrantLock();
    public void foo(){  //定义保证线程安全的方法
    	lock.lock();
    	try{
    		//线程安全代码
    	}finally{	
    		lock.unlock();
    	}
    }
}

在使用Lock对象时与同步方法有些类似,使用Lock显式使用Lock对象为同步锁;而使用同步方法时,系统将当前对象作为同步监视器并加锁。而且,使用Lock对象时,每一个Lock对象对应一个同步监视器,可以保证同一时刻只有一个线程进入临界区

同步代码块或同步方法使用与竞争资源相关的、隐式同步监视器,加锁和释放锁必须在一个块结构中。当获取多个锁时,还必须以相反顺序在所有锁被获取时相同范围内释放。所以看出同步代码块和同步方法的范围使使用同步锁时显得不灵活,因此有了Lock提供了更灵活的方法运用锁,比如tryLock()方法,获取超时失效锁的tryLock(long TimeUnit)方法,以及获取可中断锁的lockInterruptibly()方法。

死锁

当两个线程相互等待对方释放同步监视器时,就会产生死锁,线程全部进入阻塞状态。线程在获得一个锁1的情况下再去申请另外一个锁2,也就是说在获得了锁1,并且没有释放锁1的情况下,又去申请获得锁2,这个是产生死锁的原因。两个锁的申请就没有发生交叉,就避免了死锁产生的可能。Thread类提供的suspend方法容易导致死锁产生,所以不建议使用该方法。

死锁产生的四个必要条件:

  • 互斥使用:即当资源被一个线程占有时,别的线程不能使用。
  • 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持:当资源请求者在请求其他资源时保持对原有资源的占有。
  • 循环等待:即存在一个等待队列,A占有B的资源,B占有C的资源,C占有A的资源。

当上述四个条件都成立的时候,便形成死锁,当如果打破上述任何一个条件,便可让死锁消失。

所以如果我们能避免在对象的同步方法中调用其它对象的同步方法(同步锁),那么就可以避免死锁产生。在多线程编程时应注意避免发生死锁,尤其是有多个同步监视器的情况下。

公众号:菜鸡干Java
站点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值