Java多线程(上)——锁、CAS、JMM、线程间通信

1 锁和CAS原理

一个小问题:什么样的变量需要注意线程安全问题?

解答:所有实例域、静态域、数组元素都保存在堆内存,堆内存在线程之间是共享的,需要保证线程安全。而局部变量、方法定义参数、异常处理器参数是线程私有的,不需要考虑。

1.1 volatile原理

如果字段被声明为volatile,那么Java线程内存模型确保所有线程看到这个变量的值是一致的。

JMM怎么保证这个可见性的?

对volatile变量进行写操作的时候,处理器把Java代码翻译成的汇编指令多了个Lock前缀,将会执行:

1)将当前处理器缓存行数据写会系统内存(LOCK#信号一般锁缓存,而不会锁总线);

2)使其他CPU里缓存了该内存地址的数据无效。

1.2 synchronized

1.锁住的范围

  • 锁普通方法:锁的是当前实例对象;
  • 锁静态方法:锁的是当前类的Class对象;
  • 锁代码块:锁括号里配置的对象。

2.synchorized锁方法和锁代码块实现原理的区别:

锁方法:在方法修饰符上加上ACC_SUNCHRONIZED;

锁代码块:使用monitorenter和monitorexit指令

3.synchronized是怎么锁住方法或者代码块的?

    先了解下“对象监视器(Monitor)”的概念:

    任意一个对象(Object)都拥有自己的监视器(Monitor),所以“加锁”实际上就是对对对象的监视器加锁,而且任意时刻只能有一个线程获取到对象监视器,没有获取到监视器的线程就要阻塞在获取锁那里,进入BLOCKING状态(加入同步队列)。

4.锁的升级

    synchronized的锁保存在对象头的Mark Word里。

锁状态

25bit

4bit

1bit是否是偏向锁

2bit锁标志位

无锁状态

对象的hashCode

对象分代年龄

0

01

是否是偏向锁:1-是,0-否

锁标志位:00-轻量级锁,01-偏向锁,10-重量级锁,11-GC标记

锁的升级:无锁、偏向锁、轻量级锁、重量级锁,随着竞争逐渐升级,且只能升级不能降级。

1)偏向锁

加锁:在对象头里存储线程id,当新的线程进入同步块时,检查对象头的Mark Word里是否存储了指向当前线程的偏向锁,有的话就获取到锁,没有就要看下偏向锁标识是否被设置为1,如果设置了就尝试用CAS将对象头的偏向锁指向当前线程,没设置则使用CAS竞争锁。

解锁:只有当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。

2)轻量级锁

加锁:JVM首先在线程的栈桢中开辟一块空间用来存放锁记录,然后将对象头中的Mark Word复制到这块空间中,最后使用CAS将对象头中的Mark Word替换为指向这块空间的指针,如果替换成功就获取到锁,替换失败则使用自旋(死循环)来获取锁。

解锁:使用CAS将刚才的那块空间里的Mark Word替换回对象头,替换成功就释放锁了,失败了的话说明当前锁存在竞争,锁就会膨胀成重量级锁。

1.3 原子操作实现原理

Java中使用锁和循环CAS两种方式实现原子操作。

1.使用循环CAS实现原子操作

利用处理器的CMPXCHG指令实现。

自旋CAS:循环进行CAS操作直到成功为止。

JDK的并发包提供了一些支持原子操作的类,如java.util.concurrent.atomic.AtomicInteger:

(getIntVolatile和compareAndSwapInt都是native方法)

 

CAS实现原子操作存在三大问题:

  • ABA问题:一个变量的值开始是A,中间是B,最后是A,那CAS检查变量值发现值还是操作前的A,认为没变,符合预期,所以会更新这个值,但实际上已经变化了。解决办法是加上版本号,JDK原子类解决了ABA问题:当前引用等于预期引用、当前标志等于预期标志才会更新值。
  • 循环开销大。自旋CAS实际上就是一个无限死循环,如果长时间不成功,会给CPU带来非常大的开销。
  • 只能保证一个共享变量的原子操作。

 

2.使用锁实现原子操作

    只有获取锁的线程能操作锁定的内存区域。

    JVM实现锁的方式,除了偏向锁,都使用了循环CAS:当线程想进入同步块的时候使用循环CAS方式获取锁,当退出的时候使用循环CAS释放锁。

 

