java程序代码线程安全,java中哪些线程是安全的
北大青鸟java培训:关于线程安全问题分析?
在学习java编程开发语言的过程中,我们掌握了线程与线程池等相关技术知识。
创新互联自2013年创立以来,先为繁昌等服务建站,繁昌等地企业,进行企业商务咨询服务。为繁昌企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
今天,北大青鸟河南计算机学院就关于线程安全问题给大家做一个简单的说明和介绍,一起来了解一下吧。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。
不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
什么时候考虑到线程安全:一个对象是否需要线程安全,取决于该对象是否被多线程访问。
这指的是程序中访问对象的方式,而不是对象要实现的功能。
要使得对象是线程安全的,要采用同步机制来协同对对象可变状态的访问。
Java常用的同步机制是Synchronized,还包括volatile类型的变量,显示锁以及原子变量。
在多个线程中,当它们同时访问同个类时,每次执行的结果和单线程结果一致,且变量值跟预期一致,这个类则是线程安全的。
锁的特性锁机制的两种特性:互斥性:即同一时间只允许一个线程持有某个对象的锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。
互斥性我们也往往称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的,否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
挂起、休眠、阻塞和非阻塞挂起:当线程被挂起时,其会失去CPU的使用时间,直到被其他线程(用户线程或调试线程)唤醒。
休眠:同样是会失去CPU的使用时间,但是在过了指定的休眠时间之后,它会自动激活,无需唤醒(整个唤醒表面看是自动的,但实际上也得有守护线程去唤醒,只是不需编程者手动干预)。
阻塞:在线程执行时,所需要的资源不能得到,则线程被挂起,直到满足可操作的条件。
非阻塞:在线程执行时,所需要的资源不能得到,则线程不是被挂起等待,而是继续执行其余事情,等待条件满足了后,收到了通知(同样是守护线程去做)再执行。
Java中所说的线程安全是指什么?
关于线程安全,是指当多个线程访问同一个变量时,该变量不会因为多线程访问产生意想不到的问题,为了避免多线程访问的不可预知的问题,对于程序中多线程能访问到的变量要加锁,即加synchronized,放在同步块中,或者对改变该变量值的方法加synchronized限制。当然jdk中自带的一些类本身就实现了该机制,本身就是线程安全的,比如StringBuffer,Vector等。
java 程序中怎么保证多线程的运行安全?
2.1.读一致性
Java 中针对上述“读不安全”的问题提供了关键字 volatile 来解决问题,被 volatile 修饰的成员变量,在内容发生更改的时候,会通知所有线程去主内存更新最新的值,这样就解决了读不安全的问题,实现了读一致性。
但是,读一致性是无法解决写一致性的,虽然能够使得每个线程都能及时获取到最新的值,但是1.1中的写一致性问题还是会存在。
既然如此,Java 为啥还要提供 volatile 关键字呢?这并非多余的存在,在某些场景下只需要读一致性的话,这个关键字就能够满足需求而且性能相对还不错,因为其他的能够保证“读写”都一直的办法,多多少少存在一些牺牲。
2.2.写一致性
Java 提供了三种方式来保证读写一致性,分别是互斥锁、自旋锁、线程隔离。
2.2.1.互斥锁
互斥锁只是一个锁概念,在其他场景也叫做独占锁、悲观锁等,其实就是一个意思。它是指线程之间是互斥的,某一个线程获取了某个资源的锁,那么其他线程就只能睡眠等待。
在 Java 中互斥锁的实现一般叫做同步线程锁,关键字 synchronized,它锁住的范围是它修饰的作用域,锁住的对象是:当前对象(对象锁)或类的全部对象(类锁)——锁释放前,其他线程必将阻塞,保证锁住范围内的操作是原子性的,而且读取的数据不存在一致性问题。
对象锁:当它修饰方法、代码块时,将会锁住当前对象
类锁:修饰类、静态方法时,则是锁住类的所有对象
注意: 锁住的永远是对象,锁住的范围永远是 synchronized 关键字后面的花括号划定的代码域。
2.2.2.自旋锁
自旋锁也只是一个锁概念,在其他场景也叫做乐观锁等。
自旋锁本质上是不加锁,而是通过对比旧数据来决定是否更新:
如何创建线程?如何保证线程安全?
线程安全等级
之前的博客中已有所提及“线程安全”问题,一般我们常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个“非黑即白”单项选择题。按照“线程安全”的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1、不可变
在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。
2、绝对线程安全
绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。
3、相对线程安全
相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。
它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在java语言中,大部分的线程安全类都属于相对线程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保证的集合。
4、线程兼容
线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
5、线程对立
线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。
一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因此如此,这两个方法已经被废弃啦。
二、线程安全的实现方法
保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
1、互斥同步
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
2、非阻塞同步
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
CAS缺点:
ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3、无需同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1)可重入代码
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
2)线程本地存储
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。
Java中如何保证线程安全性
并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务。
而Java并发则由多线程实现的。
在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中。(当然这是比较笼统的说法,线程之间是可以交互的,他们也不一定是串行。)
多线程的存在就是压榨cpu,提高程序性能,还能减少一定的设计复杂度(用现实的时间思维设计程序)。
这么说来似乎线程就是传说中的银弹了,可事实告诉我们真正的银弹并不存在。
多线程会引出很多难以避免的问题, 如死锁,脏数据,线程管理的额外开销,等等。更大大增加了程序设计的复杂度。
但他的优点依旧不可替代。
死锁和脏数据就是典型的线程安全问题。
简单来说,线程安全就是: 在多线程环境中,能永远保证程序的正确性。
只有存在共享数据时才需要考虑线程安全问题。
java内存区域:
其中, 方法区和堆就是主要的线程共享区域。那么就是说共享对象只可能是类的属性域或静态域。
了解了线程安全问题的一些基本概念后, 我们就来说说如何解决线程安全问题。我们来从一个简单的servlet示例来分析:
public class ReqCounterServlet extends HttpServlet{ private int count = 0;
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
count++;
System.out.print("当前已达到的请求数为" + count);
}
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException { // ignore }
}
1. 了解业务场景的线程模型
这里的线程模型指的是: 在该业务场景下, 可能出现的线程调用实况。
众所周知,Servlet是被设计为单实例,在请求进入tomcat后,由Connector建立连接,再讲请求分发给内部线程池中的Processor,
此时Servlet就处于一个多线程环境。即如果存在几个请求同时访问某个servlet,就可能会有几个线程同时访问该servlet对象。如图:
线程模型,如果简单的话,就在脑海模拟一下就好了,复杂的话就可以用纸笔或其他工具画出来。
2. 找出共享对象
这里的共享对象就很明显就是ReqCounterServlet。
3. 分析共享对象的不变性条件
不变性条件,这个名词是在契约式编程的概念中的。不变性条件保证类的状态在任何功能被执行后都保持在一个可接受的状态。
这里可以引申出, 不可变对象是线程安全的。(因为不可变对象就没有不变性条件)
不变性条件则主要由对可变状态的修改与访问构成。
这里的servlet很简单, 不变性条件大致可以归纳为: 每次请求进入时count计数必须加一,且计数必须正确。
在复杂的业务中, 类的不变性条件往往很难考虑周全。设计的世界是险恶的,只能小心谨慎,用测量去证明,最大程度地减少错误出现的几率。
4. 用特定的策略解决线程安全问题。
如何解决的确是该流程的重点。目前分三种方式解决:
第一种,修改线程模型。即不在线程之间共享该状态变量。一般这个改动比较大,需要量力而行。
第二种,将对象变为不可变对象。有时候实现不了。
第三种,就比较通用了,在访问状态变量时使用同步。 synchronized和Lock都可以实现同步。简单点说,就是在你修改或访问可变状态时加锁,独占对象,让其他线程进不来。
这也算是一种线程隔离的办法。(这种方式也有不少缺点,比如说死锁,性能问题等等)
其实有一种更好的办法,就是设计线程安全类。《代码大全》就有提过,问题解决得越早,花费的代价就越小。
是的,在设计时,就考虑线程安全问题会容易的多。
首先考虑该类是否会存在于多线程环境。如果不是,则不考虑线程安全。
然后考虑该类是否能设计为不可变对象,或者事实不可变对象。如果是,则不考虑线程安全
最后,根据流程来设计线程安全类。
设计线程安全类流程:
1、找出构成对象状态的所有变量。
2、找出约束状态变量的不变性条件。
3、建立对象状态的并发访问管理策略。
有两种常用的并发访问管理策略:
1、java监视器模式。 一直使用某一对象的锁来保护某状态。
2、线程安全委托。 将类的线程安全性委托给某个或多个线程安全的状态变量。(注意多个时,这些变量必须是彼此独立,且不存在相关联的不变性条件。)
Java开发中线程的安全问题以及产生的原因?
Java如何保证原子性常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。
与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。
当前文章:java程序代码线程安全,java中哪些线程是安全的
分享地址:http://cdiso.cn/article/pheghs.html