Java多线程线程安全

来源:互联网 发布:linux 建c文件 编辑:程序博客网 时间:2024/05/01 21:37

一、什么是线程安全

我们所说的线程安全的话题都是基于一个变量会被多个线程访问的这样一个前提下,如果只是单线程应用自然就不会出现这种问题。

一个变量被多个线程访问我们称这个变量是共享的。而一个变量在其生命周期中可以被修改,则称这个变量时可变的。网络上有很多人试图给线程安全下定义,林林总总反正很多,但是归根到底线程安全的核心点就是正确性。试想下,多个线程访问某个共享的可变的变量的时候,其目的就是为了得到一个正确的数据而已。


这里给出我对线程安全的理解:一个对象被多个线程访问的情况下,如果各个线程对该对象的读操作都能得到正确的值,以及对该对象的写操作都能安全的写入(不会覆盖之前写入的数据以及写入的数据符合该类的规约),简而言之就是这个类的行为都是正确的则这个类就是线程安全的。线程安全的类客户可以在任何情况下拿来使用,当然如果是单线程的话最好不要使用,因为线程安全的类里面都封装了必要的同步,而同步设计到锁,锁的开销是比较大的,单线程下也许会影响其性能。


不知道大家有没有听说过无状态的类,无状态的类值得是类里面没有成员变量,也没有引用其他变量,或者有成员变量但是不可变的、或者成员变量是单例的、比如struts1的action、可以是单例的、因为他是没有状态的。无状态的类里面的值(可能通过计算,也可能当参数传入)会唯一的存在本地变量中,这些本地变量存储在线程的栈中,只有执行线程才能访问。

public class StatelessServlet implements Servlet{    public void service(ServletRequest req,ServletResponse resp){        BigInteger i=extractFromRequest(req);        BigInteger[] factors=factor(i);        encodeIntoResponse(resp,factors);    }}

如上代码中的i是从request中得到的,factors又是通过计算得到的,这个servlet是无状态的。我们可以得到一个结论,无状态的类都是线程安全的

二、原子性

我们修改上面的代码,加上一个计数变量

public class StatelessServlet implements Servlet{    private long count=0;    public void service(ServletRequest req,ServletResponse resp){        BigInteger i=extractFromRequest(req);        BigInteger[] factors=factor(i);        count++;        encodeIntoResponse(resp,factors);    }}

这个代码咋一看似乎没什么问题,但是这里的count++的自增操作并不是原子性的,它包括三步,“读-改-写”,所以可能会出现这样的情况:有AB两个线程同时来访问这个方法,AB两个线程都看到count的值为9,然后都对其进行加1最后都是10导致一次计数凭空消失。

为什么会出现这样的情况呢?因为这期间存在竞争条件,导致其结果不可靠。当计算的正确性依赖于运行时钟相关的时序或者多线程的交替时,会产生竞争条件。换句话说,想要得到正确的答案,要依赖于“幸运的时序”。最常见的一种竞争条件是“检查再运行”,使用一个潜在的过期值作为决定下一步操作的依据。

就像我们熟知的单例设计模式中的懒汉式,就是这样的竞争条件。

class LazySingleton{    private LazySingleton(){}    private static LazySingleton lazySingleton=null;    //这样是线程不安全的     public static LazySingleton getInstance(){        if(lazySingleton==null){            return new LazySingleton();        }        return lazySingleton;    }}

我们将像count++这样不止一步的操作称之为复合操作,如果这样的复合操作要么一次性全部执行完,要么一点都不执行则可以将这些复合操作称为原子操作

三、锁(Synchronized)

如何将一个复合操作变成原子操作,使用Synchronized关键字就可以做到。Java提供了强制原子性的内置锁机制:Synchronized块。一个Synchronized块有两部分:锁对象的引用,以及这个锁保护的代码块。锁保护的代码块可以保证里面的操作是原子性的。

synchronized(lock){//访问或修改被锁报的共享状态}

每个Java对象都可以隐式地扮演一个用于同步的锁的角色;这些内置的锁被称为内部锁或监视器锁。执行线程进入Synchronized块之前会自动获取锁;而无论正常退出还是抛出异常,线程都会放弃获得的锁。同一个内部锁同一时间只能被一个线程获取,所以内部锁在Java中还扮演了互斥锁的角色。正是因为这个机制,Synchronized才能保证里面的代码块同一时间只能由一个线程来执行,从而保证了其复合操作的原子性。

内部锁还有一个叫重进入 的特性。重进入意味着线程在试图获得它自己占有的锁的时候,请求会成功,同时也意味着锁的请求是基于“每线程”,而不是“每调用的”。重进入的实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并将请求计数置为1。如果同一线程再次请求这个锁,计数器递增,每次占用线程退出同步块,计数器减一。直到计数器为0,锁被释放。

public class Widget{    public synchronized void doSomething(){        .......    }}public class LoggingWidget extends Widget{    public synchronized void doSomething(){        super.doSomething();    }}

如果内部锁不能重进入,此代码会锁死。

对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

在使用synchronized的时候,要懂得合理的保护代码块,不能太大也不能太小。太大的话比如将servlet中的整个service包起来,虽然是安全了,但是违背了容器设计的初衷,这样过个请求一起过来只能一个一个处理;而太小可能会把复合操作拆开,所以使用的时候要进行合理分析。还有一些耗时操作比如请求网络资源,IO操作等,这样的处理尽量不要占有锁

0 0
原创粉丝点击