Synchronized深度刨析

Posted 小王子jvm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Synchronized深度刨析相关的知识,希望对你有一定的参考价值。

并发编程中的三个问题

并发这玩意也不知道让多少新手村的伙伴退游,确实有难度,不过这样才有意思嘛!

可见性问题

什么是可见性:指一个线程对共享变量经行修改,另一个先立即得到修改后的最新值。

但是在并发中一个常见的问题就是无法保证可见性。也就是,假设多个线程并发执行,这个时候,如果对于共享变量,第一个线程拿到了值,后面的线程改变了这个值,前面的线程可能依然拿着旧值运算。

举个例子:一个线程获取boolean类型的标记flag经行while循环,另一个线程改变这个flag变量的值,前一个线程并不会停止循环。

private static boolean flag = true;     //共享数据
public static void main(String[] args) throws InterruptedException 
    Thread t1 = new Thread(() -> 
        while (flag)
            //这里什么都不做,这样一来,如果是这个线程执行就会进入死循环
        
    );
    t1.start();

    Thread.sleep(1000); //记得要休眠,否则,很多时候都是t2执行,看不出效果

    Thread t2 = new Thread(() ->
        flag = false;
        System.out.println("线程二改变了数据");
    );
    t2.start();

所以并发中如何保证可见性这就是一个问题。

原子性

什么是原子性:在一次或者多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有操作都不执行。

这么一想会发现和数据库中的事务的原子性简直是一模一样。不久几个操作要么都执行要么都不执行吗。

举个例子:

private static int num = 0;

public static void main(String[] args) throws InterruptedException 
    /**
     * 这个线程就是把这个数加到100
     */
    Runnable runnable = () -> 
        for (int i = 0; i < 100; i++) 
            num++;
        
    ;
	//用一个集合装载线程,方便中断主线程,避免提前结束
    ArrayList<Thread> runnables = new ArrayList<>();
    for (int i = 0; i < 5; i++) 
        Thread thread = new Thread(runnable);
        thread.start();
        runnables.add(thread);
    
    
	//每个线程调用join方法都可以用来阻塞主线程。
    for (Thread thread : runnables) 
        thread.join();
    
    System.out.println(num);	//最后的结果很可能小于500

join方法的用法可以参考多线程常用API

这个例子中,num++可不是一步操作,编译之后看字节码文件(javap -p -v 字节码文件):

