在前面了解过一些java多线程基础之后,现在,我们用多线程来解决一个实际问题。 假定每个线程可以将一个数字加到100000,现在我们用十个线程,同时相加,看看结果是不是1000000?,代码如下:
ackage com.dhb.concurrent.test;
import java.util.concurrent.CountDownLatch;
public class SyncDemo implements Runnable{
private static int count = 0;
static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10; i++) {
Thread t = new Thread(new SyncDemo());
t.start();
}
countDownLatch.await();
System.out.println(count);
}
private void add(){
for (int i = 0; i < 100000; i++) {
count++;
}
countDownLatch.countDown();
}
@Override
public void run() {
add();
}
}
在上述代码中,分别启动了10个线程,现在看看输出结果是不是1000000?
361177
结果跟预想的并不一样,我们再执行一次看看?
294781
每次还不一样。 这就说明,一定遇到了线程安全的问题。即我们定义的count这个成员变量,在10个线程并发访问的过程中,可能出现了脏读,即一个线程还没有写入完成,另外一个线程就读到了这个没写完的结果,这样就导致了最终的结果不为1000000。为此,我们解决这个问题,不得不想到一个关键字,synchronized来解决。
并发问题,通常需要解决两类问题,一个是互斥,即资源只能同时由一个线程来访问,当这个线程在访问的过程中,其他线程不能访问这个变量。这就是互斥。另外一个问题就是同步,同步主要是解决线程间通信的问题。即线程由于获取不到访问这个变量之前需要的锁资源,就会进入阻塞状态,让出CPU执行权限。那么何时能够重新执行呢,就需要访问资源的线程在执行完成之后进行通知,wait和notify方法就是很好的线程同步方法。 实际上synchronized的英文就是同步的意思,但是比较有意思的是,synchronized主要是解决的互斥问题。即加锁。 我们将代码修改为如下方式:
private void add(){
synchronized (SyncDemo.class) {
for (int i = 0; i < 100000; i++) {
count++;
}
countDownLatch.countDown();
}
}
再次查看执行结果:
1000000
果然,输出的就是想要的结果了。但是,如果我们将sunchronized的代码块修改为如下呢?
private void add(){
synchronized (this) {
for (int i = 0; i < 100000; i++) {
count++;
}
countDownLatch.countDown();
}
}
结果如下:
205975
又不能满足了。这说明,synchronized代码块,括号中锁定的对象,是有讲究的,前面的SyncDemo.class,由于SyncDemo.class是个特殊的对象,只有一个对象。因此多线程访问的时候就会形成互斥。而改成this之后,由于这个类在使用的时候通过new,导致了多个实例,实例与实例之间加索就不能构成互斥关系。 另外,上述代码块也可以与如下情况等价:
private synchronized void add(){
for (int i = 0; i < 100000; i++) {
count++;
}
countDownLatch.countDown();
}
如果方法中除了代码块没有任何内容,那么这种方式与前面的synchronized(this)等价。 此外synchronized(SyncDemo.class)也与如下等价:
private static synchronized void add(){
for (int i = 0; i < 100000; i++) {
count++;
}
countDownLatch.countDown();
}
对,就是将方法改为静态方法,这样锁住的就是类了。我们总结一下:
分类 | 详细分类 | 被锁的对象 | 代码示例 |
---|---|---|---|
方法 | 实例方法 | 类的实例对象 | public synchronized void method(){ … … } |
方法 | 实例方法 | 类对象 | public static synchronized void method() { … … } |
代码块 | 实例对象 | 类的实例对象 | synchronized(this) { … … } |
代码块 | class对象 | 类对象 | synchronized(SyncDemo.class){ … … } |
代码块 | 任意实例对象Object | 实例对象Object | Object lock = new Object(); synchronized(lock){ … … } |
理论上来说,synchronized()的括号中可以是任意对象。但是,需要注意的是, 一般我们最好不要用String和包装类做为被锁定的对象。 这是因为,在jvm中,对这些类进行特殊处理,String类,尤其是G1中要是开启了字符串去重,那么全部jvm中都只有这一个对象。这样会导致许多系统其他的功能受到影响。包装类由于有常量池,也会导致同样的问题,这样你会莫名其妙的感觉系统卡顿。
在前面学习伪共享的时候了解过,操作系统中,实际上CPU与主内存之间存在多级缓存架构。而这些多级高速缓存的速度远远高于主内存的读取速度。其结构如下:
高速缓存和主内存以及CPU的同步关系,需要通过缓存一致性协议来确保数据的一致性。如MESI、MSI等协议。通过这些协议,才能保证各内存高速缓存与主内存的数据一致性。这个模型如下图所示
除了高速缓存之外,为了使CPU运算单元尽可能的充分利用,还会对输入的代码进行优化,其先后顺序会被改变。因此,在实际代码的执行过程中,其先后顺序不一定按照代码顺序来执行。这就是指令的重排序。关于这一点的细节再后续volatile关键字部分进行详细介绍。 那么JAVA实际上也是与这个模型类似,再java虚拟机中,虚拟机做为最外层的容器,其执行的逻辑与这个模型也非常相似。实际上,线程是CPU的最小执行单位,Java的内存模型实际上是对这个模型的抽象。在java中,也分为主内存和工作内存:
在java中,工作内存与主内存的交互,主要通过如下8种活动来进行,每个活动都是原子性的。
可以看到,上述图种绿色部分就是在工作内存种执行的活动。其他活动则是在主内存种执行。其过程详细如下图:
在每个线程中,其执行的时候的变量,实际上是其主内存中变量的副本。那么如果采用了synchronized,则会用lock操作锁定该变量,之后其他线程并无法访问。之后再进行read、load过程,之后使用或者赋值。 对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; volatile修饰的变量则除外。 上述这些操作,JSR133规定,需要满足如下规则:
本文从线程安全问题引出了synchronized的用法。以及java内存模型的简单介绍。当然,synchronized还有可重入,以及底层具体实践和优化的知识也是非常重要的部分。后续对此详细介绍。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有