关于java:多线程synchronized基础

24次阅读

共计 2691 个字符,预计需要花费 7 分钟才能阅读完成。

当初的节奏曾经要变成一周一更了吗,不行,相对不行

本次的文章也是根本讲烂了的 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 关键字

代码块

在上述类中新增一个办法

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

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

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

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

留神这里用的锁是线程本人 new 的一个实例

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

第一种状况就像一个房间只有一扇门,每个线程只有拿到同一个钥匙能力进房间,所以线程是平安的。第二种状况是线程本人 new 了一个实例,相当于给线程造了多个门,线程只须要开本人的那扇门就能进入房间。

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

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

测试后果是能可能保障线程平安,因为锁是 this,与下面不同的是整个过程咱们只 new 了一个对象。

一般办法

还有一种办法是间接在办法体外面增加 synchronized 关键字

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

能够发现同样也是能达到线程平安的目标

静态方法

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

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

后果如下:

原理

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

同步代码块

能够看到同步代码块无论是轻易 new 一个对象当锁,还是采纳 this 单锁,其实次要是由 monitorenter 和 monitorexit 来保障了线程的平安。

办法体

可看到办法体是在 flags 的字段里有个 ACC_SYNCHRONIZED 标记,两种形式的原理大略就这样,接下来着重讲下 monitor。

对象头

简略的说下对象头的组成,然而这个组成如同是 没有什么主观的外在表现形式 ,这里也只是写出了书本上以及博客上 少数批准的构造

其余的临时不必管,前期写虚拟机相干的文章的时候还会具体介绍,只有晓得 对象由对象头、实例数据和对齐填充组成,而对象头外面有个指向 monitor 的指针,这个 monitor 能够看作就是一个重量级锁

无关 monitor 的数据结构在 jvm 的源码,具体来说这里指的是 hotspot 的源码中,重要的变量正文也写在前面了。

因为每个对象都有对象头,每个对象头都有指向一个 monitor 的指针,所以每个对象都能作为锁;因为 monitor 中有个 count 的字段,所以反编译能够看到是应用了 monitorenter 和 monitorexit,用两次 monitorexit查找网上博客是说为了保障异样的状况下也能开释锁

正文完
 0