【多线程】synchronized基础

现在的节奏已经要变成一周一更了吗,不行,绝对不行

【多线程】synchronized基础

本次的文章也是基本讲烂了的synchronized,希望我写的比别人写的更简单易懂,哈哈哈。其实有关多线程的知识点有很多,无论哪门语言都是这样,所以以后会穿插着其他知识点来讲解,不然也是太枯燥了。

线程不安全

在《Java并发编程实战》中有这么一句话

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替进行,并且不需要额外的同步及调用方代码不必作其它的协调,这个类的行为仍然是正确的,那么成这个类是线程安全的。

通俗一点来说,要想代码线程安全,其实就是保证「状态」的访问时不出错的,「对象的状态一般情况下指的是数据」。但是数据大多数情况都是「共享」「可变」的。

其实在我们的日常开发中,遇到最多的线程不安全更多的是对「某一个变量的修改是否能达到预期」,所以下面的例子更多的聚焦于简单的保证变量的修改是安全的。

首先来看下著名的「i++不安全」的例子

package concurrent.safe;

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

public class SynchronizedDemo {
    //普通方法,代码块,静态方法
    public static void main(String[] args) throws InterruptedException {
        int threadSize = 1000;
        ThreadAddExample example = new ThreadAddExample();
       //保证主线程结束于各个子线程的后面
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
      
       //以不推荐的方式启动一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < threadSize; i++) {
            executorService.execute(() -> {
                example.add();
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
      
       //关闭线程池,不然会一直阻塞
        executorService.shutdown();
        System.out.println(example.get());
    }
}

class ThreadAddExample {

    private static int cnt = 0;

    public void add() {
            cnt++;
    }

    public int get() {
        return cnt;
    }
}

整个流程是说创建了一个线程池,然后执行了1000个任务,每个任务都是对cnt进行++操作,最后读取cnt。但是没有进行保护,所以肯定存在两个线程同时修改了cnt变量,导致了其中一个线程的修改是无效的,在本例中体现的就是「cnt不可能等于1000」

来看下「运行结果」,可以看到结果如预期,有的时候差的比较多,有的时候差的比较少,「主要还是看CPU」

【多线程】synchronized基础

用法

针对上述情况就需要使用一定同步措施来保证实施的结果是对的,本文主要采用的是「synchronized关键字」

代码块

在上述类中新增一个方法

public void addWithBlockSync1() {
    synchronized (ThreadAddExample.class{
        cnt++;
    }
}

是以ThreadAddExample这个类作为锁,这样每个线程都要能获取到这个类才能对cnt资源进行修改,最终的结果如下,可以看到「无论运行多少次结果都是1000」,说明没有两个及以上的线程在同一时间内修改cnt。

【多线程】synchronized基础

来看下同样是用synchronized包围代码块的另外一个例子

public void addWithBlockSync2() {
    synchronized (new ThreadAddExample()) {
        cnt++;
    }
}

注意这里用的锁是线程自己new的一个实例

【多线程】synchronized基础

奇怪了,为什么会线程不安全呢?

「第一种情况就像一个房间只有一扇门,每个线程只有拿到同一个钥匙才能进房间,所以线程是安全的。第二种情况是线程自己new了一个实例,相当于给线程造了多个门,线程只需要开自己的那扇门就能进入房间。」

那锁对象不是new ThreadAddExample() 而是 this 的情况呢

public void addWithBlockSync3() {
    synchronized (this) {
        cnt++;
    }
}

测试结果是能能够保证线程安全,因为锁是this,与上面不同的是整个过程我们只new了一个对象。

【多线程】synchronized基础
E9F122BE-86DC-4A1F-BA8B-B9A6120432CC-23969-00001E0211461F00

普通方法

还有一种方法是直接在方法体里面添加synchronized关键字

public synchronized void addWithMethodSync() {
    cnt++;
}

可以发现同样也是能达到线程安全的目的

【多线程】synchronized基础

静态方法

除了上述的方法,还有一种常用的就是在静态方法中使用关键字

public synchronized static void addWithStaticSync() {
    cnt++;
}

结果如下:

【多线程】synchronized基础
syn5

原理

采用javap -verbose xxx.class看下字节码文件

同步代码块

【多线程】synchronized基础
【多线程】synchronized基础
【多线程】synchronized基础

可以看到同步代码块无论是随便new一个对象当锁,还是采用this单锁,其实主要是由monitorenter和monitorexit来保证了线程的安全。

方法体

【多线程】synchronized基础
【多线程】synchronized基础

可看到方法体是在flags的字段里有个「ACC_SYNCHRONIZED」标志,两种方式的原理大概就这样,接下来着重讲下monitor。

对象头

简单的说下对象头的组成,但是这个组成好像是「没有什么客观的外在表现形式」,这里也只是写出了书本上以及博客上「多数同意的结构」

【多线程】synchronized基础

其他的暂时不用管,后期写虚拟机相关的文章的时候还会详细介绍,只要知道「对象由对象头、实例数据和对齐填充组成,而对象头里面有个指向monitor的指针,这个monitor可以看作就是一个重量级锁」

有关monitor的数据结构在jvm的源码,具体来说这里指的是hotspot的源码中,重要的变量注释也写在后面了。

【多线程】synchronized基础

因为每个对象都有对象头,每个对象头都有指向一个monitor的指针,所以每个对象都能作为锁;因为monitor中有个count的字段,所以反编译可以看到是使用了monitorenter和monitorexit,用两次monitorexit「查找网上博客是说为了保证异常的情况下也能释放锁」 。


原文始发于微信公众号(咖啡编程):【多线程】synchronized基础

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/23131.html

(0)

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!