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

本次的文章也是基本讲烂了的synchronized,希望我写的比别人写的更简单易懂,哈哈哈。其实有关多线程的知识点有很多,无论哪门语言都是这样,所以以后会穿插着其他知识点来讲解,不然也是太枯燥了。
线程不安全
❝
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替进行,并且不需要额外的同步及调用方代码不必作其它的协调,这个类的行为仍然是正确的,那么成这个类是线程安全的。
❞
通俗一点来说,要想代码线程安全,其实就是保证「状态」的访问时不出错的,「对象的状态一般情况下指的是数据」。但是数据大多数情况都是「共享」,「可变」的。
其实在我们的日常开发中,遇到最多的线程不安全更多的是对「某一个变量的修改是否能达到预期」,所以下面的例子更多的聚焦于简单的保证变量的修改是安全的。
首先来看下著名的「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「查找网上博客是说为了保证异常的情况下也能释放锁」 。
原文始发于微信公众号(咖啡编程):【多线程】synchronized基础
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/23131.html