七、JVM - 线程安全与锁优化

线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

一、synchronized

根据《Java 虚拟机规范》的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加一,而在执行 monitorexit 指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

  • 被 synchronized 修饰的同步块对同一条线程来说是 可重入 的。

  • 被 synchronized 修饰的同步块在持有锁的线程执行完毕并释放之前,会无条件地阻塞后面其他线程的进入。

二、JUC

除了 synchronized 外,自 JDK 5 起,Java 类库中新提供了 java.util.concurrent (J.U.C) 包,其中的 java.util.concurrent.locks.Lock 接口便成了 Java 的另一种全新的互斥同步手断。

1. 重入锁(ReentrantLock)

重入锁(ReentrantLock)与 synchronized 一样是可重入的,在基本语法上 ReentrantLock 也与 synchronized 很相似,只是代码写法上稍有区别。不过 ReentrantLock 与synchronized 相比增加了一些高级功能,主要是以下三项:等待可中断、可实现公平锁及锁可以绑定多个条件。

  • 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

  • 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致 ReentrantLock 的性能急剧下降,会明显影响吞吐量。

  • 锁绑定多个条件:是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。在 synchronized 中,锁对象的 wait() 跟它的 notify() 或 notifyAll() 方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而 ReentrantLock 则无须这样做,多次调用 newCondition() 方法即可。

    扫描二维码关注公众号,回复: 11571177 查看本文章

    在 synchronized 与 ReentrantLock 都可满足需要时推荐优先选择使用 synchronized 原因:

  • synchronized 是在 Java 语法层面的同步,足够清晰,也足够简单。但 J.U.C 中的 Lock 接口则并非如此。

  • Lock 需要确保在 finally 块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放锁。而使用 synchronized 的话则可以由 Java 虚拟机来确保即使出现异常,锁也能被自动释放。

  • Java 虚拟机更容易针对 synchronized 来进行优化,因为 Java 虚拟机可以在线程和对象的元数据中记录 synchronized 中锁的相关信息,而使用 J.U.C 中的 Lock 的话,Java 虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。

public static void main(String[] args) {
        Lock lock = new ReentrantLock(true);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName());
                } finally {
                    lock.unlock();
                }
            }, "Thread " + i).start();
        }
    }

2. CountDownLatch

​ CountDownLatch 就是倒数,Latch 是门栓的意思(倒数的一个门栓,5,4,3,2,1,0 数到零,这个门栓就开了)

public static void main(String[] args) throws InterruptedException{
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                latch.countDown();
            }, "test" + i).start();
        }
        latch.await();
        System.out.println("DONE!!!");
    }

3. CyclicBarrier 循环栅栏

​ CyclicBarrier 可以理解为人满发车,当一个车上坐满人就发车,下一辆又坐满再发车…

public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(2, () -> {
            System.out.println("人满,小火车出发!!!");
        });

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " is ready !!!");
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "Person-" + i).start();
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }

4. Phaser 阶段

​ 与CountDownLatch非常相似,允许我们协调线程的执行。与CountDownLatch相比,它具有一些额外的功能。Phaser是在线程动态数需要继续执行之前等待的屏障。在CountDownLatch中,该数字无法动态配置,需要在创建实例时提供

 public static void main(String[] args) {
        Phaser phaser = new Phaser(2);
        for (int i = 0; i < 14; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (Exception e) {
            }
            new Thread(() -> {
                phaser.arriveAndAwaitAdvance();
                phaser.register();
                System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS ") + Thread.currentThread().getName());

            }, "Thread " + i).start();
        }
    }

​ 可以继承 Phase 类并得写 onAdvance(),自定义实现多个阶段的协调逻辑。

5. ReadWriteLock

​ ReadWriteLock 读写锁,读写锁的概念其实就是共享锁和排他锁,

public static void main(String[] args) {
        ReadLock readLock = (new ReentrantReadWriteLock()).readLock();
        WriteLock writeLock = (new ReentrantReadWriteLock()).writeLock();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                readLock.lock();
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                    System.out.println("Date: " + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "read lock " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }

            }, "Thread " + i).start();

            new Thread(() -> {
                writeLock.lock();
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                    System.out.println("Date: " + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "write lock " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }, "Thread " + i).start();
        }
    }

6. Semaphore 信号灯

​ Semaphore 的含义就是限流,可以往里传一个数,permits 是允许的数量,表示同时只能有 permits 个线程可以 acquire() 获得到锁,后续线程 acquire() 会被阻塞,等到前面的执行完并 release() 释放锁。

public static void main(String[] args) throws Exception{
        Semaphore semaphore = new Semaphore(10);
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS ") + Thread.currentThread().getName());
                semaphore.release();
            },"Thread "+ i).start();
        }
    }

7. Exchanger 交换器

