首页 > 代码库 > Java并发编程实践读书笔记--第一部分 基础知识

Java并发编程实践读书笔记--第一部分 基础知识

 

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition)。最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作。比较简单的例子就是两个线程对同一个为0的int型变量进行n(n越大越能明显看出竞态条件的影响)次加一操作,最后结果很可能不是2n。选择一个点来说明此情况,假设目标变量时x,线程1读取x为12,进行加一得到13,这个时候,线程2也读取了x为12,进行加一操作得到13,然后线程1将13写入x,线程2也将13写回x,这样预期x为14的,结果x确实13。代码比较简单就不在此处贴出了,有兴趣的朋友可以试一下。要避免这个问题,就需要将先检查后执行操作以原子方式执行。比较简单的方式就是使用Synchronized关键字对方法或代码块进行同步,下一部分具体讲解。

Java提供了一种内置锁机制来支持原子性:同步代码块(Synchronized Block)。该机制包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
用法一:
Synchronized (lock){
//code block
}

用法二:
public synchronized void someMethod(){
//code
}
上述用法一,用法二分别展示了同步代码块和同步方法两种方式来让代码以原子方式执行。加锁之所以能让锁保护内的代码以原子方式执行,是因为锁只能被一个线程持有,当一个线程持有该锁后,其他线程只能等待持有锁的线程释放此锁才可以尝试获得此锁。所以当一个线程进入同步代码块获得该锁后,其他线程不会同时执行此代码,不会导致进入代码块的线程读取的数据失效。有两点需要注意
一、用法二没有具体指出使用什么锁,是因为同步方法默认以本对象为锁。
二、每个共享的可变的变量都应该只由一个锁来保护。
一个比较好的加锁约定:
将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得再该对象上不会发生并发访问。

 

可见性是一种复杂的属性,因为可见性中的错误总会违背我们的直觉。
说到可见性就要提一下内存方面的内容,变量保存在内存中,线程执行到某个方法时会将需要的变量读进自己的工作空间中,在这种情况下,如果内存中的值发生变化,工作线程不一定能察觉。也就是说工作线程不一定能看到内存中变量的变化。这就是可见性的问题。解决可见性问题的办法和解决竞态条件的办法一样:加锁。加锁可以保证一次只有一个线程对变量进行处理,当前线程的工作空间中的值永远都是最新的有效值,线程处理完之后将最新值写回内存,然后其他线程再处理。这样,对于所有线程来说,内存中的变量就是可见的(所有线程中的变量值都是有效的)。

volatile变量是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。换句话说,每次访问volatile类型的变量,都会从内存中读取变量的最新值。书中给出一个典型的用法

数绵羊程序
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();

该程序在还没睡着时会一直数绵羊,睡着之后跳出循环。因为对asleep的更新是在其他线程中完成的,所以普通变量没办法及时得到asleep的值,这里使用volatile变量就非常合适。
要注意一点,volatile变量只能保证可见性,即该变量会一直与内存中的变量保持一致,但是没办法确保原子性,所以如果对volatile变量进行基于之前值的操作,比如+1操作,就会出现竞态条件。而加锁机制既可以确保可见性又可以确保原子性。

线程封闭是一种不需要使用同步也能保证数据安全的技术。竞态条件的发生是因为在多个线程中共享数据。如果把需要的对象都封闭在线程内部,那么,即使这个对象不是线程安全的也不会发生竞态条件。当觉定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。线程封闭需要在设计时规划,也比较容易理解,就不赘述了。

Java并发编程实践读书笔记--第一部分 基础知识