2 Java内存模型(JMM)

2.1 JMM是什么?

    每个线程有一个私有的本地内存(Local Memory),存储了该线程读/写共享变量的副本,这个本地内存是JMM的一个抽象概念,不是真实存在的,是对缓存、写缓冲区、寄存器、编译器优化等的抽象。

线程A和B之间通信的步骤:

1)A把本地内存中的共享变量刷新到主内存(Main Memory,插的内存条)中;

2)线程B去主内存读取该共享变量。

——通过以上分析,我们知道了因为有一个本地内存的存在,所以每个线程看到的共享变量的值不一定相同。

2.2 重排序

编译器和处理器为了优化程序性能,会对指令序列进行重排序。

1)存在数据依赖的操作不会被重排序;

2)存在操作依赖的操作会重排序。

概念:

数据依赖,比如:a=1;b=a;  会按顺序执行。

操作依赖,比如:if(flag){b=a},可能先计算b=a并保存到缓冲中,然后再判断flag

2.3 volatile

1.对一个volatile变量的单个读/写操作,与在改变量上加锁效果一样。

2.volatile保证原子性和可见性

  • 可见性:对volatile变量的读,总是能看到任意线程对这个变量最后的写入;
  • 原子性:对任意单个volatile变量的读/写具有原子性,但对复合操作不保证原子性,例如volatile++实际是三步操作:先获取volatile值,然后对其执行+1操作,再把结果赋给volatile。

3.volatile是怎么保证所有线程看到的变量值相同的呢?——volatile写-读内存语义:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存;
  • 当读一个volatile变量时,JMM会把该线程对应的本地变量置为无效,接下来从主内存中读取共享变量。

4.实现原理

内存屏障

 

2.4 双重检查(Double Check)

单例模式的一种写法,使用不当会引起线程安全问题。

1.延迟初始化的线程安全问题

public class UnsafeLazyInitialization {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {           //1:线程A执行
            instance = new Instance();    //2:线程B执行
        }
        return instance;
    }
}

某一时刻,线程B执行到new Instance(),刚为对象分配了内存空间,还没来得及初始化,线程A判断instance不为null,就会返回一个不可用的对象。

为什么呢?

正常创建对象会经过3步:

memory = allocate(); //1:分配内存空间

ctorInstance(memory); //2:初始化对象

instance = memory; //3:设置instance指向这块内存地址

 

但是编译器可能会重排序,导致对象按下面这个流程创建:

memory = allocate(); //1:分配内存空间

instance = memory; //3:设置instance指向这块内存地址

ctorInstance(memory); //2:初始化对象

2.怎么解决线程安全问题?

可以在getInstance方法上加上synchronized同步,但是会带来严重的性能问题。

3.双重检查锁定——一个错误的优化

