Java锁机制-悲观锁synchronized、lock与乐观锁

1、线程同步的概念

        同步的概念是在发出一个功能调用后,在没有得到结果之前,该调用就不返回,也就是事情要一件一件做,等前一件做完了才能做下一件事。线程同步指的是同一时刻只有一个线程能进入临界区(访问共享数据的代码块),当前线程执行完成,并且释放了对象锁,其他等待进入临界区的线程才能执行。

        与同步相对的概念是异步,异步是指在发出一个功能调用后,被调用的对象不能立刻返回结果,在没有得到返回结果之前,调用者还可以执行别的操作,被调用者执行完成后,通过状态、通知和回调来通知调用者。异步线程指的是,当程序要执行一个比较耗时的任务时(IO操作、网络请求),程序会开启一个子线程执行这个耗时的任务,主线程继续执行其他的操作,等子线程执行完成后,再通知主线程,异步操作能提高程序的效率。

2、线程同步-synchronized

synchronized是Java中的一个关键字,用于实现线程同步,synchronized常用来:

2.1、修饰代码块

        synchronized(this) {    },作用的范围是{  }括起来的代码,作用的对象是当前对象。一个线程在访问当前对象的synchronized代码块时,其他线程会被阻塞。

public class SyncThread implements Runnable {
   private  int count;

   public  void run() {
      synchronized(this) {
         for (int i = 0; i < 5; i++) {
              System.out.println(Thread.currentThread().getName() + ":" + (count++));
         }
      }
   }

   public int getCount() {
      return count;
   }
}

调用:

public static void main(String[] args) {
	SyncThread syncThread = new SyncThread();
	new Thread(syncThread,"thread1").start();
	new Thread(syncThread,"thread2").start();
}  

结果:

thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9


        两个线程thread1和thread2同时访问同一个对象(syncThread)的synchronized修饰的代码块,同一时刻只能有一个线程进入,另一个线程受阻塞,被阻塞的线程必须等待当前线程执行完synchronized代码块以后才能执行该代码块。我们把调用代码改成如下形式,再测试一下:

public static void main(String[] args) {
	new Thread(new SyncThread(),"thread1").start();
	new Thread(new SyncThread(),"thread2").start();
}

结果:

thread2:0
thread1:0
thread2:1
thread1:1
thread2:2
thread1:2
thread2:3
thread1:3
thread2:4
thread1:4

        这时创建了两个SyncThread对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized(this) {  },而线程thread2执行的是syncThread2对象中的synchronized(this) {   };我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

        此外,当一个线程访问对象的synchronized代码块时,另一个线程仍然可以访问该对象中的非synchronized代码块,如下:

public class SyncThread implements Runnable {
   private  int count;

   public void countAdd() {
	  synchronized(this) {
	         for (int i = 0; i < 5; i ++) {
	            try {
	               System.out.println(Thread.currentThread().getName() + ":countAdd:" + (count++));
	               Thread.sleep(100);
	            } catch (Exception e) {
	               e.printStackTrace();
	            }
	         }
	   }
	}
   public void printCount() {
	      for (int i = 0; i < 5; i ++) {
	         try {
	            System.out.println(Thread.currentThread().getName() + ":printCount:" + count);
	            Thread.sleep(100);
	         } catch (Exception e) {
	            e.printStackTrace();
	         }
	      }
   }

    public void run() {
	      String threadName = Thread.currentThread().getName();
	      if (threadName.equals("thread1")) {
	         countAdd();
	      } else if (threadName.equals("thread2")) {
	         printCount();
	      }
    }
}

调用:

public static void main(String[] args) {
	SyncThread syncThread = new SyncThread();
	new Thread(syncThread,"thread1").start();
	new Thread(syncThread,"thread2").start();
} 

结果:

