什么是线程安全


        线程安全定义:

       当多个线程访问一个类的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方法代码不必做其他的协调,这个类的行为任然是正确的,那么称这个类是线程安全的。

        所有的例子都是一个servlet用来进行因数分解,通过request传入一个数字,然后service方法调用factor方法进行因数分解。

       1. 无状态的对象永远线程安全

public class StatelessFactorizer extends GenericServlet implements Servlet {

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

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}
        这个类没有一个私有或公共的变量,是一个无状态的类。因为多个线程进入service方法,所有的service方法内的局部变量都封装在各个线程的栈中,只有本线程可以访问,所以,这个类是线程安全的。

        2. 原子性

@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}
          我们在第一个例子上,添加了一个功能计数。每调一次service方法,count加1。很可惜,这个类不是线程安全的,因为++count不是一个原子操作,这是一个读-改-写三个步骤的操作。这样我们就很容易理解这个类为什么不是线程安全的了,线程A读到count值为1,然后修改为2。在线程A将count的值写回去之前,线程B读取了count的值。结果是线程A将count的值改为2,线程B也将count的值改为2。至于什么是原子操作,我也没看懂,定义如下:

        假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B的时候,要么B全部执行完成,要么一点也没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面描述的状态。(我已经晕了)

        让count加一操作变成原子操作的代码如下:

@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
	private final AtomicLong count = new AtomicLong(0);

	public long getCount() {
		return count.get();
	}

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		count.incrementAndGet();
		encodeIntoResponse(resp, factors);
	}

	void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
	}

	BigInteger extractFromRequest(ServletRequest req) {
		return null;
	}

	BigInteger[] factor(BigInteger i) {
		return null;
	}
}
        AtomicLong是什么鬼,这个大家去问度娘吧。简单说,它在读-改-写的第三步写操作时还会回去查看值是否已经被改变,如果被改变,重新加载。(可以百度搜索java cas)


     3. 锁

      我们上面所说的自增操作,通过atomic类变成了原子操作,因此类也就变成线程安全的了。但是如果有多个全局变量,并且这多个全局变量的值具有关联性。场景如下:我们想实现应对连续两个客户请求相同的数字进行因数分解,于是我们缓存了最新的计算结果

@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		if (i.equals(lastNumber.get()))
			encodeIntoResponse(resp, lastFactors.get());
		else {
			BigInteger[] factors = factor(i);
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp, factors);
		}
	}

	void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
	}

	BigInteger extractFromRequest(ServletRequest req) {
		return new BigInteger("7");
	}

	BigInteger[] factor(BigInteger i) {
		// Doesn't really factor
		return new BigInteger[] { i };
	}
这个类不是线程安全的,因为lastNumber和lastFactors这两个变量是有关系的,不是独立的。我们的atomic只能保证他们自身的操作是线程安全,无法保证两个一起是线程安全的。假设,lastNumber被线程A修改为了10,而lastFactors还是旧值9的因数分解结果,那线程B刚好请求数字10的因数分解结果,就拿到了9的因数分解结果。

        这里我们很容易想到用synchronized关键字来给service方法加锁,我就不贴代码了,这样做的确能够解决问题,但是service方法一次只能进入一个线程了,性能存在很大问题。我们在做并发编程的时候,一定要兼顾性能,最好的方式如下所示:

@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;

    public synchronized long getHits() {
        return hits;
    }

    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this) {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}
          把锁打碎在方法里面,第一个synchronized块保护着检查再运行的操作,另一个保证缓存的number和factor同步更新。提醒一点,在加锁期间,不要进行耗时操作,我们把锁移到方法里面,主要目的就是将那些耗时操作剔除到同步块之外。






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
线程安全是指在多线程环境下,当多个线程同时共享一个全局变量或静态变量进行写操作时,可能会发生数据冲突问题。而做读操作不会引发线程安全问题。为了保证线程安全,可以采用加锁机制,使每次执行的结果和单线程执行的结果一样,避免意外结果的出现。线程不安全则指在没有提供加锁机制保护的情况下,多个线程先后更改数据,导致所得到的数据是脏数据。 保证线程安全的方法有多种。其中一种方法是使用同步机制,例如使用synchronized关键字来保护共享数据的访问。在使用synchronized关键字修饰的代码块或方法中,同一时间只能有一个线程访问,其他线程需要等待。这样可以避免多个线程同时修改共享数据导致的数据错误。另外一种方法是使用原子操作类,例如使用AtomicInteger来保证对整数类型的数据的原子操作。这样可以避免多个线程同时对同一变量进行修改而导致的数据不一致问题。还可以使用锁机制,例如使用Lock接口和ReentrantLock类来控制对共享数据的访问,使用读写锁来实现读写分离的并发控制等。这些方法都可以保证多个线程在访问共享数据时的线程安全性。 总之,线程安全是在多线程环境下保证共享数据的正确访问的一种机制,可以通过使用同步机制、原子操作类和锁机制等方法来保证线程安全。这样可以避免多个线程同时对共享数据进行修改而导致的数据错误。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [什么是线程安全问题 及怎么解决线程安全问题](https://blog.csdn.net/weixin_43464372/article/details/108233648)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [什么是线程安全?如何保证线程安全?](https://blog.csdn.net/q669239799/article/details/90614077)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值