众所周知,多线程访问同一公共资源会带来线程的不安全,本文探讨一下这个问题的若干细节。
关于线程安全的基本问题
有关线程安全常涉及两个概念:
竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
临界区:导致竞态条件发生的代码区称作临界区(线程不安全的代码区)。
线程安全与竞态条件:线程安全的代码区不存在竞态条件,线程不安全的代码区(临界区)存在竞态条件。
为什么会出现线程安全问题?
道理很简单,多个线程同时访问(写操作)同一个公共资源必然带来问题,我们举2个例子:
例1:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B也对i加1,那么线程A,线程B提交后,i的结果为1,而不是我们期望的结果2。
例2:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B随后读取i,那么线程B读取的结果依然为0,而不是我们期望的结果1。这就是多线程并发导致的可见性问题。
如何解决线程不安全?
临界区进行同步,从而避免竞态条件。如:使用synchronized或JUC中的Lock对临界区加锁。给临界区加锁好比给公园的一个厕所加了一把锁,避免了人们共同进入厕所的问题,而必须是只有拿到钥匙的人才能进入(这个例子有点那个,但是我总会联想到这个例子)。
举例说明
既然多线程引发的安全问题是因为同时访问同一个公共资源导致的,相应的,如果多线程访问的不是公共资源也就不会发生线程安全问题。下面按资源是否公共举几个例子来说明问题。
公共资源:对象的成员变量
对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。
示例代码:
java;gutter:true;
public class ThreadSafe_ {
public static void main(String[] args) {
Obj obj = new Obj();
MyRunnable task = new MyRunnable(obj);
new Thread(task,"t1").start();
new Thread(task,"t2").start();
}
}
class Obj {
StringBuilder noSafeBuilder = new StringBuilder();// 对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。</p>
<pre><code>void add(String text){
noSafeBuilder.append(text);
}
</code></pre>
<p>}
class MyRunnable implements Runnable{
private Obj obj = null;</p>
<pre><code>MyRunnable(Obj obj){
this.obj = obj;
}
@Override
public void run() {// 临界区(线程不安全的代码区,因为多个线程可能同时修改成员变量noSafeBulider,所以会带来问题)
this.obj.add(" "+Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString());
}
</code></pre>
<p>}</p>
<pre><code>
打印结果:
t2=> t1 t2
t1=> t1 t2
当然,上面run方法因为线程不安全,无法保证线程的执行顺序,所以上面的代码运行多次可能带来多个结果:在这段代码中,我们期望的是,Obj对象的noSafeBuilder属性先追加1个线程名然后打印这个线程名,然后noSafeBuilder再追加第2个线程名并打印追加的2个线程名,但从打印结果看,第1个线程在执行完run方法后就已经打印了2个线程名——这就是线程不安全所带来的问题。
![线程不安全](https://johngo-pic.oss-cn-beijing.aliyuncs.com/articles/20230526/797348-20210219200135010-1769034103.png)
从上图可以知道,线程不安全会带来多少问题。
解决办法:
;gutter:true;
class MyRunnable implements Runnable{
private Obj obj = null;
MyRunnable(Obj obj){
this.obj = obj;
}
private final Object lock = new Object();
@Override
public void run() {
synchronized (lock) {//临界区 加锁同步
this.obj.add(" "+Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString());
}
}
}
打印结果:
t1=> t1
t2=> t1 t2
由于t2可能先执行,所以上面代码的打印结果也可能为:
t2=> t2
t1=> t2 t1
非公共资源:局部基本类型变量
局部变量存储在线程自己的栈中,也就是说,局部变量永远也不会被多个线程共享。如:
java;gutter:true;
public class ThreadTest {
public static void main(String[]args){
MyThread share = new MyThread();
for (int i=0;i</p>
<pre><code>
无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据。
## 特殊资源:局部的对象引用
为什么这个是特殊示例呢?因为对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。这就意味着访问对象的引用的代码可能是线程安全的,也有可能是线程不安全的——这主要取决于: **在某个方法中创建的对象是否会逃逸**(即该对象不会被其它方法获得,也不会被非局部变量引用到),如果不会,那么这个方法就是线程安全的,反之就是不安全的。
实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。如:
;gutter:true;
public void method1(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
Original: https://www.cnblogs.com/wql025/p/14391704.html
Author: Tom1997
Title: 线程不安全
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/542090/
转载文章受原作者版权保护。转载请注明原作者出处!