public class DoubleCheckedLocking {
    private static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckLocking.class) {
                if (instance == null) {
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

    只有当instance为null的时候才加锁,这样的话变量一旦实例化就不会再加锁了,认为效率可以大大提高,但其实仍然是问题的,原因跟上面介绍的懒加载一样,都是因为当判断instance不为null的时候,instance引用的对象可能还没有完成初始化。

    解决办法就是给instance变量加上volatile,valatile通过插入内存读写屏障保证了对任意变量的写一定先于对该变量的读(happens-before原则)

3 线程运行和线程间通信

3.1 线程的状态

状态名称

说明

NEW

初始状态,线程被构建,但还没有调用start()方法

RUNNABLE

运行状态

BLOCKED

阻塞状态,阻塞在获取锁

WAITING

等待状态,等待其他线程做出特性的动作(通知或中断)

TIME_WATIING

超时等待状态,可以在指定时间内返回

TERMINATED

终止状态,表示当前线程已经执行完毕

补充:

1.线程可以设置优先级:setPriority

2.如果线程设置成了Daemon,将成为守护线程,只有当不存在Deamon线程的时候,JVM才会退出。

3.2 安全地终止线程

不要使用过期的stop()、suspend()、resume()。

可以设置一个boolean变量标志是否需要停止任务:

class SafeTerminate implements Runnable{
    private volatile boolean on = true;

    @Override
    public void run() {
        while (on && !Thread.currentThread().isInterrupted()) {
            // doSomeThing;
        }
    }

    public void cancel() {
        on = false;
    }
}

3.3 线程间通信

3.3.1 等待/通知机制(wait/notify)

问题引出:当达到...条件时执行。

(1)小学生怎么做?(对不起我又黑了一次小学生)

小学生用轮询

while (value != desire) {
    Thread.sleep(1000);
}
doSomethig();

要想更快地感知到变量的最新值,就把sleep时间调短点,最极端是永不休息:

while(value != desire) {}

这样CPU要被打满。

(2)大学生怎么做?

     成年人的世界里没有那么多时间轮询,我们可以坐等别人通知我条件已经达到了,然后我就能开干了。有点意思。

我们先回顾一个小学生问题:

java对象的老祖宗java.lang.Object有哪些方法?

hashCode()、equals(Object obj)、clone()、toString()、notify()、notifyAll()、wait()、wait(long timeout)、finalize()

    面试的话接下来要问为啥wait和notify要放在这个类里,答案是:

    因为任意一个对象都有自己的监视器,所有这俩方法对任意对象都要适用,正好Object是所有对象的老祖宗,然后就放Object类里了。

 

接下来看看典型的用wait/notify实现的“生产者/消费者模型”:

消费者:

synchronized(对象) {
    while (条件不满足) {
    对象.wait();
    }
    对应的处理逻辑
}

生产者:

synchronized(对象) {
    改变条件;
    对象.notifyAll();
}

源码位置:https://github.com/hfutlilong/test/tree/master/src/main/java/test/multiThread

 

wait/notify使用注意点:

1)wait()、notify()、notifyAll()使用时要对调用对象加锁。

2)wait()会立即释放锁,notify()不会立即释放锁,而是要等到调用notify()的线程释放锁。

3)notify()会随机唤醒一个wait的线程,notifyAll()则唤醒所有线程。

4)notify()把等待队列(WaitQueue)中的一个线程从等待队列移到同步队列(SynchronizedQueue),线程状态由WATING变为BLOCKED。

 

3.3.2 Thread.join()

还可以设置超时时间:join(long millis)

含义:当前线程等待join()结束再往下走。

示例代码:

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread other = new Thread(new OtherThread(), "other");
        other.start();
        other.join();
        System.out.println("其他线程结束了,开始执行我的");
    }

    static class OtherThread implements Runnable {
        @Override
        public void run() {
            System.out.println("a");
            System.out.println("b");
            System.out.println("c");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

3.3.3 ThreadLocal

线程变量,是一个以ThreadLocal对象为key、任意对象为value的存储结构:

 

 

一个示例:统计方法的执行时间

public class Profiler {
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        protected Long initalValue() {
            return System.currentTimeMillis();
        }
    };

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        Thread.sleep(3000);
        System.out.println("Cost: " + Profiler.end() + " mills");
    }
}

运行结果:

4 Lock和AQS

4.1 Lock锁

需要显式获取和释放,而synchronized是隐性的。

Lock lock = new ReentrantLock();
lock.lock();
try {
    //doSomething
} finally {
    lock.unlock;
}

可以设置超时时间:

tryLock(long time, TimeUnit unit)

在time时间内没获取到锁,返回false。

 

4.2 队列同步器(AQS)

AbstractQueuedSynchronizer

准备在下一篇博客中结合源码讲解。

 

4.3 ReentrantLock

re-entrant-lock,字面意思“可重入锁”,啥是可重入?线程获取到对象的锁之后还能再次获取。

ReentrantLock默认使用非公平锁,非公平可以提升效率,一个线程在竞争锁时无需判断前面是否有等待的线程,直接和所有的现有线程抢锁。

在下一篇博客中介绍什么是公平锁、什么是非公平锁。

 

4.4 Condition接口——Lock的等待/通知机制

类似于对象监视器的wait()、notify()机制,Condition提供了await()、signal()、signalAll()。

public class ConditionTest {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal() {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

 

预告:

中篇将介绍AQS、公平锁和非公平锁、Condition原理和源码;

下篇介绍线程池、原理、源码以及在工程实践中的使用,同时介绍ThreadLocal不安全的情况。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值