thread1:countAdd:0
thread2:printCount:1
thread1:countAdd:1
thread2:printCount:2
thread2:printCount:2
thread1:countAdd:2
thread1:countAdd:3
thread2:printCount:4
thread2:printCount:4
thread1:countAdd:4

        由测试结果可以看出,一个线程在访问对象的synchronized代码时,其他线程可以同时访问该对象的非同步代码。

2.2、synchronized修饰成员方法

        public synchronized void method(){  },这种情况和修饰代码块类似,只是修饰范围是整个方法,作用的对象依然是当前对象。上面的例子可以改为下面的代码,效果是一样的:

public synchronized void countAdd() {
	   for (int i = 0; i < 5; i ++) {
	        try {
	                System.out.println(Thread.currentThread().getName() + ":countAdd:" + (count++));
	                Thread.sleep(100);
	            } catch (Exception e) {
	                e.printStackTrace();
	            }
	   }
}

        也就是public synchronized void method(){  }与public void method(){synchronized(this) {  }  }作用是一样的。
         同步方法时,synchronized关键字被不能继承,如果父类中的方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上synchronized关键字才可以实现子类的相应方法是同步的,或者在子类方法中用super关键字调用父类同步的方法,子类的方法也就相当于同步了。

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }
} 

2.3、Synchronized修饰静态方法

        public synchronized static void method() {  },由于静态方法是属于类的,所有synchronized修饰的静态方法锁定的是该类的所有对象。

public class SyncThread implements Runnable {
	private static int count;

	public synchronized static void method() {
	      for (int i = 0; i < 5; i ++) {
	         try {
	            System.out.println(Thread.currentThread().getName() + ":" + (count++));
	            Thread.sleep(100);
	         } catch (InterruptedException e) {
	            e.printStackTrace();
	         }
	      }
	}

	public  void run() {
	    method();
	}
}


调用:

public static void main(String[] args) {
	new Thread(new SyncThread(),"thread1").start();
	new Thread(new SyncThread(),"thread2").start();
} 


结果:

thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9

        syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁,当thread1进入method后,thread2再进入method时会被阻塞。

2.4、synchronized作用于类

synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁,形式如下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         
      }
   }
}

将3中的例子改写,将synchronized修饰静态方法改成修饰类,效果是一样的:

public class SyncThread implements Runnable {
	private static int count;

	public void method() {
		synchronized(SyncThread.class) {
			for (int i = 0; i < 5; i ++) {
		         try {
		            System.out.println(Thread.currentThread().getName() + ":" + (count++));
		            Thread.sleep(100);
		         } catch (InterruptedException e) {
		            e.printStackTrace();
		         }
		      }
		}
	      
	}

	public  void run() {
	    method();
	}
}


调用:

public static void main(String[] args) {
	new Thread(new SyncThread(),"thread1").start();
	new Thread(new SyncThread(),"thread2").start();
} 


结果:

thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9

        synchronized修饰一个类,这种情况是给这个类加锁,加锁的类的所有对象用的是同一把锁。一个线程进入了加锁类任意一个实例的synchronized修饰的代码,其他线程就会阻塞,不能同时进入这个类其他实例的这段同步的代码。


2.5、synchronized修饰一个对象

        这种情况是给这个对象加锁,一个线程拿到这个对象的锁之后,就可以访问加锁的代码,其他的线程想要同时访问使用这个对象锁锁住的代码则等待,当前线程释放对象的锁之后,其他线程才能获得这个对象的锁进而执行被锁住的代码。

/**
 * 银行账户类
 */
class Account {
   String name;
   float amount;

   public Account(String name, float amount) {
      this.name = name;
      this.amount = amount;
   }
   //存钱
   public  void deposit(float amt) {
      amount += amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   //取钱
   public  void withdraw(float amt) {
      amount -= amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }

   public float getBalance() {
      return amount;
   }
}

/**
 * 账户操作类
 */
class AccountOperator implements Runnable{
   private Account account;
   public AccountOperator(Account account) {
      this.account = account;
   }

