并发编程初探-线程安全性

  在Java并发编程中,对于线程安全是非常重要的,也是必须要考虑的一个问题.可以这么说,只要涉及到网络的,都必须考虑线程安全问题.好了,开始噼里啪啦地开始敲代码之前,我觉得有必要了解一些文绉绉的理论知识,因为这些理论知识是我们敲出来的代码是否是线程安全的一个依据.  

  当多个线程访问某个状态变量并且其中有一个线程执行写入操作的时候,必须考虑采用同步机制来协同这些线程对变量的访问,Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但"同步"这个术语还包括类型的变量,显式锁(Explicit)以及原子变量.

  如果当多个线程访问同一个可变状态变量时,没有使用合适的同步,那么就会出现错误.有三种方式可以修复这个问题:

  1)不在线程间共享该状态变量

  2)将状态变量修改为不可变的变量

  3)在访问状态变量时使用同步

  当设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助作用.

  当多个线程访问某个类时,不管运行时环境采用何种调用方式,或者这些线程将如何交替执行,并且在主调代码中不需要任务额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的.

  这里的正确性是指某个类的行为与其规范安全一致,也就是说这个类是实现什么功能的就正确的实现了什么功能.

  在线程安全的类中,封装了必要的同步机制,所以客户端无需进一步采取同步措施.

  前面提到了无状态对象,无状态对象既不包含任何域,也不包含任何其他类中域的引用,无状态对象一定是线程安全的.大多数Servlet都是无状态的,从而极大的降低了在实现Servlet线程安全性的复杂性,只有当Servlet在处理请求时需要保存一些信息,线程才会成为一个安全的线程.

  原子性

  当某个计算正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件.所谓的竞态条件就是多个线程执行的顺序不同,那么执行的结果就会有差异,必须是一个正确的执行嗯顺序才会得出正确的结果.

  有时候为了保证原子性,会采用一种措施,就是延迟初始化,将对象初始化操作推迟到实际使用时才进行,同时要确保只被初始化一次.

  java.util.concurren.atomic包中包含了一些原子变量类,用于实现在数值和对象引用撒谎嗯的原子状态转换.

  在实际情况中,应尽可能地使用现有的线程安全对象(例如AcomicLong )来管理类的状态,与非线程安全的对象相比.判断线程安全对象德 可能状态,及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性.

  加锁机制

  要保持状态的一致性,就需要在单个原子操作中更新所相关的状态变量.那么如果是多个原子操作组成一个原子操作呢?那就用到加锁机制了.

  1.内置锁

  同步代码块:包括两个部分,一个作为锁的对象引用,一个最为由这个锁保护的对象的代码块.  

synchronized (lock){
     //访问或修改由锁保护的共享状态
}    

  每个Java对象都可以用做一个实现同步锁。这些锁称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)

  线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是听过控制路径退出,还是通过同步代码块中执行抛出异常退出。获得内置锁的唯一途径就是进入这个由锁保护的同步代码块或方法。

  Java内置锁相当于一种互斥体(或互斥锁),这就意味着最多只有一个线程能持有这种锁。

  由于每次只能有一个线程执行内置锁的代码块,因此, 由这个锁保护的同步代码块会议原子方式执行,多个线在执行该代码块时也不会相互 干扰,并发环境中的原子性与事务应用程序有着相同的含义,一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。

  2.重入

  “重入”获取锁的操作的粒度是“线程”,而不是“调用”。

  重入是一种实现方法是为每一个锁关联一个获取计数值和一个所有者线程,当计数值为0时,这个锁就被认为是没有任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且获取计数值置为1.如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减,计数值为0时,这个锁将被释放。

  重入进一步提升了加锁行为的封装性,因此,简化了面向并发代码的开发

  用锁来保护状态

  对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量由这个锁保护。

  对象的内置锁与其状态之间没有内在的关联,之所以每个对象都有一个内置锁,只是为了免去显式创建对象。

  一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进同步,使得在该对象上不会发生并发访问。

  别费所有数据都需要锁的保护,只有被多个线程同步访问的可变数据才需要通过所来保护。

  对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁保护。

  活跃性与性能

  要确保同步代码块不要过小,并且不要将本应该是原子的操作拆分到多个同步代码块中,应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中其他线程可以访问共享状态。

  当执行时间较长的计算或者可能无法快速完成的操作(如网络I/O或控制台I/O),一定不要只有锁。

时间: 2024-08-10 23:28:29

并发编程初探-线程安全性的相关文章

并发编程之线程安全性

一.什么是线程安全性 并发编程中要编写线程安全的代码,则必须对可变的共享状态的访问操作进行管理. 对象的状态就是存储在实例或者静态变量中的数据,同时其状态也包含其关联对象的字段,比如字典集合既包含自己的状态, 也包含KeyValuePair. 共享即可以多个线程同时访问变量,可变即变量在其声明周期内可以发生变化. 代码线程安全性关注的是防止对数据进行不可控的并发访问. 是否以多线程的方式访问对象,决定了此对象是否需要线程安全性.线程安全性强调的是对对象的访问方式,而不是对象 要实现的功能.要实现

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

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

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

68:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析及其在Spark中的应用源码解析

今天给大家带来的是王家林老师的scala编程讲座的第68讲:Scala并发编程原生线程Actor.Cass Class下的消息传递和偏函数实战解析 昨天讲了Actor的匿名Actor及消息传递,那么我们今天来看一下原生线程Actor及CassClass下的消息传递,让我们从代码出发: case class Person(name:String,age:Int)//定义cass Class class HelloActor extends Actor{//预定义一个Actor  def act()

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

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

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并发编程:线程池的使用(转)

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