Java并发(理论知识)—— 线程安全性

1、什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
      在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步错失。

2、原子性

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。当多个线程访问某个状态变量,并且其中有一个线程执行写入操作时,必须采用同步机制来协调这些线程对变量的访问。无状态对象一定是线程安全的。

如果我们在无状态的对象中增加一个状态时,会出现什么情况呢?假设我们按照以下方式在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,这样就丢失了一次计数。

在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步操作,延迟初始化是竞态条件的常见情形:

public class LazyInitRace {
     private SomeObject instance = null;
     public SomeObject getInstance() {
            if(instance == null)
                 instance = new SomeObject();
            return instance ;
     }
}

  在LazyInitRace中包含竞态条件:首先线程A判断instance为null,然后线程B判断instance也为null,之后线程A和线程B分别创建对象,这样对象就进行了两次初始化,发生错误。

要避免静态条件,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
      在UnsafeCountingFactorizer 例子中,线程不安全的原因是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包中的原子变量类,它能够实现原子的自增操作,这样就是线程安全的了。

3、加锁机制

除了使用原子变量的方式外,我们也可以通过加锁的方式实现线程安全性。还是UnsafeCountingFactorizer,我们只要在它的service方法上增加synchronized关键字,那么它就是线程安全的了:

public class UnsafeCountingFactorizer implements Servlet {
     private long count = 0;

     public long getCount() {
            return count ;
     }

     @Override
     public synchronized void service(ServletRequest arg0, ServletResponse arg1)
                 throws ServletException, IOException {
            // do something
           count++;
     }
}

  在方法上增加synchronized关键字后,它能够保证,同一时间只会有一个线程进入方法体,这样每个线程就可以全部执行完方法后再退出,方法体内操作就相当于是原子操作了,避免了竞态条件错误。

以上代码是线程安全的,但是性能很糟糕,因为我们把整个service都给锁起来了,同一时刻只能一个线程执行service,并发任务变成了串行任务。其实我们本意只是想把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++;
          }
     }
}

我们缩小了锁的范围,这样可以更好的增加并发性。  

4、可见性

每个线程内部都保有共享变量的副本,当一个线程更新了这个共享变量,另一个线程可能看的到,可能看不到,这就是可见性问题,以下面的代码为例:

public class NoVisibility {
     private static boolean ready;
     private static int number;
     public static class ReadThread extends Thread {
            public void run() {
                 while(!ready )
                     Thread. yield();
                System. out.println(number);
           }
     }
     public static void main(String [] args) {
            new ReadThread().start();
            number = 42;
            ready = true ;
     }
}

  以上代码可能输出0或者什么也不能输出。为什么会什么也不能输出呢?因为我们在主线程中把ready置为true,但是ReadThread中却不一定能够读到我们设置的ready值,所以在ReadThread中Thread.yield()将一直执行下去。为什么可能为0呢?如果ReadThread能够读到我们的值,可能先读到ready值为true,还未读取更新number值,ReadThread就把保有的number值输出了,也就是0。

注意,上面的所有内容都是假设,在缺乏同步的情况下,ReadThread和主线程会如何交互,我们是无法预期的,以上两种情况只是两种可能性。那么如何避免这种问题呢?很简单,只要有数据在多个线程之间共享,就使用正确的同步。

4.1、加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式查看另一个线程的执行结果,当线程A进入某同步代码块时,线程B随后进入由同一个锁保护的同步代码块,此时,线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一同步代码块中的所有操作结果,如果没有同步,那么就无法实现上述保证。

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

4.2、volatile变量

volatile是一种比synchronized关键字轻量级的同步机制,volatile关键字可以确保变量的更新操作通知到其他线程。

下面是volatile的典型用法:

     volatile boolean asleep;
     ...
     while(!asleep)
        doSomeThing();

  加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。

5、总结

编写线程安全的代码,其核心在于要对状态访问操作进行管理。编写线程安全的代码时,有两个关注点,一个是原子性问题,一个是可见性问题,要尽量避免竞态条件错误。

时间: 2024-10-09 06:08:00

Java并发(理论知识)—— 线程安全性的相关文章

并发基础知识 — 线程安全性

前段时间看完了<并发编程的艺术>,总感觉自己对于并发缺少一些整体的认识.今天借助<Java并发编程实践>,从一些基本概念开始,重新整理一下自己学过并发编程.从并发基础开始,深入进去,系统学习一下并发编程. 编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问.对象的状态是指存储在状态变量(实例或静态域)中的数据.对象的状态还可能包括其他依赖对象的域.(Map.Entry) 一个对象是否需要时线程安全的,取决于该对象

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

Java并发编程系列[未完]: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) 一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线程创建完成时为新

Java并发学习之五——线程的睡眠和恢复

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.Thread类的sleep方法,可以使线程睡眠.此方法接收一个整数作为参数,表示线程暂停运行的毫秒数.在调用sleep方法后,当时间结束时,JVM会安排他们CPU时间,线程会继续按指令执行. 另一种可能是使用一个有TimeUnit列举元素的sleep方法,使用线程类的sleep方法让当前线程睡眠,但是它接收的参数单位后会转成毫秒的. 2.当你调用sleep()方法,Thread离开CPU并在一段时间内停止运行.在这段时间内,他是不消耗CP

Java并发编程:线程的同步

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发编程:线程的创建

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

【转】Java并发编程:线程池的使用

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool

Java并发基础(六) - 线程池

Java并发基础(六) - 线程池 1. 概述 这里讲一下Java并发编程的线程池的原理及其实现 2. 线程池的基本用法 2.1 线程池的处理流程图 该图来自<Java并发编程的艺术>: 从图中我们可以看出当一个新任务到线程池时,线程池的处理流程如下: 线程池首先判断线程池里面线程数是否达到核心线程数.如果不是则直接创建新线程作为核心线程来执行该任务(该线程作为核心线程不会由于任务的完成而销毁),否则进入下一流程. 判断阻塞队列是否已经满了.如果没满则将该任务放入阻塞队列中,等待核心线程处理,

19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对

Java并发编程:线程池的使用(转)

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool