锁和synchronized

锁的常见概念

  • 互斥: 同一时刻只有一个线程执行
  • 临界区:一段须要互斥执行的代码
  • 细粒度锁: 用不一样的锁对受保护资源进行精细化管理。 细粒度锁能够提升并行度,是性能优化的一个重要手段
  • 死锁 :一组互相竞争资源的线程因互相等待,致使“永久”阻塞的现象 。

用锁的最佳实践

  1. 永远只再更新对象的成员变量时加锁。
  2. 永远只在访问可变的成员变量时加锁。
  3. 永远再也不调用其它对象的方法时加锁。
  4. 减小所得持有时间,减少锁的粒度。

同步与异步

  • 调用方法若是须要等待结果,就是同步;若是不须要等待结果就是异步。
  • 同步是Java代码默认的处理方式。

如何实现程序支持异步:java

  1. 异步调用: 调用方建立一个子线程,再子线程中执行方法调用。
  2. 异步方法: 被调用方;方法实现的时候,建立一个显得线程执行主要逻辑,主线程直接return。

synchronized

class  X{
	//修饰非静态方法
	synchronized void foo(){
 	   //临界区
	}
	//修饰静态方法
	synchronized static void bar(){
 	   //临界区
	}
	
	//修饰代码块
	Object obj = new Object();
	void baz(){
   		synchronized(obj){
        	//临界区
    	}
	}
}

Java编译器会在synchronized修饰的方法或代码块先后自动加上加锁lock()和解锁unlock(),这样作的好处就是加锁lock()和解锁unlock()必定 是成对出现的,毕竟忘记解锁unlock()但是个致命的Bug(意味着其余线程只能死等下去了)。安全

修饰静态方法:
//修饰静态方法是用当前类的字节码文件做为锁
class  X{
	//修饰静态方法
	synchronized(X.class) static void bar(){
 	   //临界区
	}
}
修饰非静态方法:
//修饰非静态方法是用当前对象做为锁
class  X{
	//修饰非静态方法
	synchronized(this) static void bar(){
 	   //临界区
	}
}

如何用一把锁保护多个资源性能优化

受保护资源和锁之间合理的关联关系应该是N:1的关系,也就是说能够用一把锁来保护多个资源,可是不能用多把锁来保护一个资源,并发

使用锁的正确姿式

依转帐业务做为示例

示例一:异步

public class Account {
    /**
     *锁:保护帐⼾余额
     */
    private	final	Object	balLock	= new Object();
    /**
     * 帐⼾余额
     */
    private	Integer	balance;
   
    /**
     * 错误的作法
     * 非静态方法的锁是this, 
     * this这把锁能够保护本身的余额this.balance,保护不了别人的余额 target.balance
     * 
     */
   synchronized void transfer(Account target,int amt){
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;//这段代码会出现线程安全,要保证线程安全的话要使用同一个锁
        }
    }
}

示例二:性能

public class Account {
    /**
     *锁:保护帐⼾余额
     */
    private	final	Object	balLock	= new Object();
    /**
     * 帐⼾余额
     */
    private	Integer	balance;
   

