共计 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查找网上博客是说为了保障异样的状况下也能开释锁。