【多线程与并发】synchronized同步锁

一、概念

synchronized为java内置的关键字,用于保证一组代码的原子性以及该代码中共享变量的可见性(happens-before原则的Monitor Lock Rule),同时由于as-if-serial语义(不管怎么重排序,单线程下的执行结果不能被改变),该关键字又可以说是有序性的,所以synchronized在解决并发问题上可以说是“万能”的。从并发策略上来看,这种互斥同步(阻塞同步)锁是一种悲观锁

那么如何使用呢?可以将该关键字加在方法(构造方法和接口方法例外)上,也可以使用synchronized(obj){}的方式

二、等价性

如下代码中,m1等价于m2;m3等价于m4

public class SynchronizedTest {
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        test.m1();
        m3();
    }
    
	// 构造方法不支持synchronized关键字
    // synchronized SynchronizedTest(){}
    
    public synchronized void m1() {
        System.out.println("m1...");
        m2();
    }

    public void m2() {
        synchronized (this) {
            System.out.println("m2...");
        }
    }

    public static synchronized void m3() {
        System.out.println("m3...");
        m4();
    }

    public static void m4() {
        synchronized (SynchronizedTest.class) {
            System.out.println("m4...");
        }
    }
}

三、可重入性

上述代码中主线程在执行m1()时获取了test对象锁,但是在执行m2()时并不会阻塞,只是在test对象的monitor计数器中加一,执行完m2()后计数器减一,执行完m1()后计数器再减一变为0,此时释放test对象锁。同理在执行m3()时,获取了SynchronizedTest.class对象,在执行m4()时class对象计数器加一。

四、线程间协作

线程协作主要通过Object类的如下几个方法来完成,其实我一直没明白这些wait和notify方法为什么不给Thread类而是给了Object类。。。如果通过Thread.wait(lock)以及Thread.notifyAll(lock)调用方法,我感觉更符合认知直觉。

  1. Object::wait() 该方法使线程释放对象锁并进入无限期等待状态,直到有其他线程唤醒它
  2. Object::wait(long millis) 该方法使线程释放对象锁并进入有限期等待,若期间没有其他线程唤醒它,那么到了时间后会自动唤醒并争抢对象锁
  3. Object::notify() 该方法会随机通知一个等待该对象锁的线程,注意调用该方法不会释放对象锁
  4. Object::notifyAll() 该方法会通知所有等待该对象锁的线程,同样调用该方法不会释放对象锁

通常情况下方法1和4最为常用。同时根据源码注释,wait()方法一般伴随while循环使用。这其实也比较好理解,如果你用if判断使线程等待了,当该线程苏醒时就会继续往下执行了,但很有可能if中的条件还是true。

//As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
     synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     }

下面通过一个经典的生产者消费者程序来看下如何使用:

