Java的线程编程非常简单。但有时会看到一些关于线程的错误用法。下面列出一些应该注意的问题。
1.同步对象的恒定性
All java objects are references.
对于局部变量和参数来说,java里面的int, float, double, boolean等基本数据类型,都在栈上。这些基本类型是无法同步的;java里面的对象(根对象是Object),全都在堆里,指向对象的reference在栈上。
java中的同步对象,实际上是对于reference所指的“对象地址”进行同步。
需要注意的问题是,千万不要对同步对象重新赋值。举个例子。
class A implements Runnable{
Object lock = new Object();
void run(){
for(...){
synchronized(lock){
// do something
...
lock = new Object();
}
}
}
run函数里面的这段同步代码实际上是毫无意义的。因为每一次lock都给重新分配了新的对象的reference,每个线程都在新的reference同步。
大家可能觉得奇怪,怎么会举这么一个例子。因为我见过这样的代码,同步对象在其它的函数里被重新赋了新值。
这种问题很难查出来。
所以,一般应该把同步对象声明为final.
final Object lock = new Object();
使用Singleton Pattern 设计模式来获取同步对象,也是一种很好的选择。
2.如何放置共享数据
实现线程,有两种方法,一种是继承Thread类,一种是实现Runnable接口。
上面举的例子,采用实现Runnable接口的方法。本文推荐这种方法。
首先,把需要共享的数据放在一个实现Runnable接口的类里面,然后,把这个类的实例传给多个Thread的构造方法。这样,新创建的多个Thread,都共同拥有一个Runnable实例,共享同一份数据。
如果采用继承Thread类的方法,就只好使用static静态成员了。如果共享的数据比较多,就需要大量的static静态成员,令程序数据结构混乱,难以扩展。这种情况应该尽量避免。
编写一段多线程代码,处理一个稍微复杂点的问题。两种方法的优劣,一试便知。
3.同步的粒度
线程同步的粒度越小越好,即,线程同步的代码块越小越好。尽量避免用synchronized修饰符来声明方法。尽量使用synchronized(anObject)的方式,如果不想引入新的同步对象,使用synchronized(this)的方式。而且,synchronized代码块越小越好。
4.线程之间的通知
这里使用“通知”这个词,而不用“通信”这个词,是为了避免词义的扩大化。
线程之间的通知,通过Object对象的wait()和notify() 或notifyAll() 方法实现。
下面用一个例子,来说明其工作原理:
假设有两个线程,A和B。共同拥有一个同步对象,lock。
1.首先,线程A通过synchronized(lock) 获得lock同步对象,然后调用lock.wait()函数,放弃lock同步对象,线程A停止运行,进入等待队列。
2.线程B通过synchronized(lock) 获得线程A放弃的lock同步对象,做完一定的处理,然后调用 lock.notify() 或者lock.notifyAll() 通知等待队列里面的线程A。
3.线程A从等待队列里面出来,进入ready队列,等待调度。
4.线程B继续处理,出了synchronized(lock)块之后,放弃lock同步对象。
5.线程A获得lock同步对象,继续运行。
例子代码如下:
public class SharedResource implements Runnable{
Object lock = new Object();
public void run(){
// 获取当前线程的名称。
String threadName = Thread.currentThread().getName();
if( “A”.equals(threadName)){
synchronized(lock){ //线程A通过synchronized(lock) 获得lock同步对象
try{
System.out.println(“ A gives up lock.”);
lock.wait(); // 调用lock.wait()函数,放弃lock同步对象,
// 线程A停止运行,进入等待队列。
}catch(InterruptedException e){
}
// 线程A重新获得lock同步对象之后,继续运行。
System.out.println(“ A got lock again and continue to run.”);
} // end of synchronized(lock)
}
if( “B”.equals(threadName)){
synchronized(lock){//线程B通过synchronized(lock) 获得线程A放弃的lock同步对象
System.out.println(“B got lock.”);
lock.notify(); //通知等待队列里面的线程A,进入ready队列,等待调度。
//线程B继续处理,出了synchronized(lock)块之后,放弃lock同步对象。
System.out.println(“B gives up lock.”);
} // end of synchronized(lock)
boolean hasLock = Thread.holdsLock(lock); // 检查B是否拥有lock同步对象。
System.out.println(“B has lock ? -- ” +hasLock); // false.
}
}
}
public class TestMain{
public static void main(){
Runnable resource = new SharedResource();
Thread A = new Thread(resource,”A”);
A.start();
// 强迫主线程停止运行,以便线程A开始运行。
try {
Thread.sleep(500);
}catch(InterruptedException e){
}
Thread B = new Thread(resource,”B”);
B.start();
}
}
5.跨类的同步对象
对于简单的问题,可以把访问共享资源的同步代码都放在一个类里面。
但是对于复杂的问题,我们需要把问题分为几个部分来处理,需要几个不同的类来处理问题。这时,就需要在不同的类中,共享同步对象。比如,在生产者和消费者之间共享同步对象,在读者和写者之间共享同步对象。
如何在不同的类中,共享同步对象。有几种方法实现,
(1)前面讲过的方法,使用static静态成员,(或者使用Singleton Pattern.)
(2)用参数传递的方法,把同步对象传递给不同的类。
(3)利用字符串常量的“原子性”。
对于第三种方法,这里做一下解释。一般来说,程序代码中的字符串常量经过编译之后,都具有唯一性,即,内存中不会存在两份相同的字符串常量。
(通常情况下,C++,C语言程序编译之后,也具有同样的特性。)
比如,我们有如下代码。
String A = “atom”;
String B = “atom”;
我们有理由认为,A和B指向同一个字符串常量。即,A==B。
注意,声明字符串变量的代码,不符合上面的规则。
String C= new String(“atom”);
String D = new String(“atom”);
这里的C和D的声明是字符串变量的声明,所以,C != D。
有了上述的认识,我们就可以使用字符串常量作为同步对象。
比如我们在不同的类中,使用synchronized(“myLock”), “myLock”.wait(),“myLock”.notify(), 这样的代码,就能够实现不同类之间的线程同步。
本文并不强烈推荐这种用法,只是说明,有这样一种方法存在。
本文推荐第二种方法,(2)用参数传递的方法,把同步对象传递给不同的类。
分享到:
相关推荐
以下是一些Java线程编程中需要注意的关键点: 1. **同步对象的恒定性**: - Java中的对象引用存储在栈中,对象实例存储在堆中。同步是基于对象的引用,而不是对象的值。这意味着,如果同步的对象引用在运行过程中...
### Java Thread用法详解 #### 一、Java线程基础概念与重要性 在Java编程语言中,线程是程序执行的基本单位之一,它能够帮助我们实现多任务处理,提高程序运行效率。Java中的线程主要通过`java.lang.Thread`类来...
Java线程亲和性(Thread Affinity)是一个高级并发编程概念,主要涉及到操作系统调度和硬件资源的优化。在多核处理器系统中,线程亲和性允许开发者指定某个线程应该运行在哪个特定的处理器核心上,从而提高性能、...
提供的两个压缩包文件,`Java Thread Programming (Sams).chm`和`Java Thread Programming (Source).rar`可能包含详细的章节内容和源代码示例,这对于实践和深入理解书中所讲的知识非常有帮助。记得结合实际编程练习...
在多线程环境中,需要注意异常处理,因为异常可能会在一个线程中发生,但需要在另一个线程中被捕获和处理。 --- #### 三、线程的生命期 线程的生命期涵盖了从创建到结束的整个过程。了解线程的生命期对于理解线程...
`readme.zip` 文件通常是提供软件使用说明、安装指南或者授权信息的文档,解压后应该会看到关于如何运行和使用IBM Thread and Monitor Dump Analyzer的具体步骤和注意事项。 `license` 文件则包含该软件的许可协议...
Java线程是多任务编程的重要组成部分,特别是在服务器端应用程序中,它使得程序能够同时执行多个任务,提高了...合理使用线程可以帮助提高程序性能,但也需要注意线程安全和资源管理,避免出现竞态条件和死锁等问题。
### Java-编写跨平台Java程序注意事项 在Java编程领域中,跨平台性是其核心优势之一,这得益于Java“一次编写,到处运行”的设计理念。然而,在实际开发过程中,要实现真正的跨平台应用并非易事,需要注意诸多细节...
在深入探讨Java线程(Java Thread)的基本概念与创建方式之前,我们首先应当明确线程在计算机科学中的地位。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以...
在多线程编程中,需要注意线程安全问题。在这个例子中,因为我们只是简单地打印数值,没有共享变量,所以不存在竞态条件。但如果涉及共享资源,如全局变量,就需要使用同步机制,如`synchronized`关键字或`java.util...
`Thread`类是Java中用于实现线程的基本接口,它位于`java.lang`包中。本实例将深入探讨`Thread`类的使用,这对于初学者掌握多线程编程至关重要。 在Java中,创建线程主要有两种方式:继承`Thread`类和实现`Runnable...
理解这些基本概念对于编写多线程的Java程序至关重要,因为它们可以帮助你有效地管理线程,避免竞态条件、死锁等问题,从而提高程序的并发性能和响应速度。正确地使用`start()`和`run()`方法是实现线程并发的基础。
`javathread.part02.rar`这个压缩包可能包含的是关于Java线程深入理解和实践的材料,可能是代码示例、课件或教程。 Java线程的创建主要有两种方式:通过实现Runnable接口和继承Thread类。实现Runnable接口更为灵活...
首先,Java中的线程是通过`java.lang.Thread`类来表示的,这个类继承自`java.lang.Object`并实现了`Runnable`接口。`Runnable`接口只有一个抽象方法`run()`,它是线程执行的主要逻辑。创建线程主要有两种方式:一是...
### Java面试问题集锦 #### 一、运行程序之前: ##### Path与classpath图解 - **Path**:指的是操作系统环境变量中的PATH,它定义了系统查找可执行文件的路径列表。当用户在命令行中输入一个命令时,系统会在PATH...
Java.util.concurrent包提供了丰富的并发工具类,如Semaphore(信号量)、CyclicBarrier(回环栅栏)、CountDownLatch(倒计时 latch)和FutureTask等,帮助开发者更好地处理多线程问题。 九、线程安全的集合类 ...
Java提供了线程的中断机制,但需要注意的是,阻塞状态的线程无法检测中断状态。 线程具有属性,如线程优先级,用于决定哪个线程应优先执行。默认情况下,新创建的线程会继承其父线程的优先级。守护线程是一种特殊...
然而,开发者可以通过设置线程的优先级来影响调度,但需要注意,过度依赖线程优先级可能会导致调度问题。 工作队列(Work Queue)是线程池的核心组件之一,它负责存储待执行的任务。常见的工作队列类型有无界队列...