synchronized和ReentrantLock锁住了谁?
一、synchronized
案例1:
public class LockDemo{ public static void main(String[] args) throws Exception { Human human = new Human(); new Thread(() -> { try { human.drink(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } },"A").start(); Thread.sleep(100);//确保A线程先启动 new Thread(() -> { Human.sleep(); },"B").start(); } } class Human{ public void eat() { System.out.println(Thread.currentThread().getName()+ ": *****eat*****"); } public synchronized void drink() throws Exception { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+": *****drink*****"); } public synchronized static void sleep() { System.out.println(Thread.currentThread().getName()+": *****sleep*****"); } }
由于输出结果是动态的不好截图,是能口述输出结果:先输出B:******sleep*****,2.9秒后输出A:******drink*****
在main方法中,使用Thread.sleep(100)秒让主线程睡眠,确保A线程先于B线程拿到资源。首先,我们知道sleep方法并不会是释放锁对象,按理说输出结果应该是三秒后同时输出A:******drink*****和B:******sleep*****,怎么会出现上面的结果呢?原因很简单,dink方法上的synchronized和sleep方法上的synchronized锁的不是同一个资源!
当在非静态方法上加锁,锁的是类的实例对象。当在静态方法上加锁,所得就是类的对象。也就是说当线程A调用加锁方法drink后,其他线程不能再调用此方法的加锁资源,但是线程B之所以可以调用sleep方法,是因为线程B拿到的是类对象的锁,两者并不冲突,就好像两个人进两扇门,谁也不碍着谁。
案例2:
public class LockDemo{ public static void main(String[] args) throws Exception { Human human = new Human(); new Thread(() -> { try { human.drink(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } },"A").start(); Thread.sleep(100);//确保A线程先启动 new Thread(() -> { human.eat(); },"B").start(); } } class Human{ public void eat() { System.out.println(Thread.currentThread().getName()+ ": *****eat*****"); } public synchronized void drink() throws Exception { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+": *****drink*****"); } public synchronized static void sleep() { System.out.println(Thread.currentThread().getName()+": *****sleep*****"); } }
输出结果是:先输出B:******eat*****,2.9秒后输出A:******drink*****
分析:首先不用多虑,A线程拿到资源后,锁住了贡献资源human对象,但是B线程访问的并不是加了锁的方法,而是普通方法,这就好像两个人去上厕所,一个人要蹲大号,一个人是小号,小号完成后,蹲大的才刚刚开始【如果你在吃饭,请你原谅我,实在想不出什么形象的案例】
过多的案例不再多举,只需要搞明白一点:锁是对象的一部分,而不是线程的一部分。线程只是暂时的持有锁,在线程持有锁的这段时间里,其他线程不能访问此对象的同步资源,可以访问此对象的费同步资源。
二、线程通信以及while
上面的案例中并未涉及到线程通信,然而现实的业务纷繁复杂,通常都是线程之间的协作完成业务的处理,最典型的就是———生产者消费者模式
实现复杂的业务需要更加灵活的锁——Lock,Lock接口有多个实现类,提供了更加灵活的结构,可以为同一把锁“配多把钥匙”。
案例1:
public class LockDemo{ public static void main(String[] args) throws Exception { Shop shop = new Shop(); new Thread(() -> { try { shop.produce(); } catch (Exception e) { e.printStackTrace(); } },"P_A").start(); new Thread(() -> { try { shop.produce(); } catch (Exception e) { e.printStackTrace(); } },"P_B").start(); new Thread(() -> { try { shop.consume(); } catch (Exception e) { e.printStackTrace(); } },"C_C").start(); new Thread(() -> { try { shop.consume(); } catch (Exception e) { e.printStackTrace(); } },"C_D").start(); } } class Shop{ int number = 1; public synchronized void produce() throws Exception { if(number != 0) { wait(); } number++; System.out.println(Thread.currentThread().getName()+": "+number); notifyAll(); } public synchronized void consume() throws Exception { if(number == 0) { wait(); } number--; System.out.println(Thread.currentThread().getName()+": "+number); notifyAll(); } }
输出:
C_C: 0 P_B: 1 P_A: 2 C_D: 1
灵魂质问:为什么会输出 2 ?
情况可以这样发生:当线程P_A抢到资源后,发现初始库存为1,于是进入wait状态,释放锁资源,此时线程C_C抢到资源,执行number--,输出C_C:0,然后唤醒所有线程,此时P_B抢到资源,发现number=0,于是执行number++,然后输出P_B:1,然后释放锁,唤醒其他线程,此时CPU转给了P_A,线程P_A先加载上下文,不会再去进行if判断,因为之前判断过了,于是执行number++,输出了P_A:2。问题就出在这个if,所以在同步方法的flag判断是否执行时,杜绝使用if,一律使用while。当使用了while后,当线程P_A加载完上下文继续执行时,会再执行一遍判断,只有当while循环条件不成立时,才会执行后续代码。
在这里再提一下notify和notifyAll的区别:当有多个线程时,notify会随机唤醒一个线程,被唤醒的线程百分百获得资源,但是具体唤醒哪一个是不确定的。而是用notifyAll时,会唤醒其实所有线程,所有线程再次抢占同步资源,谁抢到谁执行。
案例2:
上面的案例只是简答演示了,可以通过定义flag的方式,控制线程的执行。但是涉及到更加复杂情况时,还可以使用更加优秀的解决办法:组合使用Lock和Condition
public class LockDemo{ public static void main(String[] args) throws Exception { Shop shop = new Shop(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { shop.superproduce(); } } catch (Exception e) { e.printStackTrace(); } },"P_A").start(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { shop.produce(); } } catch (Exception e) { e.printStackTrace(); } },"P_B").start(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { shop.consume(); } } catch (Exception e) { e.printStackTrace(); } },"C_C").start(); } } class Shop{ int number = 0; Lock lock = new ReentrantLock(); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Condition c3 = lock.newCondition(); public void superproduce() throws InterruptedException{ lock.lock(); try { while(number != 0) { c1.await(); } number+=2; System.out.println(Thread.currentThread().getName()+": "+number); c2.signal(); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void produce() throws InterruptedException{ lock.lock(); try { while(number != 2) { c2.await(); } number++; System.out.println(Thread.currentThread().getName()+": "+number); c3.signal(); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void consume() throws InterruptedException{ lock.lock(); try { while(number != 3) { c3.await(); } number-=3; System.out.println(Thread.currentThread().getName()+": "+number); c1.signal(); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }
本案例大概意思为:当库存为0时,加快生产,当库存为2时,普通生产,当库存为3时,提供给消费者消费,轮番十次。由于初始初始状态为0,就算是P_B和C_C线程先抢到了资源,由于条件不符合,也只能将执行权交给P_A,当P_A执行完成后,标记线程P_B的执行。
以此类推