并发(二):synchronized、锁升级过程

137 阅读4分钟

1、案例引发的思考:多线程环境下不加锁计算累加和

static int i = 0;

public static void incr() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    i++;
}

public static void main(String[] args) throws InterruptedException {
    for (int index = 0; index < 1000; index++) {
        new Thread(() -> {
            SyncDemo.incr();
        }).start();
    }
    Thread.sleep(5000);
    System.out.println(i);
}

输出结果:

image.png

1000个线程去对一个共享变量累加,得到的结果是一个小于等于1000的值。这是线程切换导致的结果。
i++ 的底层是一组指令:\

image.png 拿到i的值
i+1
给i赋值
多线程去操作i++的时候,可能线程A这一组指令没有执行完,然后发生了线程切换,线程B又去执行,执行完让出CPu时间片给到A,此时A读取到自己工作内存中的值还是线程B未加之前的,这种情况就会少加一次1然后返回,最终我们看到的结果就是小于等于1000。\

image.png

2、原子性

线程安全问题:原子性、有序性、可见性
原子性:一组操作要么全部成功执行要么全部不执行
有序性:一个线程执行的一组指令是按顺序执行的
可见性:一个线程对共享变量的修改要对其他线程可见

要解决上面i计算结果小于等于1000这个问题,就是要保证i++这一组操作的原子性,一个线程在执行i++没有完成的时候不让其他线程进来。我们可以使用synchronized关键字来保证。

image.png

给i++操作加锁

输出结果:

image.png

3、synchronized的用法

synchronized锁住的是什么? 是共享资源。 sychronized的锁对象可以是类对象,也可以是对象
修饰静态方法的锁对象是类对象

image.png

也可以修饰代码块,这里的锁对象也是类对象

image.png

也可以使用类的实例也就是对象作为锁对象

image.png 使用synchronized的关键就是要让多个线程竞争同一把锁,才能保证原子性,要是1000个线程去竞争1000个锁,就失去了锁的作用,所以这里使用的时候要特别注意锁对象的作用域

3、synchronized的原理

锁的存储

对象在堆中布局分为对象标记、类元信息、实例数据、对齐填充四个部分, 对象标记中存储着hashcode、分代年龄、锁的标记等消息。

锁的升级

偏向锁->轻量级锁->重量级锁 偏向锁:线程A进来判断对象头中是否有其他线程id,如果没有,通过一次CAS操作设置对象头的线程id为自己的线程id;此时线程B进来,也进行同样的操作,发现对象头中已经有线程A的id,尝试撤销锁,撤销成功就把对象头的线程id设置为自己,只有达到全局安全点的时候才能撤销成功,撤销失败就升级为轻量级锁。
轻量级锁:假设线程A抢占到轻量级锁,就在自己的线程栈帧中维护一个Lock Record,然后将对象头中维护一个指针指向Lock Record;线程B进来也进行通过的操作,修改指针失败之后通过多次CAS(自旋)去尝试获取锁,如果获取锁失败就升级为重量级锁。
重量级锁:假设线程A执行monitorenter指令,抢占到重量级锁,此时线程B进来,进入同步队列,如果是执行了wait方法就进入等待队列,等待抢占到锁的线程执行monitorexit指令然后自己被唤醒。

4、生产者消费者模型

创建一个生产者生产消息,队列满了就阻塞,等待被唤醒;创建一个消费者消费消息,队列空了就阻塞,等待被唤醒。
生产者代码:

public class Product implements Runnable{

    private Queue<String> msg;

    private Integer size;

    public Product(Queue<String> msg, Integer size) {
        this.msg = msg;
        this.size = size;
    }

    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    @Override
    public void run() {
        int i = 0;
        while (true) {
            synchronized (msg) {
                i++;
                while (msg.size() == size) {
                    try {
                        msg.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("生产者生产消息," + i);
                msg.add("生产消息" + i);
                msg.notify();
            }
        }
    }
}

消费者代码:

public class Consumer implements Runnable {

    private Queue<String> msg;

    private Integer size;

    private static int i = 0;

    public Consumer(Queue<String> msg, Integer size) {
        this.msg = msg;
        this.size = size;
    }


    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    @Override
    public void run() {
        while (true) {
            synchronized (msg) {
                while (msg.isEmpty()) {
                    try {
                        msg.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("消费者消费消息" + msg.remove());
                msg.notify();
            }
        }
    }
}

测试方法:

public static void main(String[] args) {
    Queue<String> queue = new LinkedList<>();
    Product product = new Product(queue, 5);
    Consumer consumer = new Consumer(queue, 5);
    Thread thread1 = new Thread(product);
    Thread thread2 = new Thread(consumer);
    thread1.start();
    thread2.start();
}

image.png wait和nofity也是基于重量级锁去实现的,抢占的必须是同一把锁。