    /**
     * 正确的作法,可是会致使整个转帐系统的串行
     *
     * Account.class是全部Account对象共享的,
     * 并且这个对象是Java虚拟机在加载Account类的时候建立的,
     * 因此咱们不用担忧它的惟一性
     *
     * 这样还有个弊端:全部的转帐都是串行了
     */
    void transfer2(Account target,int amt){
        synchronized(Account.class){
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

这样的话转帐操做就成了串行的了,正常的逻辑应该只锁转入帐号和被转入帐户;不影响其余的转帐操做。稍做改造:优化

示例三:this

public class Account {
    /**
     *锁:保护帐⼾余额
     */
    private	final Object lock;
    /**
     * 帐⼾余额
     */
    private	Integer	balance;
   
    //私有化无参构造
    private Account(){}
    //设置一个传递lock的有参构造
    private Account(Object lock){
        this.lock = lock;
    }
    
    /**
     * 转帐
     */
    void transfer(Account target,int amt){
        //此处检查全部对象共享锁
        synchronized(lock){
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

这个方法虽然可以解决问题,可是它要求建立Account对象的时候必须传入同一个对象,线程

还有就是传递对象过于麻烦,写法繁琐缺少可行性。code

示例四:

public class Account {
    
    /**
     * 帐⼾余额
     */
    private	Integer	balance;
    
    /**
     * 转帐
     */
    void transfer(Account target,int amt){
        //此处检查全部对象共享锁
        synchronized(Account.class){
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

用Account.class做为共享的锁,锁定的范围太大。 Account.class是全部Account对象共享的,并且这个对象是Java虚拟机在加载Account类的时候建立的,因此咱们不用担忧它的惟一性。使用Account.class做为共享的锁,咱们就无需在建立Account对象时传入了。

这样新的问题就出来了虽然用Account.class做为互斥锁,来解决银行业务里面的转帐问题,虽然这个方案不存在 并发问题,可是全部帐户的转帐操做都是串行的,例如帐户A转帐户B、帐户C转帐户D这两个转帐操做现实 世界里是能够并行的,可是在这个方案里却被串行化了,这样的话,性能太差。因此若是考虑并发量这种方法也不行的

正确的写法是这样的(使用细粒度锁):

示例五:

public class Account {
    
    /**
     * 帐⼾余额
     */
    private	Integer	balance;
    
    /**
     * 转帐
     */
    void transfer(Account target,int amt){
        //锁定转出帐户
        synchronized(this){
             //锁住转入帐户
            synchronized(target){
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

咱们试想在古代,没有信息化,帐户的存在形式真的就是一个帐本,并且每一个帐户都有一个帐本,这些帐本 都统一存放在文件架上。银行柜员在给咱们作**转帐时,要去文件架上把转出帐本和转入帐本都拿到手,而后作转帐。**这个柜员在拿帐本的时候可能遇到如下三种状况:

  1. 文件架上刚好有转出帐本和转入帐本,那就同时拿走;
  2. 若是文件架上只有转出帐本和转入帐本之一,那这个柜员就先把文件架上有的帐本拿到手,同时等着其 他柜员把另一个帐本送回来;
  3. ​ 转出帐本和转入帐本都没有,那这个柜员就等着两个帐本都被送回来。

细粒度锁有可能会出现死锁

  • 死锁 :一组互相竞争资源的线程因互相等待,致使“永久”阻塞的现象 。
  • 两个线程彼此拿着对方的资源都不释放就会致使死锁,
  • 使用细粒度锁可能会致使死锁

若是有客户找柜员张三作个转帐业务:帐户 A转帐户B 100元,此时另外一个客户找柜员李四也作个转帐业务:帐户B转帐户A 100元,因而张三和李四同时都去文件架上拿帐本,这时候有可能凑巧张三拿到了帐本A,李四拿到了帐本B。张三拿到帐本A后就等着 帐本B(帐本B已经被李四拿走),而李四拿到帐本B后就等着帐本A(帐本A已经被张三拿走),他们要等 多久呢?他们会永远等待下去…由于张三不会把帐本A送回去,李四也不会把帐本B送回去。咱们姑且称为死等吧。

如何避免死锁
  1. 互斥,共享资源X和Y只能被一个线程占用;
  2. 占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源x;
  3. 不可抢占,其余线程不能强行抢占线程T1占有的资源;
  4. 循环等待,线程1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。

只要破坏其中一个就能够避免死锁

等待-通知机制

用synchronized实现等待-通知机制

  • synchronized 配合wait(),notif(),notifyAll()这三个方法可以轻松实现.
  • wait(): 当前线程释放锁,进入阻塞状态
  • notif(),notifAll(): 通知阻塞的线程有能够继续执行,线程进入可执行状态
  • notif()是会随机地地通知等待队歹一个线程
  • notifyAll()会通知等待队列中的全部线程,建议使用notifAll()

wait与sleep区别:

sleep是Object的中的方法,wait是Thread中的方法

wait会释放锁,sleep不会释放锁

wait须要用notif唤醒,sleep设置时间,时间到了唤醒

wait无需捕获异常,而sleep须要

wait(): 当前线程进入阻塞


**** 码字不易若是对你有帮助请给个关注****

**** 爱技术爱生活 QQ群: 894109590****

相关文章
相关标签/搜索