Home 成长之路 线程安全与否

线程安全与否

0 60

查阅资料时常常看到线程安全和线程不安全的字样,特此记录二者的详细定义,同时讨论线程安全我们有必要了解什么是原子操作,什么是原子类以及同步,非同步和阻塞非阻塞的区别

 

 

1.同步与异步

编辑

同步和异步 [1]  关注的是消息通信机制 (synchronous communication/ asynchronous communication)。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2.阻塞与非阻塞

编辑

阻塞和非阻塞 [2]  关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

3.结合两概念分析

编辑

  • 进程通信 [3]  层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。
  • 发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。
  • 在 IO 系统调用层面( IO system call )层面, 非阻塞 IO 系统调用  异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )
  • 非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。

 

原子操作:

原子操作(atomic operation)是不需要synchronized”,这是多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 [1]  换到另一个线程)

定义

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。

特性

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是” 原子操作”,因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的”读-改-写”过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:
⒈ CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
⒉ CPU B从内存单元把当前计数值⑵装载进它的寄存器中。
⒊ CPU A在它的寄存器中将计数值递减为1;
⒋ CPU B在它的寄存器中将计数值递减为1;
⒌ CPU A把修改后的计数值⑴写回内存单元。
⒍ CPU B把修改后的计数值⑴写回内存单元。
我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它–两个进程都推断出:计数值是1,共享资源仍然在被使用。

线程安全:

 

一、线程安全

是指你的代码所在的进程中有多个线程同时运行,而这些线程可能会同时运行这段代码,如果每次运行的代码结

果和单线程运行的结果是一样的,而且其他变量的值和预期的也是一样的,那么就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,

也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行

写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

二、非线程安全

是不提供代码数据访问保护,可能出现多个线程先后访问更改数据造成所得的数据是脏数据。*(脏数据是表示一个数据已经

被修改,但是还没有保存或进一步的处理。例如你操作数据库修改某一字段内容,在你修改了但还没commit时,另一线程在读取这

数据,他读取的就是你修改前的数据,但事实上你已经修改了,这就是脏数据了。)*在多个线程同时访问同一个对象时会发生数据

错误 不完整等情况时 那就是线程不安全,不会发生上续错误时是线程安全的,一般采用锁机制。

三、线程安全出现的根本原因

存在两个或者两个以上的线程对象共享同一个资源;

多线程操作共享资源代码有多个语句。

存在竞争的线程不安全,不存在竞争的线程就是安全的

 

 

存在线程安全问题必须满足三个条件:

1.有共享变量

2.处在多线程环境下

3.共享变量有修改操作。

所谓解决线程安全问题无非就是将操作原子化,正如楼上各位所说的加sychronized,或者加lock什么的,只要将操作原子化就能避免线程安全的问题。但加锁会有性能问题。

所以在多线程情况下,优先考虑能否不用共享,优先使用局部变量代替共享的全局变量。

只能用共享变量的时候优先使用原子类,诸如AtomicInteger尔尔。

没有原子类,可以自己创造自己的原子类。

如以上方法都不能奏效,再考虑使用sychro,lock之类尔尔。

别一上来就sychro,不仅low,而且显的不够专业

 

四、怎么实现线程安全

1、synchronized
同步代码块、同步方法

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完

整性,使用方法一般是加在方法上。

public class ThreadDemo {

int count = 0; // 记录方法的命中次数

public synchronized void threadMethod(int j) {

count++ ;

int i = 1;

j = j + i;
}
}

这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2.Lock
Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块

五、分别在什么情况下使用

线程安全是多线程操作同一个对象不会有问题;

非线程安全是指多线程操作同一个对象可能会出现问题;

线程安全会导致性能的降低。

如果是多个线程操作同一个对象,那么使用线程安全的Vector;否则,就使用效率更高的ArrayList。

PS(非线程安全并非不安全)

非线程安全并不是多线程环境下就不能使用。

 

六、集合线程安全实例

1.Vector
Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用

2.HashTable
HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

3.Collections包装方法
Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());
Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

4.java.util.concurrent包中的集合
4.1ConcurrentHashMap
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

4.2CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

5.除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

 

转载整理自:

————————————————
版权声明:本文为CSDN博主「写代码的彭于晏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lixiaobuaa/article/details/79689338

————————————————
版权声明:本文为CSDN博主「CodeHunter_qcy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43390235/article/details/90897699

作者:花溪的小石头
链接:https://www.zhihu.com/question/49855966/answer/298161490
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SIMILAR ARTICLES

发表评论

发表评论