   public void run() {
      synchronized (account) {
         account.deposit(500);
         account.withdraw(500);
         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
      }
   }
}




调用:

Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);

final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
   threads[i] = new Thread(accountOperator, "Thread" + i);
   threads[i].start();
}


结果:

Thread3:10000.0 
Thread2:10000.0 
Thread1:10000.0 
Thread4:10000.0 
Thread0:10000.0

        在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束,也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。

        下面的例子,两个线程会同时执行,thread1拿到的是Object对象的锁,thread2拿到的是当前对象的锁,这是两把不同的锁,两把锁是互不干扰的,不形成互斥,所以两个线程会同时执行。

public class Main {
	
	Object obj = new Object();
	
	public void method1() {
		synchronized(obj) {
			for(int i=0;i<5;i++) {
				System.out.println("method1:"+i);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		
	}
	
	public void method2() {
		synchronized(this) {
			for(int i=0;i<5;i++) {
				System.out.println("method2:"+i);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}
	}
	public static void main(String[] args) {
		final Main mainClass = new Main();
		new Thread(new Runnable() {
			public void run() {
				mainClass.method1();
			}
		}).start();
		new Thread(new Runnable() {
			public void run() {
				mainClass.method2();
			}
		}).start();
    }  
	
	
}


结果:

method2:0
method1:0
method1:1
method2:1
method2:2
method1:2
method2:3
method1:3
method2:4
method1:4

线上case举例:

2.6、synchronized底层实现原理

2.6.1、synchronized代码块底层原理

举例如下:

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码块
       synchronized (this){
           i++;
       }
   }
}

通过javap -verbose反编译后得到字节码如下:

public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return

        从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指向同步代码块的结束位置。当执行monitorenter指令时,当前线程将试图获取monitor对象的所有权,如果monitor对象的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有了monitor 的所有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1。倘若其他线程已经拥有monitor 对象的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor并设置计数器值为0 ,其他线程将有机会持有 monitor 。

        值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

2.6.2、synchronized方法底层原理

举例如下:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

javap反编译后得到的字节码:

public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10

         JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

2.7、Java虚拟机对synchronized的优化

        Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方从JVM层面对synchronized进行了较大优化。Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。

        锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。

2.7.1、偏向锁

        偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

2.7.2、轻量级锁

        倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

2.7.3、自旋锁

        轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

2.7.4、消除锁

        消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
}

StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,不会被其他线程所使用,因此StringBuffer不可能存在多个线程同时访问的情形,JVM会自动将其锁消除。


2.8、synchronized总结

(1)synchronized作用于代码块、成员方法时取得的是当前对象的锁;synchronized作用于某个对象时,取得的是这个对象的锁;synchronized作用于静态方法、类时,取得的是这个类的锁,这个类的所有对象共用同一把锁。  
(2)实现同步需要很大的系统开销,甚至可能造成死锁,应该尽量避免无谓的同步控制。
 

3、线程同步-lock

        首先我们来说一说为什么会出现Lock接口,之所以出现Lock接口,是因为synchronized同步方式存在某些不足。

1、如果一个代码块被synchronized修饰,当一个线程获取了对应的锁,其他线程只能一直等待获取锁的线程释放锁,而获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了代码块,然后线程释放对锁的占有

2)线程执行发生异常,此时JVM会自动释放线程占有的锁

         如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便就只能一直等待下去,这样会影响程序执行效率,因此就需要有一种机制可以不让等待的线程无期限地等待下去,、Lock接口可以实现,比如让线程只等待一定的时间或者让等待的线程能够响应中断。

2、当有多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但是读操作和读操作不会发生冲突。但是采用synchronized来实现同步的话,就会导致一个问题,如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程可以同时进行读操作,通过ReadWriteLock接口就可以办到。

3、通过Lock接口可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。

下面我们就来分析一下java.util.concurrent.locks包中常用的类和接口。

