当线程安全: 多个线程访问某个类时, 不管运行时环境采用何种调度方式或者这些线程将如何交替执行, 并且在主调代码中不需要任何额外的同步或协调, 这个类都能表现出正确的行为, 那么久称这个类是线程安全的
在线程安全类中封装了必要的同步机制, 因此客户端无需采取进一步的同步措施
原子性
要么不执行, 要么执行到底原子性就是当某一个线程修改 i 的值的时候, 从取出 i 到将新的 i 的值写给 i 之间不能有其他线程对 i 进行任何操作也就是说保证某个线程对 i 的操作是原子性的, 这样就可以避免数据脏读 通过锁机制或者 CAS(Compare And Set 需要硬件 CPU 的支持)操作可以保证操作的原子性
当多个线程访问某个状态变量, 并且其中有一个线程执行写入操作时, 必须采用同步机制来协调这些线程对变量的访问无状态对象一定是线程安全的
如果我们在无状态的对象中增加一个状态时, 会出现什么情况呢? 假设我们按照以下方式在 servlet 中增加一个 "命中计数器" 来管理请求数量: 在 servlet 中增加一个 long 类型的域, 每处理一个请求就在这个值上加 1
- public class UnsafeCountingFactorizer implements Servlet {
- private long count = 0;
- public long getCount() {
- return count ;
- }
- @Override
- public void service(ServletRequest arg0, ServletResponse arg1)
- throws ServletException, IOException {
- // do something
- count++;
- }
- }
不幸的是, 以上代码不是线程安全的, 因为 count++ 并非是原子操作, 实际上, 它包含了三个独立的操作: 读取 count 的值, 将值加 1, 然后将计算结果写入 count 如果线程 A 读到 count 为 10, 马上线程 B 读到 count 也为 10, 线程 A 加 1 写入后为 11, 线程 B 由于已经读过 count 值为 10, 执行加 1 写入后依然为 11, 这样就丢失了一次计数
在 count++ 例子中线程不安全是因为 count++ 并非原子操作, 我们可以使用原子类, 确保确保操作是原子, 这样这个类就是线程安全的了
- public class CountingFactorizer implements Servlet {
- private final AtomicLong count = new AtomicLong(0);
- public long getCount() {
- return count .get() ;
- }
- @Override
- public void service(ServletRequest arg0, ServletResponse arg1)
- throws ServletException, IOException {
- // do something
- count.incrementAndGet();
- }
- }
AtomicLong 是 java.util.concurrent.atomic 包中的原子变量类, 它能够实现原子的自增操作, 这样就是线程安全的了 同样, 上述情况还会出现在 单例模式的懒加载过程中, 当多个线程同时访问 getInstance()函数时这篇文章中有讲解: 实现优雅的单例模式
加锁机制
线程在执行被 synchronized 修饰的代码块时, 首先检查是否有其他线程持有该锁, 如果有则阻塞等待, 如果没有则持有该锁, 并在执行完之后释放该锁
除了使用原子变量的方式外, 我们也可以通过加锁的方式实现线程安全性还是 UnsafeCountingFactorizer, 我们只要在它的 service 方法上增加 synchronized 关键字, 那么它就是线程安全的了当然在整个方法中加锁在这里是效率很低的, 因为我们只需要保证 count++ 操作的原子性, 所以这里只对 count++ 进行了加锁, 代码如下:
- public class UnsafeCountingFactorizer implements Servlet {
- private long count = 0;
- public long getCount() {
- return count ;
- }
- @Override
- public void service(ServletRequest arg0, ServletResponse arg1)
- throws ServletException, IOException {
- // do something
- synchronized(this){
- count++;
- }
- }
- }
Synchronized 代码块使得一段程序的执行具有 原子性, 即每个时刻只能有一个线程持有这个代码块, 多个线程执行在执行时会互不干扰
java 内存模型及 可见性
的内存模型没有上面这么简单, 在 Java Memory Model 中, Memory 分为两类, main memory 和 working memory,main memory 为所有线程共享, working memory 中存放的是线程所需要的变量的拷贝 (线程要对 main memory 中的内容进行操作的话, 首先需要拷贝到自己的 working memory, 一般为了速度, working memory 一般是在 cpu 的 cache 中的) 被 volatile 修饰的变量在被操作的时候不会产生 working memory 的拷贝, 而是直接操作 main memory, 当然 volatile 虽然解决了变量的可见性问题, 但没有解决变量操作的原子性的问题, 这个还需要 synchronized 或者 CAS 相关操作配合进行
每个线程内部都保有共享变量的副本, 当一个线程更新了这个共享变量, 另一个线程可能看的到, 可能看不到, 这就是可见性问题
下面这段代码中 main 线程中 改变了 ready 的值, 当开启多个子线程时, 子线程的值并不是马上就刷新为最新的 ready 的值(这里的中间刷新的时间间隔到底是多长, 或者子线程的刷新机制, 自己也不太清楚当开启一个线程去执行时, ready 值改变时就会立刻刷新, 循环立刻就结束, 但是当开启多个线程时, 就会有一定的延迟)
- public class SelfTest {
- private static boolean ready;
- private static int number;
- private static long time;
- public static class ReadThread extends Thread {
- public void run() {
- while(!ready ){
- System. out.println("*******"+Thread.currentThread()+""+number);
- Thread. yield();
- }
- System. out.println(number+"currentThread:"+Thread.currentThread());
- }
- }
- public static void main(String [] args) {
- time = System.currentTimeMillis();
- new ReadThread().start();
- new ReadThread().start();
- new ReadThread().start();
- new ReadThread().start();
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- number = 42;
- ready = true ;
- System.out.println("赋值时间: ready = true");
- }
- }
上面这段代码的执行结果: 可以看出赋值后, 循环还是执行了几次
此时如果把 ready 的属性加上 volatile 结果便是如下的效果:
由此可见 Volatile 可以解决内存可见性的问题
上面讲的加锁机制同样可以解决内存可见性的问题, 加锁的含义不仅仅局限于互斥行为, 还包括内存可见性为了确保所有线程都能看到共享变量的最新值, 所有执行读操作或者写操作的线程都必须在同一个锁上同步
注: 由于 System.out.println 的执行仍然需要时间, 所以这面打印的顺序还是可能出现错乱
参考:
http://www.mamicode.com/info-detail-245652.html
并发编程实战
http://www.cnblogs.com/NeilZhang/p/7979629.html
来源: https://www.cnblogs.com/NeilZhang/p/8682266.html