高并发编程之ReentrantLock
上文学习jvm提供的同步方法synchronized的用法,一些常见的业务类型以及一道以前阿里的面试题,从中学习到了一些并发编程的一些规则以及建议,本文主要学习jdk提供的同步方法reentrantLock。
一、ReentrantLock关键字
reentrantLock是JDK提供的一款同步手工锁,可重入锁。
reentrantLock可以完成synchronized做的同样的功能,但是需要手工释放锁,使用synchronized的时候遇到异常jvm会自动释放锁,但是reentrantLock不会自动释放,需要手动去释放锁,所以一般是将释放锁写到finally里面的。
我们看下面代码:
1 /** 2 * reentrantLock可以完成synchronized做的同样的功能,但是需要手工释放锁, 3 * 使用synchronized的时候遇到异常jvm会自动释放锁,但是reentrantLock不会自动释放, 4 * 需要手动去释放锁,所以一般是将释放锁写到finally里面的。 5 * @author Wuyouxin 6 * 7 */ 8 public class ReentrantLock1 { 9 Lock lock = new ReentrantLock(); 10 void m1 (){ 11 try { 12 lock.lock();//相当于synchronized(this) 13 for (int i = 0; i < 10; i++) { 14 TimeUnit.SECONDS.sleep(1); 15 System.out.println(i); 16 } 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } finally { 20 lock.unlock();//释放锁 21 } 22 } 23 24 void m2 (){ 25 //如果想要两个方法互斥,则锁定同一把锁即可 26 lock.lock(); 27 System.out.println("m2 ..."); 28 lock.unlock(); 29 } 30 31 public static void main(String[] args) { 32 final ReentrantLock1 r1 = new ReentrantLock1(); 33 new Thread(new Runnable() { 34 35 @Override 36 public void run() { 37 r1.m1(); 38 } 39 }, "t1").start(); 40 41 try { 42 TimeUnit.SECONDS.sleep(1); 43 } catch (InterruptedException e) { 44 e.printStackTrace(); 45 } 46 47 new Thread(new Runnable() { 48 49 @Override 50 public void run() { 51 r1.m2(); 52 } 53 }, "t2").start(); 54 } 55 }
二、tryLock方法
在使用reentrantLock时还可以进行尝试性锁定“tryLock”,这样会去判断是否可以锁定,或者指定时间内是否可以锁定,线程可以决定是否继续等待。
我们看下面代码:
1 /** 2 * 在使用reentrantLock时还可以进行尝试性锁定“tryLock”,这样会去判断是否可以锁定, 3 * 或者指定时间内是否可以锁定,线程可以决定是否继续等待。 4 * @author Wuyouxin 5 * 6 */ 7 public class ReentrantLock2 { 8 Lock lock = new ReentrantLock(); 9 10 void m1 (){ 11 try { 12 lock.lock(); 13 for (int i = 0; i < 10; i++) { 14 TimeUnit.SECONDS.sleep(1); 15 System.out.println(i); 16 } 17 } catch (Exception e) { 18 e.printStackTrace(); 19 } 20 } 21 22 /** 23 * 使用tryLock锁定尝试锁定,不管是否锁定,方法都将继续执行 24 * 也可以根据tryLock的返回值来判断是否锁定 25 */ 26 void m2 (){ 27 boolean b = lock.tryLock(); 28 try { 29 if (b){ 30 System.out.println("m2已经锁定"); 31 //已经锁定的业务逻辑 32 } else { 33 System.out.println("m2没有锁定"); 34 //没有锁定的业务逻辑 35 } 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } finally { 39 if (b){ 40 lock.unlock(); 41 } 42 } 43 } 44 45 /** 46 * 如果在5秒内没有锁定对象则继续进行 47 */ 48 void m3 (){ 49 boolean b = false; 50 try { 51 b = lock.tryLock(5, TimeUnit.SECONDS); 52 if (b){ 53 System.out.println("m3已经锁定"); 54 //已经锁定的业务逻辑 55 } else { 56 System.out.println("m3没有锁定"); 57 //没有锁定的业务逻辑 58 } 59 } catch (Exception e) { 60 e.printStackTrace(); 61 } finally { 62 if (b){ 63 lock.unlock(); 64 } 65 } 66 } 67 68 public static void main(String[] args) { 69 final ReentrantLock2 r2 = new ReentrantLock2(); 70 71 new Thread(new Runnable() { 72 73 @Override 74 public void run() { 75 r2.m1(); 76 } 77 }, "t1").start(); 78 79 try { 80 TimeUnit.SECONDS.sleep(1); 81 } catch (InterruptedException e) { 82 e.printStackTrace(); 83 } 84 85 new Thread(new Runnable() { 86 87 @Override 88 public void run() { 89 r2.m2(); 90 } 91 }, "t2").start(); 92 93 try { 94 TimeUnit.SECONDS.sleep(1); 95 } catch (InterruptedException e) { 96 e.printStackTrace(); 97 } 98 99 new Thread(new Runnable() { 100 101 @Override 102 public void run() { 103 r2.m3(); 104 } 105 }, "t3").start(); 106 } 107 }
三、lockInterruptibly方法
使用ReentrantLock还可以使用lockInterruptibly方法可以对interrupt做出响应,在一个线程等待锁的过程中可以被打断。
我们看下面代码:
1 /** 2 * 使用ReentrantLock还可以使用lockInterruptibly方法可以对interrupt做出响应, 3 * 在一个线程等待锁的过程中可以被打断。 4 * @author Wuyouxin 5 * 6 */ 7 public class ReentrantLock3 { 8 9 public static void main(String[] args) { 10 final Lock lock = new ReentrantLock(); 11 12 new Thread(new Runnable() { 13 14 @Override 15 public void run() { 16 try { 17 lock.lock(); 18 System.out.println("t1 start"); 19 TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); 20 System.out.println("t1 end"); 21 } catch (Exception e) { 22 e.printStackTrace(); 23 } finally { 24 lock.unlock(); 25 } 26 } 27 }, "t1").start(); 28 29 Thread t2 = new Thread(new Runnable() { 30 31 @Override 32 public void run() { 33 boolean b = false; 34 try { 35 lock.lockInterruptibly(); 36 b = true; 37 System.out.println("t2 start"); 38 TimeUnit.SECONDS.sleep(5); 39 System.out.println("t2 end"); 40 } catch (InterruptedException e) { 41 System.out.println("Interrupt!"); 42 e.printStackTrace(); 43 } finally { 44 System.out.println("unlock"); 45 if (b){ 46 lock.unlock(); 47 } 48 } 49 } 50 }, "t2"); 51 t2.start(); 52 53 try { 54 TimeUnit.SECONDS.sleep(3); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 t2.interrupt(); 59 } 60 }
上面代码由于t2线程在调用interrupt方法时没有获取到资源,所以由主线程可以直接打断t2线程。
四、ReentrantLock公平锁
公平锁:当一个线程池运行结束之后其他线程获得锁是公平的先等待先得到。所以叫做公平锁。
非公平锁:当一个线程池运行结束之后其他线程获得锁是随机的,所以叫非公平锁,也叫竞争锁。
ReentrantLock还可以指定为公平锁。
我们看下面代码:
1 ** 2 * ReentrantLock还可以设置公平锁 3 * @author Wuyouxin 4 * 5 */ 6 public class ReentrantLock4 extends Thread{ 7 //默认为非公平锁,true为公平锁 8 private static ReentrantLock lock = new ReentrantLock(true); 9 public void run (){ 10 for (int i = 0; i < 100; i++) { 11 lock.lock(); 12 try { 13 System.out.println(Thread.currentThread().getName() + "获得锁"); 14 } catch (Exception e){ 15 e.printStackTrace(); 16 } finally{ 17 lock.unlock(); 18 } 19 } 20 } 21 22 public static void main(String[] args) { 23 ReentrantLock4 r4 = new ReentrantLock4(); 24 new Thread(r4, "t1").start(); 25 new Thread(r4, "t2").start(); 26 } 27 }
面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程和10个消费者线程的阻塞调用。
使用synchronized 的 wait 和 notify 来实现:
1 public class MyContainer1<T> { 2 3 private final LinkedList<T> lists = new LinkedList<T>(); 4 private final static int MAX = 10; 5 private int count = 0; 6 7 public int getCount (){ 8 return this.count; 9 } 10 11 public synchronized void put (T t){ 12 //这里为什么使用while而不是if,因为如果是if, 13 //当线程被唤醒后,其他线程先一步put进去,if不会再继续判断, 14 //而是直接往下走这时再add则会超出范围 15 while (lists.size() == MAX){ 16 try { 17 this.wait(); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 } 22 lists.add(t); 23 count++; 24 this.notifyAll();//通知消费者线程进行消费 25 } 26 27 public synchronized T get(){ 28 T t = null; 29 while (lists.size() == 0){ 30 try { 31 this.wait(); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 } 36 t = lists.removeFirst(); 37 count--; 38 this.notifyAll();//通知生产者线程进行生产 39 return t; 40 } 41 42 43 public static void main(String[] args) { 44 final MyContainer1<String> container = new MyContainer1<String>(); 45 for (int i = 0; i < 10; i++) { 46 //消费者线程 47 new Thread(new Runnable() { 48 @Override 49 public void run() { 50 for (int j=0;j<5;j++)System.out.println(container.get()); 51 } 52 }, "c消费者:" + i).start(); 53 } 54 55 try { 56 TimeUnit.SECONDS.sleep(2); 57 } catch (InterruptedException e) { 58 e.printStackTrace(); 59 } 60 for (int i = 0; i < 2; i++) { 61 //生产者线程 62 new Thread(new Runnable() { 63 @Override 64 public void run() { 65 for (int j = 0; j < 25; j++) { 66 container.put(Thread.currentThread().getName()); 67 } 68 } 69 }, "p生产者:" + i).start(); 70 } 71 72 73 } 74 }
这里为什么使用while而不是if,因为如果是if,当线程被唤醒后,其他线程先一步put进去,if不会再继续判断,而是直接往下走这时再add则会超出范围。
使用Lock 和 Condition实现,可以精确的唤醒某些线程:
1 /** 2 * 使用lock和 Condition实现,Condition方法可以精确的唤醒某些线程。 3 * @author Wuyouxin 4 * 5 */ 6 public class MyContainer2<T> { 7 8 final private LinkedList<T> list = new LinkedList<T>(); 9 final private static int MAX = 10; 10 private int conut = 0; 11 12 private Lock lock = new ReentrantLock(); 13 14 private Condition producer = lock.newCondition();//生产者锁 15 private Condition consumer = lock.newCondition();//消费者锁 16 17 public int getCount (){ 18 return this.conut; 19 } 20 21 public void put (T t){ 22 try { 23 lock.lock(); 24 while (this.list.size() == MAX){ 25 producer.await(); 26 } 27 this.list.add(t); 28 this.conut++; 29 this.consumer.signalAll();//通知消费者线程开始消费 30 } catch (Exception e) { 31 e.printStackTrace(); 32 } finally { 33 lock.unlock(); 34 } 35 } 36 37 public T get(){ 38 T t = null; 39 try { 40 lock.lock(); 41 while (this.list.size() == 0){ 42 consumer.await(); 43 } 44 t = this.list.removeFirst(); 45 this.conut--; 46 producer.signalAll(); 47 } catch (Exception e) { 48 e.printStackTrace(); 49 } finally { 50 lock.unlock(); 51 } 52 return t; 53 } 54 55 public static void main(String[] args) { 56 final MyContainer2<String> c = new MyContainer2<String>(); 57 58 for (int i = 0; i < 10; i++) { 59 new Thread(new Runnable() { 60 61 @Override 62 public void run() { 63 while(true){ 64 System.out.println(c.get()); 65 } 66 } 67 }, "c" + i).start(); 68 } 69 70 for (int i = 0; i < 2; i++) { 71 new Thread(new Runnable() { 72 73 @Override 74 public void run() { 75 while(true){ 76 c.put(Thread.currentThread().getName()); 77 } 78 } 79 }, "p" + i).start(); 80 81 } 82 } 83 84 }
五、ThreadLocal(线程局部变量)
ThreadLocal是空间转换时间,synchronized是时间转换空间,比如Hibernate中的session就存在ThreadLocal中,避免synchronized使用。
1 /** 2 * ThreadLock线程局部变量 3 * 4 * ThreadLocal是空间转换时间,synchronized是时间转换空间, 5 * 比如Hibernate中的session就存在ThreadLocal中,避免synchronized使用。 6 * @author Wuyouxin 7 * 8 */ 9 public class ThreadLocal1 { 10 static ThreadLocal<Person> t1 = new ThreadLocal<Person>(); 11 12 public static void main(String[] args) { 13 new Thread(new Runnable() { 14 15 @Override 16 public void run() { 17 try { 18 TimeUnit.SECONDS.sleep(5); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 System.out.println(t1.get()); 23 } 24 }, "t1").start(); 25 26 new Thread(new Runnable() { 27 28 @Override 29 public void run() { 30 t1.set(new Person()); 31 } 32 }, "t2").start(); 33 } 34 } 35 36 class Person { 37 String name = "zhangsan"; 38 }
按理来说t1对象是同一个,第二个线程往里面set了一个对象,第一个线程应该可以get到,但是ThreadLocal不可以,他相当于线程的局部变量,不可以被其他线程获取。