《使用线程的场景 - 线程并发安全的产生原因以及解决方法》

1. 理解多线程

利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任务。 像这样的每个子任务都叫作一个“线程”(Thread)。编写程序时,可将每个线程都想象成独立运行,而且都有自己的专用CPU。一些基础机制实际会为我们自动分割CPU 的时间。我们通常不必关心这些细节问题, 所以多线程的代码编写是相当简便的。 这时理解一些定义对以后的学习很有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。 “多任务”操作系统能同时运行多个进程(程序)——但实际是由于CPU 分时机制的作用,使每个进程都能循环获得自己的CPU 时间片。但由于轮换速度非常快,使得所有程序好像是在“同时”运行一样。“线程” 是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。 多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并让它独立于主程序运行。一个很好的例子便是“Quit”或“退出”按钮——我们并不希望在程序的每一部分 代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。——《THINKING IN JAVA》

2. 多线程引起的安全问题 

  虽然使用多线程可以将同特定的事件联系在一起的独立运行的子任务分隔出来,但是如果不加处理而直接使用多线程有时会引起安全问题。

 可将单线程程序想象成一种孤立的实体,它能遍历我们的问题空间,而且一次只能做一件事情。由于只有一个实体,所以永远不必担心会有两个实体同时试图使用相同的资源,就像两个人同时都想停到一个车位,同时都想通过一扇门,甚至同时发话。 进入多线程环境后,它们则再也不是孤立的。可能会有两个甚至更多的线程试图同时使用同一个有限的资源。必须对这种潜在资源冲突进行预防,否则就可能发生两个线程同时访问一个银行帐号,打印到同一台计算机, 以及对同一个值进行调整等等。——《THINKING IN JAVA》

   于是我们可以知道了所谓多线程引起的安全问题指的就是多个独立运行的线程可能会同时操作同一个资源,这会引起冲突(冲突在此不作详细介绍,仅通过下面例子示意)。

3. 多线程对同一个值进行调整

 写一个任务线程 class Task extends Thread {...},其中有一个静态属性static int num = 0; 重写run方法,执行对num做10000次累加的操作。

class Task extends Thread{
    static int num = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num++;
        }
    }
}

我们以三个该任务线程为例进行演示,先创建三个任务线程并执行,最后看结果。

class main {
    public static void main(String[] args) {
        Task t1 = new Task();
        Task t2 = new Task();
        Task t3 = new Task();
        t1.start();
        t2.start();
        t3.start();
        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num: " + Task.num);
    }
}

   三个线程分别都进行了对同一个变量 num 的10000次的累加操作,最后输出的 num 应该是30000才对,可是我们四次运行的结果(结果如下表)都是介于10000~30000之间的一个数。这就会让我们联想到线程冲突而引起的安全问题。

实验次数1234
num输出26718230042702929703

 4. 原因和解决方法

   a) 冲突的原因 

堆内存:进程公共区域,存储所有线程都可以看到的资源。

 在知道原因之前,我们先要对Java的JVM存储机制有初步了解。静态(static) 属性会存储在一个名为堆的内存中,这个堆只用一个。但是会有多个存储进程的栈内存。我们构建的三个线程会从堆内存中读取并复制一份 num 的值到栈内存中,对栈内存中的 num 累加一次后再写入到堆内存中的 num。当有一个线程复制了 num 但是还没有重新写入,有另外一个线程也做了复制操作,这时这两个线程做的操作实际上就只是对num做了一次累加,于是因为这种不同步的机制就会让实验中 num 的值无法到达30000。

  b) 用synchronized修饰符来解决 

在任务线程中增添一个静态属性object,并用synchronized对其上锁做到“监听器”的作用。这样每个线程要对 num 复制之前都会观察object是否上锁,如果object处于上锁中,那么就会等待监听而不去复制,如果处于开锁状态,就执行复制操作。

// 新写的Task
class Task extends Thread{
    static int num = 0;
    static Object object = new Object();
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 上锁
            synchronized (object) {
                num++;
            }
        }
    }
}

   最后执行的结果如下表所示,表明我们解决了多线程冲突的安全问题。

实验次数1234
num输出30000300003000030000

 5. 参考资料

 1. The code of THINKING IN JAVAicon-default.png?t=M666https://github.com/lwk-blog/thing-in-java

2. Java并发编程icon-default.png?t=M666https://www.runoob.com/java/java-multithreading.html 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值