3.1、Lock接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

         Lock接口中lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的。采用Lock接口同步线程,在发生异常时,JVM不会自动释放锁,必须由程序员主动去释放锁,因此使用Lock接口必须在try{ }catch{ }块中进行,并且将释放锁的操作放在finally{ }块中进行,以保证锁一定被被释放,防止死锁的发生。


3.1.1、lock()方法

        用来获取锁,如果锁已被其他线程获取,则进行等待,通常使用lock()方法来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

3.1.2、tryLock()

        tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}


3.1.3、lockInterruptibly()

        lockInterruptibly()当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只能等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。因此当通过lockInterruptibly()方法获取某个锁时,只有等待锁的线程是可以响应中断的,线程获取了锁之后,是不会被interrupt()方法中断的,而用synchronized修饰的话,当线程等待锁时是无法被中断的,只能一直等待下去。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}


3.2、ReentrantLock

ReentrantLock(可重入锁)是唯一实现了Lock接口的类,lock()方法使用如下:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
	private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方,lock声明为成员变量
    public static void main(String[] args)  {
        final Main test = new Main();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}


结果:

Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁


tryLock()方法使用如下:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
	private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方,lock声明为成员变量
    public static void main(String[] args)  {
        final Main test = new Main();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }
}


结果:

Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁


lockInterruptibly()使用如下:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
    	Main main = new Main();
        MyThread thread1 = new MyThread(main);
        MyThread thread2 = new MyThread(main);
        thread1.start();
        thread2.start();
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();  
        try {  
            System.out.println(thread.getName()+"得到了锁");
        }catch(Exception e) {
        	e.printStackTrace();
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Main test = null;
    public MyThread(Main test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}


结果:

Thread-1被中断
Thread-0得到了锁
Thread-0执行finally
Thread-0释放了锁


Thread-0、Thread-1同时获取锁,但是Thread-0获取到了,接着执行任务,finally中释放锁;Thread-1未获取到锁,进入等待状态,随后被中断。

3.3、ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}


        一个用来获取读锁,一个用来获取写锁,将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

3.4、ReentrantReadWriteLock

假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main {
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    public static void main(String[] args)  {
        final Main test = new Main();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public synchronized void get(Thread thread) {
        for(int i=0;i<5;i++) {
        	System.out.println(thread.getName()+"正在进行读操作");
        }
        System.out.println(thread.getName()+"读操作完毕");
    }
}

结果:

Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0读操作完毕


        两个线程同时调用一个对象的synchronized方法,只能有一个线程拿到对象锁,另一个线程被阻塞。改成读写锁的话:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main {
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    public static void main(String[] args)  {
        final Main test = new Main();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
        	for(int i=0;i<5;i++) {
        		Thread.currentThread().sleep(10);
        		System.out.println(thread.getName()+"正在进行读操作");
        	}
            System.out.println(thread.getName()+"读操作完毕");
        } catch(Exception e) {
        	
        }finally {
        	System.out.println(thread.getName()+"释放了锁");
            rwl.readLock().unlock();
        }
    }
}

结果:

Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0读操作完毕
Thread-0释放了锁
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-1释放了锁


        Thread1和Thread2在同时进行读操作,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

4、synchronized、lock比较

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

5、锁的相关概念

5.1、可重入锁

        如果锁具备可重入性,则称作为可重入锁,像synchronized和ReentrantLock都是可重入锁。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}


        上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁,但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样线程A就会一直等待永远没法获取到锁,而由于synchronized和Lock都具备可重入性,所以这种情况不会发生。

5.2、可中断锁

        如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。synchronized不是可中断锁,Lock是可中断锁,通过lockInterruptibly()实现。

5.3、公平锁

        公平锁即尽量以请求锁的顺序来获取锁,比如有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样可能会导致某些线程永远获取不到锁。synchronized是非公平锁,它无法保证等待的线程获取锁的顺序;ReentrantLock和ReentrantReadWriteLock默认情况下是非公平锁,但是可以设置为公平锁。