其中,对于num++ 而言(num 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic#12//Fieldnum:I
iconst_1
iadd
putstatic#12//Fieldnum:I

看到了吧,num++并不是一条语句。这样一来,如果是一个线程执行,到也没有什么问题。但是如果多线程执行,就可能,造成一个线程获得数据0,然后被中断,另一个获得0并加一,然后恢复,结果依然用0去加一,本应该为2的结果最后确定可能出现结果为1。

有序性

有序性应该很好理解,就是代码按照编写的顺序执行,但是呢,编译器可不一定按照你编写的代码进行编译!因为很多时候编译器会给你优化代码,可能就不是按照写的顺序进行编译。

举个例子:

private static boolean flag = false;     //共享数据
private static int num = 0;
public static void main(String[] args) throws InterruptedException 
    Thread t1 = new Thread(() -> 
        if(flag)
            num = num + num;
         else 
            num = 1;
        
        System.out.println(num);
    );
    t1.start();

    Thread t2 = new Thread(() ->
        num = 2;
        flag = true;
    );
    t2.start();

正常情况下,不论t1还是t2先执行,结果都不可能为0吧,但是如果编译后t2这两句因为谁先执行都不影响自己,所以有可能编译后执行顺序是这样的:

Thread t2 = new Thread(() ->
    flag = true;
    num = 2;
);

也就是编译器有可能觉得这样编译效率更高,但是如果是这样,就可能出现结果为0!

这也就是并发中的有序性问题!

小结一下

总的来说,在多线程的情况下,基本上就是这几个问题,搞懂这些玩意,自然就可以搞懂并发这个玩意。

Java内存模型概述

冯诺依曼结构

在这之前,先看一下计算机的基本内存模型(作为一个学计算机的,这个必须知道)

提到这又又又又一次需要提到:冯诺依曼结构,即计算机由五大部分组成:

  • 输入设备(比如我们的键盘)
  • 输出设备(显示器,声音)
  • 存储器(内存)
  • 控制器
  • 运算器

后面的控制器和运算器是划分到CPU中。

基本知识

  • CPU:中央处理器,怎么运算,怎么控制,都是由这玩意搞。我们写的程序最后也会编程一条一条的指令,然后被CPU去执行,去处理数据。

  • 内存:我们跑起来的程序总需要有地方可以放吧,不就是放在内存中吗,电脑中的内存条,越大不就可以放更多的东西了吗,这不就是意味着后台可以开启的进程就越多吗。

  • 缓存:内存读取写入数据是有一个上限的,但是CPU执行的速度远比这个快,如果CPU都把一条指令执行完了,内存却还没有读取完下一条,这就浪费太多时间。这个时候缓存就来了,内存是单独的一的设备,缓存相当于在CPU自家开了一个小仓库,这样读取数据不需要每次跑到内存那么远去读。最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存,CPU缓存模型如图下图所示:

一个处理的过程就是:

CPU先看L1中有没有数据,有就取出(也就是命中)经行运算,然后把处理的最新结果刷新到主存(也就是内存)中。如果没有命中,会依次到L2,L3,直到内存,然后把这个数据放一份到前面的缓存中。

Java中的内存模型

首先自行区分Java中的内存结构和内存模型这个概念Java内存模型的英文名称为Java Memory Model(JMM),其并不想JVM内存结构一样真实存在,而是一个抽象的概念。JMM和线程有关,它描述了一组规范或规则,一个线程对共享变量的写入时对另一个线程是可见的。Java多线程对共享内存进行操作的时候,会存在一些如可见性、原子性和顺序性的问题,JMM是围绕着多线程通信及相关的一些特性而建立的模型。

这张图中,就是描述每个线程都有自己的工作内存,这个部分独有,互不干扰,但是对于共享的主内存就可能出现各种并发问题。

  • 主内存:所有的线程共享。
  • 工作内存:每个线程都有自己的工作内存,工作内存只存储该线程对主内存的副本。线程对所有数据进行操作都是在自己的工作内存中完成的。

这个玩意就是synchronized和volatile这些东西可以控制并发安全的核心。

了解一下这个工作内存到主内存这个读写过程:

  1. 一个线程操作之前先对该数据加锁,防止别人乱搞(此时会清空工作内存中这个变量的值)
  2. 然后读取数据
  3. 把数据加载到工作内存
  4. 对数据操作
  5. 操作完了再写回主内存
  6. 释放这个锁(只有同步数据完成才可以释放)

Synchronized解决并发问题

这个关键字可以保证再同一时刻最多只有一个线程执行这段代码,这样就保证了并发情况下的安全问题。

synchronized(一把锁)
    //需要被保护的代码

保证原子性

还是上面的原子性的例子:

private static int num = 0;
private static Object lock = new Object();

public static void main(String[] args) throws InterruptedException 
    /**
     * 这个线程就是把这个数加到100,注意加锁了
     */
    Runnable runnable = () -> 
        for (int i = 0; i < 100; i++) 
            synchronized(lock)
                num++;     
            
        
    ;
	//用一个集合装载线程,方便中断主线程,避免提前结束
    ArrayList<Thread> runnables = new ArrayList<>();
    for (int i = 0; i < 5; i++) 
        Thread thread = new Thread(runnable);
        thread.start();
        runnables.add(thread);
    
    
	//每个线程调用join方法都可以用来阻塞主线程。
    for (Thread thread : runnables) 
        thread.join();
    
    System.out.println(num);	//最后的结果很可能小于500

这个时候无论怎么运行,结果都会是500。

进行反编译看这个结果:(会发现多了monitorenter这个指令)

怎么理解这个加锁过程:就是一个线程准备操作num这个变量,就会把这个东西占位独有,用自己定义的lock变量提示对方已经被我锁住了(相当于上厕所门把手,你进门会锁门,从外边看就变成了红色,别人就不会NT还去尝试开这门,而是在外边等着)。

所以再次理解这个原子性,这分明就是好几条指令,但是这些指令再某个时刻只能被一个人操作!

保证可见性

依然是上面的例子:

private static boolean flag = true;     //共享数据
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException 
    Thread t1 = new Thread(() -> 
        while (flag)
            synchronized (lock)
				//加个锁,
            
        
    );
    t1.start();

    Thread.sleep(1000); //记得要休眠,否则,很多时候都是t2执行,看不出效果

    Thread t2 = new Thread(() ->
        flag = false;
        System.out.println("线程二改变了数据");
    );
    t2.start();

这个时候无论怎么运行都不会出现线程t1一直跑的情况。

那之前没加锁为什么会出现这种情况呢?了解完Java内存模型应该就明白了。

因为每个线程都是都是有自己的工作内存,假设线程t1先拿到数据flag,他就会复制到自己的工作内存,然后就死循环,因为他没对数据操作改变,就不会刷新主内存。然后t2拿到,改变了,刷新了主内存,但是对t1来说,他一直使用的都是之前复制的。

加了这个锁之后呢,每次循环我都要加锁,会对主内存操作(参考内存模型图),出了这个同步代码块又会释放锁。也会对主内存操作。然后如果t2改变了,这个时候t1由于死循环,会不断的重复这个代码块,也就是不断地去主内存刷新,自然能读取到t2改变的新数据。

保证有序性

上面提到过,编译器在编译的时候,不一定所有的代码都是按照我们写的方式经行编译。

但是像这样的代码一定会按照我们写的顺序执行:(有数据依赖关系)

//读后写,这个例子中,就必须按照这种顺序编译,否则就会结果出错
int a = 1;
int b = a;

//总之如果一些代码没有依赖关系,就有可能发生重排序。例如:
int a = 1;
int b = 2;
int c = a + b;
//这个也可能发生重排序,但是最多只能是a b互换,c必须是最后
int b = 2;
int a = 1;
int c = a + b;

然后使用synchronized是如何保证的呢:

private static boolean flag = false;     //共享数据
private static int num = 0;
private static Object o = new Object();
public static void main(String[] args) throws InterruptedException 
    Thread t1 = new Thread(() -> 
        synchronized(o)
            if(flag)
                num = num + num;
             else 
                num = 1;
            
        
        System.out.println(num);
    );
    t1.start();

    Thread t2 = new Thread(() ->
        synchronized(o)
            num = 2;
        	flag = true;
        
    );
    t2.start();

这样一来,其实t2中的这两句依然有可能重排序,但是,不论怎么排序,这两句一定是被执行完了,才能轮到t1,或者说t1执行完了才能轮到t2,不存在,t2执行到重排序后的flag = true,然后t1执行,这样也就保证了整个过程结果的都是符合预期的!

Synchronized的特性

可重入性

先给出定义:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获得该锁。

public class ZiJie 
    public static void main(String[] args) 
        Runnable runnable = () -> 
            synchronized (ZiJie.class)
                System.out.println("第一层");
                synchronized ((ZiJie.class))
                    System.out.println("第二层");
                
            
        ;

        new Thread(runnable).start();
        new Thread(runnable).start();
    

//最后的结果一定是:
第一层
第二层
第一层
第二层

当然也可以这么写:(更加的符合实际一点)

public class ZiJie 
    public static void test()
        synchronized (ZiJie.class)
            System.out.println("第二层");
        
    
    public static void main(String[] args) 
        Runnable runnable = () -> 
            synchronized (ZiJie.class)
                System.out.println("第一层");
                test();
            
        ;

        new Thread(runnable).start();
        new Thread(runnable).start();
    

这个是什么意思,好比你手里拿着一个本子,你第一次进入synchronized,拿到这个锁,你就记录这把锁数量为1,此时别人肯定拿不到,然后你再进一个,发现又是这把锁,然后就把这个锁数量变为2,这个时候即使你退出第一层synchronized,别人依然进不来,因为,你表示你还有一层没有出去。(也就是你本子上必须写了这个数量为0的时候,别人才可以使用)

总结:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.

可重入带来的好处:

  • 避免死锁

    避免死锁的原因: 如果synchronized不具备可重入性,当一个线程想去访问另一个方法时,它自身已经持有一把锁,而且还没有释放锁,又想获取另一个方法的锁,于是造成了永远等待的僵局,就会造成死锁。有了可重入性后,自己持有一把锁,并且可以直接进入到内层函数中,就避免了死锁。

    注意这是针对同一把锁。

  • 更好的封装代码

    编程人员不需要手动加锁和解锁,统一由JVM管理,提高了可利用性。

Synchronized可重入性的作用范围是整个获得锁的线程,线程内部的所有被调用的方法都共享该锁。

不可中断性

一旦这个锁被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁,如果别人永远不释放锁,那么我只能永远等下去

相比之下,Lock类,可以拥有中断的能力,第一点,如果我觉的我等的时间太长了,有权中断现在已经获取到锁的线程的执行,第二点,如果我觉的我等待的时间太长了不想等了,也可以退出

synchronized 的不可中断演示:

public static void main(String[] args) throws InterruptedException 
    Runnable runnable = () -> 
        synchronized (ZiJie.class)
            System.out.println(Thread.currentThread().getName()+"第一层");
            while (true)

            
        
    ;

    Thread t1 = new Thread(runnable,"线程1");
    Thread t2 = new Thread(runnable,"线程2");
    t2.start();
    t1.start();
    Thread.sleep(200);
    //下面这两种一个处于RUNABLE,另一个处于BLOCKED
    System.out.println(t1.getState());	
    System.out.println(t2.getState());

查看源码如下:

public enum State 
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * @link Object#wait() Object.wait.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>@link Object#wait() Object.wait with no timeout</li>
     *   <li>@link #join() Thread.join with no timeout</li>
     *   <li>@link LockSupport#park() LockSupport.park</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>@link #sleep Thread.sleep</li>
     *   <li>@link Object#wait(long) Object.wait with timeout</li>
     *   <li>@link #join(long) Thread.join with timeout</li>
     *   <li>@link LockSupport#parkNanos LockSupport.parkNanos</li>
     *   <li>@link LockSupport#parkUntil LockSupport.parkUntil</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;

发现BLOCKED表示处于阻塞状态。

Lock锁演示

Lock是有两中的,一个是普通的lock方法,同synchronized一样是不可中断的。然后就是另一个tryLock,可中断锁。

public static void main(String[] args) throws InterruptedException 
    ReentrantLock lock = new ReentrantLock();
    boolean flag = true;
    Runnable runnable = () -> 
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"执行中");

        while (flag)

        
        lock.unlock();
    ;

    Thread t1 = new Thread(runnable,"线程1");
    Thread t2 = new Thread(runnable,"线程2");
    t2.start();
    Thread.sleep(200);
    t1.start();
    t1.interrupt();
    Thread.sleep(200);
    //一个处于WAITING,另一个RUNABLE
    System.out.println(t1.getState());
    System.out.println(t2.getState());

如果使用tryLock(可中断),成功拿到返回true,这样即使别的线程没拿到锁,就会进入else分支!

public static void main(String[] args) throws InterruptedException 
    ReentrantLock lock = new ReentrantLock();
    boolean flag = true;
    Runnable runnable = () -> 
        boolean b = lock.tryLock();
        if(b)
            System.out.println(Thread.currentThread().getName()+"执行中");
            while (flag)

            
            lock.unlock(以上是关于Synchronized深度刨析的主要内容,如果未能解决你的问题,请参考以下文章

深度刨析-reduxTodo

C++三大特性之继承,由浅入深全面讲解,由基础语法到深度刨析。

C++三大特性之继承,由浅入深全面讲解,由基础语法到深度刨析。

一篇搞定CAS,深度讲解,面试实践必备

一篇搞定CAS,深度讲解,面试实践必备

一篇搞定CAS,深度讲解,面试实践必备