​ Exchanger 对两两线程间进行数据交换,交换后可以再往下继续执行。

	public static void main(String[] args) {
        Exchanger exchanger = new Exchanger();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    User user = new User(Thread.currentThread().getName());
                    User u = (User) exchanger.exchange(user);
                    System.out.println("My name is " + Thread.currentThread().getName() + ", I get " + u.name);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "Thread "+ i).start();
        }
    }

    @AllArgsConstructor
    static class User {
        String name;
    }

8. LockSupport

​ LockSupport.part() 方法比较灵活,没有加锁的限制:

  • LockSupport.unpark() 可以先于 LockSupoort.park() 方法执行
  • LockSupport 不需要 synchronized 加锁就可以实现线程的阻塞和唤醒
  • 一个线程处于等待状态,连续调用两次 part() 方法,就会使该线程永远无法被唤醒
static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
            System.out.println("My name is " + Thread.currentThread().getName() + ". I'm running...");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyThread(), "T1");
        Thread t2 = new Thread(new MyThread(), "T2");
        t1.start();
        t2.start();
        LockSupport.unpark(t2);
        TimeUnit.MILLISECONDS.sleep(2000);
        LockSupport.unpark(t1);
    }

9. Condition

/**
 * 写一个固定容量同步容器,拥有put和get 方法,能够支持2个生产者以及10个消费者线程的阻塞调用。
 */
public class ConditionTest {

    static class Container<T> {
        int size;
        List<T> datalist;
        int max;

        Lock lock = new ReentrantLock();
        Condition provider = lock.newCondition();
        Condition consumer = lock.newCondition();

        public Container(int max) {
            this.datalist = new ArrayList();
            this.max = max;
        }

        public int put(T t) throws InterruptedException {
            lock.lock();
            try {
                while (size == max) {
                    provider.await();
                }
                datalist.add(t);
                size++;
                consumer.signalAll();
            } finally {
                lock.unlock();
            }
            return size;
        }

        public T get() throws InterruptedException {
            T t = null;
            lock.lock();
            try {
                while (size == 0) {
                    consumer.await();
                }
                t = datalist.get(0);
                datalist.remove(0);
                size--;
                provider.signalAll();
            } finally {
                lock.unlock();
            }
            return t;
        }
    }

    public static void main(String[] args) throws Exception {
        final Container<Integer> container = new Container(5);
        //2个生产者
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    try {
                        int num = container.put(j);
                        System.out.println(Thread.currentThread().getName() + " put " + j + ", size :" + num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "provider-" + i).start();
        }


        //10个消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 200; j++) {
                    try {
                        int num = container.get();
                        System.out.println("------" + Thread.currentThread().getName() + " get " + num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }, "consumer-" + i).start();
        }
    }
}

三、VarHandle

句柄,可以直接指向内存的某个字段的内存区域,并对该字段进行CAS操作。

四、非阻塞同步 CAS

​ 在 JDK 5 之后,Java 类库中才开始使用 CAS 操作,该操作由 sum.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供。Hotspot 虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器 CAS 指令,没有方法调用的过程。

​ CAS 需要3个操作数,内存位置旧的预期值准备设置的新值,CAS 指令执行时,仅当内存位置的值符合旧的预期值时,处理器才会用准备设置的新值更新内存位置的值,否则它就不执行更新。该处理过程是一个原子操作,执行期间不会被其他线程中断。

五、锁优化

​ 高效并发是从 JDK 5 升级到 JDK 6 后一项重要的改进项,Hotspot 虚拟机在该版本上实现各种锁优化技术,如 自适应性自旋(Adaptive Spinning)锁消除(Lock Elimination)锁膨胀(Lock Coarsening)轻量级锁(Lightweight Locking)偏向锁(Biased Locking) 等。

1. 自适应性自旋

2. 锁消除

3. 锁膨胀

4. 轻量级锁

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。轻量级锁设置的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

Mark Word 是实现轻量级锁和偏向锁的的关键。Mark Word 被设计成一个非固定的动态数据结构,以便极小的空间内存储尽量多的信息。会根据对象的状态利用自己的存储空间。

Hotspot 虚拟机对象头 Mark Word
在这里插入图片描述

轻量级锁的工作过程:在代码即将进入同步块的时候,如果此同步块对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的 Mark Word 的拷贝。然后虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个动作成功了,即代表该线程拥有了这个对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后两个比特)将转变为“00”,表示此对象处于 轻量级锁定 状态。

如果更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机会首先检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。

如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时 Mark Word 中存储的就是指向 重量级锁(互斥量)的指针,后面等待的线程也必须进入阻塞状态。

上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向线程的锁记录,那就用 CAS 操作把对角当前的 Mark Word 和线程中拷贝的 Mark Word 替换回来。如果能替换成功,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

5. 偏向锁

如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不去做了。锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中。如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁、及对 Mark Word 的更新操作等)。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向锁模式就马上宣告结束。当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。

猜你喜欢

转载自blog.csdn.net/huanghuitan/article/details/107922718