5.4、乐观锁与悲观锁

5.4.1、悲观锁

        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

5.4.2、乐观锁

        总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁常见的两种实现方式:

1. 版本号机制

        一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被更新时version值会加一。线程A读取数据的同时也会读取version值,再操作完数据后version值加一,此时如果version值大于数据库中的version值,说明没有其它线程更新该数据,这是才能更新数据;如果此时的version值等于或者小于数据库中的version值,说明该数据已经被其他线程更新过一次或者多次,这时线程A应该放弃此次操作,应再次读取数据以及version值,直到更新成功。

举一个简单的例子 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果。

2.cas算法

CAS(Compare And Swap)就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。举个例子,内存值V、期望值A、更新值B,当V == A的时候将V更新为B

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

6、Java死锁

6.1、什么是死锁

        两个或两个以上的线程情景下,线程A持有锁资源A,但是还想要资源B,于是请求B锁,线程B持有锁资源B,但是还想要资源A,于是请求A锁。两者互不释放锁,又想获得对方资源,导致两个线程永久阻塞的现象。

实例如下:

package com.sukang.sort;

public class ThreadDeadlock {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj1), "t2");

        t1.start();
        t2.start();
    }
}

class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;

    public SyncThread(Object obj1, Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on " + obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on " + obj1);
            work();
            System.out.println(name + " acquiring lock on " + obj2);
            synchronized (obj2) {
                System.out.println(name + " acquired lock on "+obj2);
                work();
            }
            System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }

    private void work(){
        try {
            Thread.sleep(3000);
        } catch ( InterruptedException e ) {
            e.printStackTrace();
        }
    }
}

        每个线程中都能成功获取第一个对象的锁,最终却阻塞在了获取第二个对象,造成了线程之间的互相等待,形成了死锁。

6.2、死锁产生的4个必要条件

1、互斥条件:一个资源每次只能被一个线程使用。
2、请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
4、循环等待条件:若干线程之间形成循环等待资源关系。

6.3、如何预防死锁

1)尽量避免锁的嵌套使用,如必须使用多个锁的话,锁的顺序要一致。

//可能发生顺序死锁的代码
class StaticLockOrderDeadLock {
	private final Object lockA = new Object();
	private final Object lockB = new Object();
	public void a() {
		synchronized (lockA) {
			synchronized (lockB) {
				System.out.println("function a");
			}
		}
	}
	
	public void b() {
		synchronized (lockB) {
			synchronized (lockA) {
				System.out.println("function b");
			}
		}
	}
}

应改为:所有需要多个锁的线程,都要以相同的顺序来获得锁。

//正确的代码
class StaticLockOrderDeadLock {
	private final Object lockA = new Object();
	private final Object lockB = new Object();
	public void a() {
		synchronized (lockA) {
			synchronized (lockB) {
				System.out.println("function a");
			}
		}
	}
	
	public void b() {
		synchronized (lockA) {
			synchronized (lockB) {
				System.out.println("function b");
			}
		}
	}
}


(2)持有锁的方法避免调用外部对象的方法,可能外部对象也持有你的锁,会造成死锁。此种场景在实际开发中更为常见。

6.4、如何排查死锁

        获取Java线程dump文件,dump文件记录了线程在jvm中的执行信息,可以看成是线程活动的日志。

jstack:jdk自带的工具jstack通过它,可以生成应用程序的线程转储文件,只需要两步即可完成:

1>找到应用程序的进程ID  
ps -eaf | grep java
2> 输出线程转储信息到文件或控制台
jstack PID >> mydumps.tdump
jstack PID  

jcmd: jdk8中介绍了jcmd工具,在jdk8及以上环境下,可以使用此命令生成线程转储文件。

6.5、如何检测死锁
 

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值