package com.hch.concurrency;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ProducerAndConsumer {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        int producerNum = 5;
        int consumerNum = 3;
        ProductQueue productQueue = new ProductQueue(5);
        for (int i = 0; i < producerNum; i++) {
            pool.execute(new Producer(3, productQueue));
        }
        for (int i = 0; i < consumerNum; i++) {
            pool.execute(new Consumer(5, productQueue));
        }
        pool.shutdown();
    }

    static class Product {
        private static int count = 0;
        private int id = count++;

        @Override
        public String toString() {
            return "product(id=" + id + ")";
        }
    }

    static class ProductQueue {
        private Product[] products;
        private int tail;
        private int head;
        private int size;

        ProductQueue(int size) {
            if (size <= 0) {
                throw new RuntimeException("size must > 0");
            }
            this.size = size;
            this.head = (tail + 1) % size;
            products = new Product[size];
        }

        public synchronized void put(Product product) {
            while (products[(tail + 1) % size] != null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            int next = (tail + 1) % size;
            products[next] = product;
            tail = next;
            System.out.printf("%s has been put at position %d%n", product, next);
            notifyAll();
        }

        public synchronized Product take() {
            while (products[head] == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Product product = products[head];
            products[head] = null;
            System.out.printf("%s has been taken from position %d%n", product, head);
            head = (head + 1) % size;
            notifyAll();
            return product;
        }
    }

    static class Producer implements Runnable {
        private static int count;
        private int id = count++;
        private int num;
        private ProductQueue queue;

        Producer(int num, ProductQueue queue) {
            this.num = num;
            this.queue = queue;
            System.out.printf("producer%d produces %d products%n", id, num);
        }

        @Override
        public void run() {
            for (int i = 0; i < num; i++) {
                queue.put(new Product());
            }
        }
    }

    static class Consumer implements Runnable {
        private static int count;
        private int id = count++;
        private int num;
        private ProductQueue queue;

        Consumer(int num, ProductQueue queue) {
            this.num = num;
            this.queue = queue;
            System.out.printf("consumer%d consumes %d products%n", id, num);
        }

        @Override
        public void run() {
            for (int i = 0; i < num; i++) {
                queue.take();
            }
        }
    }
}

【多线程与并发】synchronized同步锁_第1张图片
通过synchronized关键字将该put和take方法声明为一个原子操作(不会有其他线程干扰),生产者线程/消费者线程往队列里放/取东西时,必须获取this对象锁。

当队尾的下一个元素不为空时,可以认为队列已满,该生产者线程需要等待,并释放对象锁;当队头元素为空时,可以认为整个队列为空,该消费者线程需要等待,并释放对象锁。

当生产者线程/消费者线程放完/取出东西后需要通知其他线程,不然可能会使所有生产者消费者都处于无限期等待中。

若生产者线程T1通知到了另一个生产者线程T2并且此时队列已满,则T2进入等待状态并释放对象锁;同理若消费者线程T3通知到了另一个消费者线程T4并且此时队列为空,则T4进入等待状态并释放对象锁。

五、锁升级

在JDK1.5之前synchronized关键字的效率非常差(向操作系统申请锁造成频繁的内核态切换),JDK1.6之后Hotspot虚拟机对这个同步锁进行了很大的优化,性能已和ReentrantLock不分上下。锁状态可以分为如下几个阶段,锁优化的过程和markword息息相关,下面通过这几个状态来分析一下

  1. 无锁:对象刚被new出来的时候,处于无锁状态
  2. 偏向锁:命令java -XX:+PrintFlagsFinal -version|grep -i biasedlocking可以查看JVM关于偏向锁的配置,JDK8默认在JVM启动后4s后开启偏向锁。也就是说java程序启动四秒后new出来的对象都会被加上偏向标记,此时再上锁就是一个偏向锁。JVM是如何实现的呢?在锁对象的markword中存储一个指向加锁线程的指针,并将偏向位设为1.
  3. 轻量级锁:若启动后四秒内上锁则直接升级为轻量级锁,不经过偏向锁;若四秒后一个线程上锁后有其他线程竞争对象锁,则偏向锁升级为轻量级锁,若竞争加剧则继续升级为重量级锁。可以通过java -XX:+PrintFlagsFinal -version|grep -i PreInflateSpin查看默认锁膨胀的条件。轻量级锁如何实现?线程在自己的栈帧中创建一个lock record,将锁对象的markword拷贝到这块区域,然后通过cas的方式在锁对象的markword中存储一个指向自己lock record的指针,将锁标志位置为00
  4. 重量级锁:向操作系统申请的互斥锁pthread_mutex
    【多线程与并发】synchronized同步锁_第2张图片

下面一段程序使用jol帮助我们查看锁和markword的对应关系。输出是按照little endian排列的,因此关于锁的标志位只需要看第一个字节(前8位)就行了。

import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;

public class LockUpgradeTest {
    public static void main(String[] args) throws InterruptedException {
        testNonLock();
        testLightWeightLock();
        // testBiasedLock();
    }

    public static void testBiasedLock() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(4100);

        System.out.printf("%n----------------可以偏向----------------%n");
        Object biasedLockObj = new Object();
        System.out.println(ClassLayout.parseInstance(biasedLockObj).toPrintable());
        System.gc();

        System.out.printf("%n----------------偏向锁(gc age + 1)----------------%n");
        synchronized (biasedLockObj) {
            System.out.println(ClassLayout.parseInstance(biasedLockObj).toPrintable());
        }

        System.out.printf("%n----------------偏向锁升级----------------%n");
        Object stillBiasedLockObj = new Object();
        Thread t = new Thread(() -> {
            synchronized (stillBiasedLockObj) {
                System.out.println("thread....");
                System.out.println(ClassLayout.parseInstance(stillBiasedLockObj).toPrintable());

            }
        });

        synchronized (stillBiasedLockObj) {
            System.out.println(ClassLayout.parseInstance(stillBiasedLockObj).toPrintable());
            t.start();
            //只能看到偏向锁升级到重量级锁
            System.out.println(ClassLayout.parseInstance(stillBiasedLockObj).toPrintable());
        }

        System.out.printf("%n----------------解锁----------------%n");
        TimeUnit.MILLISECONDS.sleep(1000);
        System.out.println(ClassLayout.parseInstance(stillBiasedLockObj).toPrintable());
    }

    public static void testLightWeightLock() throws InterruptedException {
        System.out.printf("%n----------------无锁----------------%n");
        Object lightWeightLock = new Object();
        System.out.println(ClassLayout.parseInstance(lightWeightLock).toPrintable());
        System.gc();

        System.out.printf("%n----------------轻量级锁----------------%n");
        synchronized (lightWeightLock) {
            System.out.println(ClassLayout.parseInstance(lightWeightLock).toPrintable());
        }

        System.out.printf("%n----------------解锁(gc age+1)----------------%n");
        TimeUnit.MILLISECONDS.sleep(200);
        System.out.println(ClassLayout.parseInstance(lightWeightLock).toPrintable());

        System.out.printf("%n----------------轻量级锁升级----------------%n");
        Object LWLock = new Object();
        Thread t = new Thread(() -> {
            synchronized (LWLock) {
                System.out.println("thread:");
                System.out.println(ClassLayout.parseInstance(LWLock).toPrintable());

            }
        });

        synchronized (LWLock) {
            System.out.println(ClassLayout.parseInstance(LWLock).toPrintable());
            t.start();
            System.out.println(ClassLayout.parseInstance(LWLock).toPrintable());
        }

        // t.join();
        TimeUnit.SECONDS.sleep(1);
        System.out.printf("%n----------------解锁----------------%n");
        System.out.println(ClassLayout.parseInstance(LWLock).toPrintable());
    }

    public static void testNonLock() throws InterruptedException {
        System.out.printf("%n----------------无锁----------------%n");
        Object nonLockObj = new Object();
        System.out.println(ClassLayout.parseInstance(nonLockObj).toPrintable());

        System.out.printf("%n----------------依旧无锁----------------%n");
        TimeUnit.SECONDS.sleep(3);
        Object stillNonLockObj = new Object();
        System.out.println(ClassLayout.parseInstance(stillNonLockObj).toPrintable());
    }
}

六、参考

深入理解java虚拟机
Synchronized你以为你真的懂?

你可能感兴趣的:(Java)