Java并发编程规则:构建线程安全的共享对象

构建线程安全的共享对象,使其在多线程环境下能够提供安全的访问。编写正确的并发程序关键在于控制共享、可变的状态进行访问管理。synchornized关键字既可以阻塞程序,也可以维护操作的原子性,它是一个线程安全与非线程安全的临界区标识,通过它我们可以控制对象的内存可见性。不得不提到volatile,volatile仅仅是控制可见性,而同步性却不及synchornized。

多线程访问共享变量的诡异结果:

package net.jcip.examples;

/**
 * NoVisibility
 * <p/>
 * Sharing variables without synchronization
 *
 * @author Brian Goetz and Tim Peierls
 */

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

static的变量意味着,在内存中它是一个公用变量;在多线程中,这里输出的结果是难于预估的,可能是42也可能是0,甚至线程不停止;因为这里没用使用任何的同步机制加以控制。 处理方法是:需要跨线程的变量,需要加入恰当的同步。

解决多线程同步的可见性问题:

锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入必须使用公共的锁进行同步。根据上述多线程诡异问题,下面来加入适当的同步:

package net.jcip.examples;

/**
 * NoVisibility
 * <p/>
 * Sharing variables without synchronization
 *
 * @author Brian Goetz and Tim Peierls
 */

public class NoVisibility {
	private static boolean ready;
	private static int number;

	private static class ReaderThread extends Thread {
		public void run() {
			synchronized (NoVisibility.class) {
				while (!ready)
					Thread.yield();

				System.out.println(this.getName() + number);
			}
		}
	}

	public static void resetValue(boolean ready, int number) {
		synchronized (NoVisibility.class) {
			NoVisibility.number = number;
			NoVisibility.ready = ready;
		}
	}

	public static void main(String[] args) {
		for (int i = 0; i < 200; i++) {
			Thread thread = new ReaderThread();
			thread.setName("THREAD:"+i + "---");
			thread.start();
			NoVisibility.resetValue(true, i);
		}
	}
}

最简单的改法是将变量的可见性改了,使用volatile关键字(这里不推荐,请看代码后面的注意):

package net.jcip.examples;

/**
 * NoVisibility
 * <p/>
 * Sharing variables without synchronization
 *
 * @author Brian Goetz and Tim Peierls
 */

public class NoVisibility {
	private static volatile boolean ready;
	private static volatile int number;

	private static class ReaderThread extends Thread {
		public void run() {
			while (!ready)
				Thread.yield();
			System.out.println(this.getName() + number);
		}
	}

	public static void resetValue(boolean ready, int number) {
		NoVisibility.number = number;
		NoVisibility.ready = ready;
	}

	public static void main(String[] args) {
		for (int i = 0; i < 200; i++) {
			Thread thread = new ReaderThread();
			thread.setName("THREAD:"+i + "---");
			thread.start();
			NoVisibility.resetValue(true, i);
		}
	}
}

注意:只有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应当避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用对象的状态的可见性,或者用于标识重要的生命周期事件的发生(如,状态:启用、关闭)。加锁可以保证可见性和原子性,volatile只能保证可见性。

Volatile变量使用的条件:

1.写入变量不依赖于当前值;或者确保只有单一线程在修改该值;

2、变量不需要与其他状态参与不变约束;

3、访问变量时,没有其他原因需要加锁;

多线程访问造成过期数据问题:

当一个线程在没有同步的情况下读取变量,它可能会得到一个过期值。还是上面那段代码中的例子。在多线程中,可能ready在其它线程中的值还未被修改。所以很有可能ready=false;如果在多线程中处理业务,那么变量的状态将是数据出错的致命元凶。有时候数据不能快速地实现同步并不伤大雅,但是是以牺牲体验效果为代价的。如果我们多个指令在更新设备的GPS位置,可能偏差不是很大,但是效果不佳是显而易见的。来看个例子:

package net.jcip.examples;

import net.jcip.annotations.*;

/**
 * MutableInteger
 * <p/>
 * Non-thread-safe mutable integer holder
 *
 * @author Brian Goetz and Tim Peierls
 */

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}
将其修改为线程安全类的做法,为getter、setter加synchronized同步关键字:

