由于很多优秀的Java Web容器或者是J2EE容器的涌现,作为一个java web程序员,很少或者不需要去处理线程的问题,因为服务器或者是框架(如Spring,Struts)等都帮我们处理好了。但当我们查看JDK的API的时候,我们总会看到一些类写着:线程安全或者线程不安全。最简单的例子,比如说StringBuilder这个类中,有这么一句:“将StringBuilder的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用StringBuffer.
为了说明线程的不安全会带来什么问题,下面手动创建一个线程不安全的类,然后在多个线程中去测试使用这个类,看看有什么效果。
/** * */ package com.wsheng.thread.simlesafe; /** * 在这个类中的Count方法是计算1一直加到10的和,并输出当前线程名和最后的总和, * 我们期望的结果应该是每一个线程都会输出55 * * @author Wang Sheng(Josh) * */ public class Count { private int num; // 我们想查看Count类的对象在内存中有几个, 以判断是否有资源共享和竞争 private int objectNum; public Count(int objectNum) { this.objectNum = objectNum; } public void count() { for (int i = 1; i <= 10; i++) { num += i; } // Object Number一值都是1,说明只有一个Count对象,保证多个线程共享一个Count对象。 System.out.println(Thread.currentThread().getName() + " : " + num + " Object Number: " + objectNum); } }
在这个类中的count方法是计算1一直加到10的和,并输出当前线程名,还有共享的对象(Count)的个数和数字的总和,我们期望的是每个线程都会输出55。
/** * */ package com.wsheng.thread.simlesafe; /** * @author Wang Sheng(Josh) * */ public class ThreadTest { public static void main(String[] args) { Runnable run = new Runnable() { int i = 1, j = 1; // 只会new一次,即10个线程共享1个对象 Count count = new Count(i++); public void run() { System.out.println(" ----- Thread running " + j++ + " times"); count.count(); } }; for (int i = 0; i < 10; i++) { new Thread(run).start(); } } }
这里启动了10个线程,我们先看下输出的结果是不是我们预期的那样
----- Thread running 3 times ----- Thread running 8 times Thread-8 : 110 Object Number: 1 ----- Thread running 10 times Thread-9 : 165 Object Number: 1 ----- Thread running 7 times ----- Thread running 6 times Thread-7 : 275 Object Number: 1 ----- Thread running 5 times ----- Thread running 2 times ----- Thread running 4 times ----- Thread running 1 times Thread-3 : 440 Object Number: 1 Thread-1 : 385 Object Number: 1 Thread-4 : 330 Object Number: 1 Thread-5 : 220 Object Number: 1 ----- Thread running 9 times Thread-2 : 55 Object Number: 1 Thread-6 : 550 Object Number: 1 Thread-0 : 495 Object Number: 1
我们看到只有一个线程(此处是Thread-2)线程输出的结果是我们期望的,而输出的每次都是累加的。为什么都是累加的呢?
根本的原因是我们创建的Count对象是线程共享的,一个线程改变了成员变量num的值,下一个线程正巧读到了修改后的num,所以会递增输出。
要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。 拿上篇博文中的例子来说明,在多个线程之间共享了Count类的一个对象,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程 栈),工作内存存储了主内存Count对象的一个副本,当线程操作Count对象时,首先从主内存复制Count对象到工作内存中,然后执行代码 count.count(),改变了num值,最后用工作内存Count刷新主内存Count。当一个对象在多个内存中都存在副本时,如果一个内存修改了 共享变量,其它线程也应该能够看到被修改后的值,此为可见性。由上述可知,一个运算赋值操作并不是一个原子性操作,多 个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款 100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇 款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操 作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后 取款,此为有序性。
特别说明: 1. 10个线程,可能一开始都从主内存中读取到count对象的num的值都是1并放到各自的线程栈的工作内存中,但是当线程1执行完并刷新结果到主内存以后,线程2会在进行具体的操作之前,会去清楚自己的工作内存并重新从主内存中读取新的变量num的值。
2. 有序性可以简单的理解为,无论是A线程还是B线程先执行,都要保证有序,即A线程要么先执行完,再执行B线程,或者B线程先执行完,再执行A线程。即要么先取款,或者要么先存款。
3. 这一点大家一定要注意:特性1是可见性,这是多个线程共享同一个资源时,多个线程天然具有的特性,但是特性2 即有序性并不是天然具有的,而是我们要通过相关的API来解决的问题,我们往往要确保线程的执行是有序的,或者说是互斥的,即一个线程执行时,不允许另一个线程执行。如果你足够细心,那就会提出这样一个疑问,那么在上面的例子中,我们并没有用到线程相关的API,但最后的线程之间的输出结果是如此的有序(输出结果是很有规律的:55, 110,165, 220等后一个比前一个恰好大55的输出结果,如果上面的10个线程是随机执行的,那么输出结果肯定不是55, 110,165等. 因为有可能一个线程恰好加到53时,此时另一个线程开始执行,并从53开始逐渐的加1,而不是从我们期望的55逐渐加1,这是什么原因呢?这是因为CPU执行上面的10个线程都足够快,这是因为我们只是从1简单的加到10,等cpu时间片还没来得及执行下一个线程时,这个线程已经执行完了,所以看到的线程执行都是有序的,这个结果告诉我们的表现其实是不对的,因为线程的执行是随机的。要验证我们这个说法其实很简单,只需要将加大上面的for循环,加大for循环的执行时间,那么等其中一个线程没有执行完时,另一个线程可能就开始执行了,所以我们可以这样去修改:)
将for (int i = 1; i <= 10; i++) {num += i;}
改为:
for (int i = 1; i <= 1000; i++) {num += i;}, 下面是输出结果,从输出结果可以看出,此时的多个线程的执行不再是有序的,而是随机执行了。在下一篇博文中,我将通过一个例子,让你更加深入的体会到线程的随机执行性。
----- Thread running 1 times ----- Thread running 10 times Thread-0 : 1001000 Object Number: 1 ----- Thread running 8 times ----- Thread running 9 times Thread-9 : 2002000 Object Number: 1 ----- Thread running 7 times ----- Thread running 6 times Thread-5 : 3003000 Object Number: 1 ----- Thread running 5 times ----- Thread running 4 times ----- Thread running 3 times ----- Thread running 2 times Thread-2 : 4504500 Object Number: 1 Thread-3 : 4004000 Object Number: 1 Thread-4 : 3503500 Object Number: 1 Thread-8 : 2502500 Object Number: 1 Thread-7 : 1501500 Object Number: 1 Thread-6 : 1001000 Object Number: 1 Thread-1 : 5005000 Object Number: 1
如果想要得到我们的期望结果,即每个线程的输出结果都是55.怎么办?有几种解决方案:
1. 将Count类中num变成count方法的局部变量:
/** * */ package com.wsheng.thread.simlesafe; /** * 在这个类中的Count方法是计算1一直加到10的和,并输出当前线程名和最后的总和, * 我们期望的结果应该是每一个线程都会输出55 * * @author Wang Sheng(Josh) * */ public class Count2 { // 我们想查看Count类的对象在内存中有几个, 以判断是否有资源共享和竞争 private int objectNum; public Count2(int objectNum) { this.objectNum = objectNum; } public void count() { int num = 0; for (int i = 1; i <= 10; i++) { num += i; } // Object Number一值都是1,说明只有一个Count对象,保证多个线程共享一个Count对象。 System.out.println(Thread.currentThread().getName() + " : " + num + " Object Number: " + objectNum); } }
2. 将线程类成员变量拿到run方法中;
/** * */ package com.wsheng.thread.simlesafe; /** * @author Wang Sheng(Josh) * */ public class ThreadTest3 { public static void main(String[] args) { Runnable run = new Runnable() { int i = 1, j = 1; public void run() { Count count = new Count(i++); System.out.println(" ----- Thread running " + j++ + " times"); count.count(); } }; for (int i = 0; i < 10; i++) { new Thread(run).start(); } } }
很明显,这个方法会构造10个Count对象。
3. 每次启动一个线程使用不同的线程类,不推荐。
上述测试,我们发现,存在成员变量的类用于多线程时是不安全的,而变量定义在方法内是线程安全的。想想在使用struts1时,不推荐创建成员变量,因为 action是单例的,如果创建了成员变量,就会存在线程不安全的隐患,而struts2是每一次请求都会创建一个action,就不用考虑线程安全的问 题。
相关推荐
【Java多线程-创建多线程的基本方式一:继承Thread类】 在Java编程中,多线程是一种并发执行任务的机制,它允许多个任务在同一时间运行,从而提高程序的效率和响应速度。Java提供了多种创建多线程的方式,其中最...
### Java多线程-JDK5.0新增线程创建方式 #### 一、新增方式1:实现Callable接口 ##### (1)介绍 自Java 5.0起,为提高线程管理的灵活性与效率,引入了`Callable`接口,这是一种全新的创建线程的方式。与传统的`...
本教程《Java多线程编程核心技术》将深入探讨这一主题。 一、线程的创建与启动 1. 继承Thread类:创建一个新的类,该类继承自Thread类,然后重写run()方法,最后创建该类的实例并调用start()方法启动线程。 2. 实现...
为了提高效率,可以采用多线程或者非阻塞I/O(如NIO,Java的新I/O库)来改进。但是,对于初学者来说,理解单线程阻塞模型是学习网络编程的基础,有助于深入理解Socket通信的工作原理。 此外,源码分析可以帮助我们...
本示例中的“生产者-消费者”模型是一种经典的多线程问题,它模拟了实际生产环境中的资源分配与消耗过程。下面我们将详细探讨如何在Java中实现这个模型。 首先,我们要理解生产者-消费者模型的基本概念。在这个模型...
总的来说,通过Java多线程和队列数据结构,我们可以有效地模拟排队叫号系统。这种模拟有助于理解和实践并发编程,同时也为我们解决实际问题提供了思路。在这个过程中,我们学习了线程同步、队列操作以及如何在Java中...
### 最好的Java多线程电子书 #### 一、并发与多任务 - **并发**:指在同一时间段内,系统能够处理多个任务的能力。在计算机领域中,这意味着多个任务看起来像是同时进行的,但实际上可能是通过快速切换的方式实现...
Java多线程是Java编程中的一个核心概念,它允许程序同时执行多个独立的任务,从而提高应用程序的效率和响应性。在Java中,多线程主要通过两种方式实现:继承Thread类和实现Runnable接口。这份"JAVA多线程的PPT和示例...
java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针时钟 和多线程服务器java 实现绘制指针...
Java多线程断点续传文件下载技术就是一种能够显著提高下载速度和稳定性的方法。本文将深入解析Java多线程下载文件的关键技术和实现细节。 #### 一、Java多线程下载原理 多线程下载的核心思想是将一个大文件分割成...
根据给定文件的信息,本篇文档是关于Java多线程同步技术在简易模拟售票系统...通过一个简易的售票系统案例,本文档展示了如何将理论知识应用于实际问题的解决中,对于学习和理解Java多线程同步技术具有重要的参考价值。
### 深入浅出Java多线程.pdf #### 知识点概览 本PDF文档涉及了Java多线程的全面介绍,分为基础篇、原理篇和JDK工具篇三个部分,旨在帮助读者深入了解Java多线程的概念、原理及实践应用。 #### 基础篇 **1. 进程...
### Java多线程技术知识点详解 #### 一、实验目的 本实验旨在帮助学习者深入理解Java中的多线程编程技巧。具体目标包括: 1. **掌握Java中的多线程编程**:熟悉如何在Java中利用多线程来提高程序性能和响应能力。...
生产者-消费者模式是一种经典的多线程设计模式,用于解决数据共享问题,尤其是在一个线程生产数据而另一个线程消费数据的情况下。在这个模式中,生产者负责生成数据并放入共享的数据结构(如队列),而消费者则从这...
以上就是本文的主要知识点,涉及到JAVA中的多线程编程、线程的创建与管理、集合的分割操作以及简单的面向对象设计。对于需要学习JAVA并发编程和网络编程的开发者来说,本文提供了一个很好的实例来加深理解,并且可以...
一、Java多线程基础 1. **Thread类与Runnable接口** Java中实现多线程有两种方式:继承Thread类或实现Runnable接口。在本项目中,更推荐使用Runnable接口,因为它可以避免单继承的限制,提高代码的可复用性。创建一...
这是一件好事,因为如果没有线程,那么除了最简单的applet之外,几乎不可能编写出任何程序。如果你想使用Java,就必须学习线程。 本书的新版本展示了如何利用Java线程工具的全部优势,并介绍了JDK 2线程接口中的...