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的执行。

  以此类推

 

 

  

posted @ 2019-10-11 21:35  菜菜菜鸡  阅读(1150)  评论(0编辑  收藏  举报