并发基础知识(一·)

1.1 什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

1.2 原子性

//存在于类A中的变量count
++count;

现在假设有两个线程,他们都将运行上方 count++的代码,那么在当前的多线程环境下,他很会可能出错,那是因为count并不是一个原子性操作,他并不会作为一个不可分割的操作来执行,它实际上包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。这是一个“读取——修改——写入”的操作序列,并且其结果状态依赖于之前的状。

类似着严寒的写法,在多次调用代码后,将导致严重的数据完整性问题。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果,我们成为状态竞争(Race Condition 也翻译为竞态条件)。

1.2.1 竞态条件(状态竞争)
当某个计算的正确性取决于多个线程交替执行时序的时候,就会发生状态竞争(竞态条件)。也就是说,获得什么样的结果取决于运气啦。 最常见的状态竞争(竞态条件)类型就是“先检查后执行”操作,就是说通过一个可能失效的观测结果来决定下一步的动作。

1.2.2 复合操作——加锁机制
要避免状态竞争(竞态条件)问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。

为了确保线程安全性,类似“先检查后执行”和“读取——修改——写入”等操作必须是原子性的。我们将“先检查后执行”以及“读取——修改——写入”等操作称为复合操作: 有一组必须以原子方式执行的操作,为了确保线程安全性。

JAVA提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block),他分为两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。一关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

synchronized(lock)
{
//访问或修改由锁保护的共享状态
}

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程师徒获得一个已经由它自己持有的锁,那么这个请求就会成功。 这意味着,获取锁的对象不仅仅可以是“调用”,还可以是“线程”自己,这将大大细化我们获取锁操作的粒度,从调用,变成线程。 同时,重入也进一步提高了加锁行为的封装性。

1.2.3 用锁来保护状态,关于活跃性和性能
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
但是,并非所有数据都素要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
另外,我们举一个例子

if(!vector.cotains(element))
 vector.add(element)

在这段代码中,虽然contains和add等方法都是原子方法,但是上方的代码仍然存在状态竞争(竞态条件),就是说,如果把多个操作合并为一个复合操作的话买还是需要额外的加锁机制。此外,将每个方法都作为同步方法还可能导致活跃性问题或性能问题。

关于性能,如果我们直接对方法使用synchronized,那么这种简单且大粒度的方法确实能确保线程安全,但付出的性能代价却很高。
就是说,在很多程序中,我们并不需要将方法的全部都加锁起来,而是只需要使用同步代码块,包含一小段影响线程安全的代码。就是说,我们要判断同步代码块的合理大小,需要在各种设计需求之间平衡,包括安全性,简单性和性能。
建议:在执行时间较长的计算或者可能无法快速完成的操作时候,例如网络I/O或控制台I/O,一定不要持有锁。

猜你喜欢

转载自blog.csdn.net/qq_36120793/article/details/80504302
今日推荐