package net.jcip.examples;

import net.jcip.annotations.*;

/**
 * SynchronizedInteger
 * <p/>
 * Thread-safe mutable integer holder
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class SynchronizedInteger {
    @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value) {
        this.value = value;
    }
}
注:getter也需要同步,否则不能保证其他线程读取到的value是最新的。

安全地发布对象:

发布对象 实际上就是扩大对象提供访问范围,如public的属性和方法。安全发布对象的目的是:维护对象的封装性和不可变性,防止不正当地使用对象。下面来看一个对象发布示例:

package net.jcip.examples;

import java.util.*;

/**
 * Secrets
 *
 * Publishing an object
 *
 * @author Brian Goetz and Tim Peierls
 */
class Secrets {
    public static Set<Secret> knownSecrets;

    public void initialize() {
        knownSecrets = new HashSet<Secret>();
    }
}


class Secret {
}

最常见的发布对象就是将对象存储到静态变量中提供访问。上面initialize方法实例化存储对象到knownSecrets的引用,将Secret对象添加到knownSecrets就是一个发布对象的操作。 发布对象,可以说就是提供对象对外访问的入口。它可以是一个私有属性提供共有方法的访问,这也实现了对象的发布。但是,下面的示例不推荐大家这样写:

package net.jcip.examples;

/**
 * UnsafeStates
 * <p/>
 * Allowing internal mutable state to escape
 *
 * @author Brian Goetz and Tim Peierls
 */
class UnsafeStates {
    private String[] states = new String[]{
        "AK", "AL" /*...*/
    };

    public String[] getStates() {
        return states;
    }
}
虽然我们可以获取到状态名称,但是我们不知道对于使用getStates()方法的线程将如何使用它,这是一个线程引用的安全问题。所以, 封装使得程序的正确性分析更可行,而且不容易偶然地破坏设计约束。

将对象封装好了再发布:

使用一个未完全封装的对象,就破坏了程序的封装性。不要让this构造在构造期间逸出。来看下面一个例子:

package net.jcip.examples;

/**
 * ThisEscape
 * <p/>
 * Implicitly allowing the this reference to escape
 *
 * @author Brian Goetz and Tim Peierls
 */
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}


此处的构造函数存在方法的重写,source.registerListener的实现就是不安全的对象发布,因为在发布期间调用了registerListener方法。

修正方法:使用工厂方法防止this引用在构造期间逸出。示例如下:

package net.jcip.examples;

/**
 * SafeListener
 * <p/>
 * Using a factory method to prevent the this reference from escaping during construction
 *
 * @author Brian Goetz and Tim Peierls
 */
public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

只要在初始化期间不要进行业务处理就可以实现线程安全了,即通过其他方法来启动线程的调用。

安全地共享对象:

在并发程序中,使用和共享对象的一些最有效的策略如下:

-------------线程限制:--------------

一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改。

-------------共享只读(read-only):-------------

一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发地访问,但任何线程都不能修改它。共享只读对象包括可变对象与高效不可变对象。

-------------共享线程安全(thread-safe):-------------

一个线程安全的对象在内部进行同步,所以其它线程无额外同步,就可以通过公共接口随意地访问它。

-------------被守护(Guarded):-------------

