现在的节奏已经要变成一周一更了吗,不行,绝对不行
本次的文章也是基本讲烂了的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查找网上博客是说为了保证异常的情况下也能释放锁 。