在构建稳定的并发程序师,必须正确的使用线程和锁。但这些终究只是一些机制。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(shared)和可变的(mutable)状态访问。
共享意味着变量可以有多个线程同时访问,而可变则意味着变量的值在其生命周期内可以发生变化。
一个对象是否需要时线程安全的,取决于它是否被多个线程访问。当多个线程访问某个状态变量并且其中有一个线程执行写入操作室,必须采用同步机制来协同这些线程对变量的访问。
java中主要同步机制是关键字 synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括 volatile类型的变量,显式锁(Explicit Lock)以及院子变量。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题。
1、不再线程之间共享该状态变量。
2、将状态变量修改为不可变的变量。
3、在访问状态变量时使用同步。
状态变量:个人理解为一个类的成员变量,或是可以由多个程序/线程调用的变量。
一、原子性
例 ++count;
虽然这个递增操作是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非是原型的,因而它并不会作为一个不可分割的操作来执行。实际上,他包含了三个独立的操作:读取count的值,将值+1,然后将结果写入count。这事一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。
1、竟态条件:当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竟态条件。简单的说正确的结果取决于“运气”
要避免竟态条件,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程智能在修改操作完成之前活之后读取和修改状态,而不是在修改状态的过程中。
在java.util.concurrentl.atomic包中包含了ixie院子变量类,用于实现在数值和对象引用上的院子状态转换。
二、加锁机制
1、内置锁:java提供了一种那只的锁机制来支持原子性:同步代码块(Synchronized Block)
以关键字synchronizated 来修饰的方法就是一种横跨整个方法提的同步代码块,其中台同步代码块的锁就是方法调用所在的对象。静态的synchronized方法是以Class对象作为锁的。
synchronizated(lock){ //TODO }
每个java对象都可以用作一个实现同步的所,这些所被称为内置锁 或是监视器锁。线程在进入同步代码块之前会自动获得所,并且在推出同步代码块是自动释放锁,而无论是通过正常的控制路径推出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
java的那只所相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种所。当线程A尝试获取一个有B持有的锁时,A必须等待或者阻塞,知道B释放这个锁。由于每次之恩那个有一个线程执行内置锁保护的代码块,银子这个锁保护的同步代码块会议原子方式进行执行,多个线程在执行该代码块时也不会互相干扰。
2、重入
重入意味着 获取所的操作的力度是线程而不是调用。
例:
public class Widget{ public synchronized void doSomething(){ ...... } } public class LogginWidget extends Widget{ public synchronized void doSomething(){ System.out.println("calling doSomething"); super.doSomething(); } }
如果一个线程调用了 LoginWidget.doSomething();那么它会执行两个含有内置锁的方法 LoginWidget.doSomething()和Widget.doSomthing();如果锁是不可以重入的话,那么就永远也无法获得super.doSomething的锁。
个人对重入的理解:这两个内置锁为当前对象,当锁相同的时候就可以进行重入操作。
一种常见的加锁约定是,将所有的可以变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路经进型同步,使得在该对象上不会发生并发访问。
并非所有的数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
当执行时间较长的计算或者可能无法夸苏完成的操作时(例如:网络IO或者控制台IO)一定不要持有锁