一个被守护的对象只能通过特定的锁来访问。被守护的对象包括哪些被线程安全对象封装的对象,和已知特定的锁保护起来的已发布对象。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Java并发编程实战》是一本经典的Java并发编程指南,它深入解析了Java中线程、锁、并发集合、并发工具等关键概念和技术,并提供了各种实用的实战技巧和例子。 该书包含以下主要章节:引言、线程安全性、对象共享对象的构造与发布、基础构建模块、任务执行、取消与关闭、线程池、同步器、构建自定义的同步工具、性能与可伸缩性、显式锁、构建锁的高级特性、原子变量与非阻塞同步、隐式的线程局部变量、构建自定义的同步器、构建一个框架。 这本书的特点是理论结合实践,通过大量的实例和案例演示了各种并发编程问题的解决方案,以及如何正确地使用Java提供的并发工具和类库。在阅读过程中,读者可以逐步了解并掌握线程安全、锁、线程池等核心概念,从而能够更好地应对多线程编程中的各种挑战。 该书的目标读者主要面向有一定Java编程基础的开发人员和架构师,但也适合初学者通过系统学习并发编程知识。无论是学习Java并发编程的基础知识,还是深入研究Java并发编程的高级特性和技术,这本书都是不可或缺的参考书籍之一。 总之,通过阅读《Java并发编程实战》,读者可以系统地学习和掌握Java并发编程的基础和高级知识,提升自己在多线程编程方面的能力,从而编写出更高效、更可靠、更安全的并发程序。 ### 回答2: 《Java并发编程实战》是一本非常经典的Java并发编程书籍,由Brian Goetz等人合著。这本书以清晰易懂的方式介绍了Java并发编程的概念、原理和实践技巧,对于Java开发者来说,是学习和掌握多线程编程不可或缺的参考资料。 这本书的目录包括: 第1章:介绍并发编程 第2章:线程安全性 第3章:对象共享 第4章:对象的组合 第5章:构建并发程序 第6章:任务执行 第7章:取消和关闭 第8章:线程池 第9章:构建自定义的同步工具类 第10章:避免活跃性危险 第11章:性能与可伸缩性 第12章:并发程序的测试 第13章:隐式使用锁 第14章:构建高性能并发程序 这本书通过实例、案例和代码来解释并发编程的概念和技巧。它涵盖了从基本概念到高级主题的内容,包括线程安全性、锁、对象共享、线程池、自定义同步工具类等。通过学习这本书,读者可以了解多线程编程的核心概念,学会如何避免并发问题,提高程序的性能和可伸缩性。 《Java并发编程实战》不仅适用于Java初学者,也适合有一定经验的开发人员。它提供了一系列实用的技巧和建议,帮助开发人员编写高质量、高效率的并发程序。无论是想学习并发编程的基本原理,还是深入研究Java并发库中的高级特性,这本书都是一个不可多得的学习资源。 ### 回答3: 《Java并发编程实战》是由美国计算机科学家Brian Goetz等人合著的一本经典的Java并发编程指南。本书全面介绍了Java并发编程的基本概念、原则、机制和设计模式,是学习和理解Java并发编程不可或缺的重要参考书。 本书的目录包括以下主要部分: 第1部分:基础知识 1. 引论:介绍了并发编程的基本概念、挑战和解决方案。 第2部分:结构化并发应用程序 2. 线程安全性:详细介绍了线程安全性的概念、实现原理和验证方法。 3. 对象共享:讲述了在多线程环境下如何安全地共享对象。 4. 对象的组合:介绍了如何使用复合对象来提高并发应用程序的性能和可扩展性。 第3部分:构建并发应用程序 5. 基础构建模块:详细介绍了Java并发编程的基本构建模块,包括线程池、任务和执行者框架。 6. 任务执行:讲述了如何利用任务和执行者框架来实现并发任务的执行。 7. 取消与关闭:介绍了如何取消和关闭并发任务以及处理取消操作的相关问题。 8. 线程池的扩展:讲述了如何扩展Java线程池以满足特定需求。 第4部分:活跃性、性能和测试 9. 阻塞与响应性:详细介绍了如何解决并发编程中的阻塞和响应性问题。 10. 性能与可伸缩性:讲述了如何提高并发应用程序的性能和可伸缩性。 11. 显式锁:详细介绍了显式锁的使用和相关设计模式。 12. 构建自定义的同步工具:介绍了如何构建自定义的同步工具。 第5部分:构建更安全的并发应用程序 13. 基础模块:详细介绍了Java并发编程中的基础模块,包括原子变量、非阻塞同步机制等。 14. 高级主题:讲述了一些高级的并发编程主题,包括显式条件、原子类等。 15. 显式的通知:详细介绍了如何使用显式的通知机制来实现线程间的协作和通信。 《Java并发编程实战》通过系统、全面的内容和丰富的示例展示了如何正确地并发编程。无论是初学者还是有一定经验的开发人员,都可从中获得宝贵的知识和实战经验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值