geekyouth / geekyouth.github.io

👣极客青年博客😘,基于github pages+issues + VUE 2.0 框架构建的轻量级静态博客系统💎[速度慢请翻墙]
https://java666.cn
39 stars 6 forks source link

Java基础—线程安全与锁 #7

Closed geekyouth closed 2 years ago

geekyouth commented 6 years ago

https://github.com/johnnian/Blog/issues/37

一、定义

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

一个线程安全的代码,要有这样的特征:

代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己实现任何措施来保证多线程的正确调用。

二、线程间共享数据类型

2.1 不可变

不可变的对象一定是线程安全的,如 使用 final 关键字修饰的变量、String类型对象、枚举对象等;

2.2 绝对线程安全

在Java中标注自己是线程安全的类,如Vector、HashTable等,大多数都不是绝对的线程安全,可以运行下面测试代码:

package com.johnnian.thread;

import java.util.Vector;

public class ThreadDemo {

    private static Vector< Integer> vector = new Vector< Integer>();
    public static void main(String[] args)  {
         while (true) { 
             try {
                 for (int i = 0; i < 100; i++) { 
                        vector. add( i); 
                    } 
                    Thread removeThread = new Thread( new Runnable() {
                        @Override 
                        public void run() { 
                            for (int i = 0; i < vector. size(); i++) { 
                                vector. remove( i); 
                            } 
                        } 
                    });
                    Thread printThread = new Thread( new Runnable() { 
                        @Override 
                        public void run() { 
                            for (int i = 0; i < vector. size(); i++) { 
                                Integer item = vector. get(i);
                            } 
                        } 
                    });
                    removeThread.start(); 
                    printThread.start(); 
                } catch (Exception e) {
                    // TODO: handle exception
                    System.out.println(e);
                }       
            } 
    }   
}

运行结果:

Exception in thread "Thread-23" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 141
    at java.util.Vector.get(Vector.java:748)
    at com.johnnian.thread.ThreadDemo$2.run(ThreadDemo.java:30)
    at java.lang.Thread.run(Thread.java:745)

如果对vector对象进行同步操作,修改代码如下:

Thread removeThread = new Thread( new Runnable() {
    @Override 
    public void run() { 
        synchronized (vector) {
            for (int i = 0; i < vector. size(); i++) { 
                vector. remove( i); 
            } 
        }
    } 
});

Thread printThread = new Thread( new Runnable() { 
    @Override 
    public void run() { 
        synchronized (vector) {
            for (int i = 0; i < vector. size(); i++) { 
                Integer item = vector. get(i);
            } 
        }
    } 
});

结果一切正常, :)

2.3 相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

2.4 线程兼容和线程对立

线程兼容指的是,原本不是线程安全的,例如 HashTable,通过一些同步方法(同步锁),保证线程安全。

线程对立指的是,无论如何都无法在多线程环境中使用,例如 Thread.suspend() & Thread.resume()方法。

三、线程安全的方法

3.1 同步互斥(阻塞同步)

方法一: 使用synchronized关键字

在Java里面,最基本的互斥同步手段就是synchronized关键字。

synchronized关键字,编译后,在同步块的前后生成 monitorentermonitorexit 这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。

monitorenter 指令(reference参数):锁计数器 +1,阻塞其他线程
代码块...
monitorexit 指令(reference参数):锁计数器 -1

reference参数:

方法二:ReentrantLock(重入锁)

可以用 java.util.concurrent 中的ReentrantLock实现, ReentrantLock是API级别的互斥锁,synchronized是系统级别(Java中重量级操作)。

ReentrantLock可以实现下面三种策略的锁:

名字 说明
等待可中断 持有锁的线程长时间不释放锁, 等待线程可以放弃等待,去做其他事情
公平锁 多个线程在申请锁的时候,按照申请的时间顺序依次获得锁
锁绑定多个条件 可以同时绑定多个Condition对象(可以绑定多个条件) 相比: synchronized只能绑定一个条件)

方法三: 使用第三方同步互斥锁(适用于分布式系统场景下)

可以使用Zookeeper、Redission分布式锁, 实现在分布式系统下的资源同步。

3.2 非阻塞同步

原理: 先进行正常操作,如果发现有线程操作冲突,则再进行处理。

可以通过 CPU的CAS指令(Compare-and-swap)实现(JDK1.5之后)。

四、锁优化

使用同步互斥锁,会阻塞等待中的线程(使其挂起),而挂起线程、恢复线程都算是重量级操作(这些操作需要转入内核进行),给操作系统的并发与性能带来不小的压力。

JDK1.6后,引入了一系列的锁优化技术,尽量减少线程直接的挂起,主要如下:

4.1 自旋锁 & 自适应自旋锁

自旋锁 qq20170930-092844 2x

可以从上图中看到:自旋锁,将原先使得线程挂起的操作 改为自循环,等到锁资源释放后,再继续。这种操作节省了线程挂起/恢复的开销,但是占用了处理器处理的时间。

JDK1.6后默认开启锁的自旋,当然,锁自旋是有限制的,如果超过自定次数的自旋后还没获得锁,就直接挂起线程(用 -XX: PreBlockPin 参数来配置自旋次数,默认10)

自适应自旋锁

在自旋锁基础上,自旋的时间不固定,而是由前一次同一个锁的自旋状态以及时间决定。

4.2 锁消除

JVM的JIT编译器在编译的时候,对于一些代码写着要同步锁,但是实际不存在数据资源竞争,JVM会消除这种锁。

4.3 锁粗化(扩大锁的范围)

正常情况下,我们总是尽量缩小锁的范围,但是对于一些频繁在同一个对象上加锁的操作,甚至在循环中没有竞争的情况下加锁,这个时候JVM会将锁的范围适当的扩大,节省加锁的次数。

4.4 轻量级锁

如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

4.5 偏向锁

在无竞争的情况下,把整个同步操作都消除了。