为了保证在多线程情况下数据访问的正确性,通常需要使用同步机制。
java 语言从 JDK1.0 版本开始就提供了同步锁,并且从 JDK1.5 开始提供了可重入锁、读写锁和原子操作等同步控制方式。
当多个线程同时对某一个数据区或内存位置进行操作时,如果不施加任何措施,很可能造成数据操作混乱。
如同数据库中的 脏读、不可重复读和幻读等情况。
当只有一个线程访问数据时,数据竞争基本不会存在;只有多线程同时访问数据时,才会发生数据竞争。
数据竞争问题是有至少两个同时执行的线程访问同一个内存位置并且至少有一个线程尝试写入数据而引起的问题。
为了避免数据竞争,通常需要在程序中加入同步机制,以保证数据访问的正确性。有些同步机制(如:锁)可以保证数据在某一时间内只有一个线程访问,
有些同步机制(如:软件事务性内存)可以让数据由多个线程操作,虽然多线程同时访问,但是会保证最早提交的数据有效,其他的数据操作要回滚。
某一段被多个线程共享的数据区域,线程必须对它进行互斥访问;线程中访问共享数据的那段代码被称为临界区(Critical Section)。线程进入临界区需要遵循一定的原则。
为了帮助程序员实现临界区,Java 提供了同步机制。当一个线程试图访问临界区时,同步机制会判断当前是否有其他线程正在使用临界区。
在 Java 语言中,监视器(Monitor)具有如下特征:
当线程请求某一种资源时,如果线程的请求得不到响应,线程可以采用多种方式来决定接下来要采取的动作。
1、线程可以采用一直尝试的方式,在每次请求资源得不到得不到满足的情况下,下一次依然继续请求,直到请求获得满足。采用这种方式的线程处于一种非阻塞状态,如果资源被占用的时间过长,这种方式必然会导致CPU资源的浪费。
2、另外一种方式:线程并不是一直等待,而是被阻塞;这样CPU的资源可以让出来执行一些其他的操作。线程被阻塞表示线程可能被CPU挂起,等待某一时间后,再去尝试获取资源。
在程序设计过程中,一般要先保证程序的正确性,其次才是提高程序的性能。
在传统的串行执行的程序中,程序往往有一个固定的执行次序,对于数据的访问操作也是有顺序的。
然而,在多线程程序中,必须保证数据被多个线程操作是安全的。
一个对象是否是线程安全的?
当多线程同时访问某个类时,不管线程之间如果交替执行,总能够得到正确的执行结果,则称这个类是线程安全的;否则是线程不安全的。
要编写线程安全的代码,需要特别注意哪些共享的(Shared)和可变的(Mutable)数据或状态的操作。共享意味着变量可以被对个线程所访问,可变意味着变量的值在其生命周期内会发生变化。
线程安全的代码需要采用同步机制来控制对于共享的或者可变的变量的访问,特别是多个线程中至少存在一个写操作的情况下。
Java 工具集合中提供的类有些是线程安全的(如:HashTable),有的则不是(如:HashMap)。一般在线程安全的类中已经封装了必要的同步控制机制,因此不必进一步采取同步控制措施。
在程序的设计语言中,锁提供了一种数据安全访问的方式,锁一般分为加锁和解锁两个操作。对共享数据操作前,要先进行加锁;操作完成后,再进行解锁。
加锁以后的临界区只能被持有锁的线程占有,其他线程不能进入这段临界区,只能等待。
Java 语言提供同步锁、可重入锁和读写锁等同步机制,用于确保数据访问的正确性。
在 JAVA 语言中,从 JDK1.0 开始就支持同步锁的使用了。它可以采用两形式:同步方法和同步代码块。
不论哪种方式,都需要使用 synchronized 关键字,但两者表现形式不同。
同步方法
采用 synchronized 作为方法的修饰词,将方法整体限定在同步控制区域内;同一时刻只能有一个线程对其进行访问。
同步块
同步块是使用 synchronized 修饰的一块代码,它不像同步方法那样使整个方法都是被同步控制,而是针对某一块代码进行同步控制。
同步块需要明确地指出监视器对象,通常加载 synchronized 后的小括内,使用比较多的情况是使用当前对象 this 作为监视器对象。
比较
同步块比同步方法可以实现更细粒度的同步控制,但同步方法的使用更加简便,不用考虑同步对象等因素。但是,有时整个方法加上 synchronized 块,程序性能并不好,这是因为函数内部可能需要同步的只有小部分共享数据而已。
需要注意的是,这两种方法都是使用 JVM 内置的监视器。
可重入锁是一种无阻塞的同步机制,它在 java.util.concurrent.locks 包下;定义的形式如下:
public class ReentrantLock extends Object implements Lock ,Serializable
可重入锁是互斥锁,它和同步锁具有基本相同的行为和语义,但是比同步锁功能更强大。如获取锁时的公平性设置、测试锁 trylock、测试锁是否正在被持有、锁的获取顺序等。
读写锁从 JDK1.5 版本开始引入的一种锁机制,它维护一对相互关联的锁:读锁和写锁。在没有线程持有写锁的情况下,读锁可以由多个线程同时持有;写锁是排他锁,只能有一个线程持有。
读写锁允许多个线程同时读,只允许一个线程同时写。
读写锁 ReentrantReadWriteLock 类定义的一般形式:
public class ReentrantReadWriteLock extends Object implements ReadWriteLock,Serializable
邮戳锁是 JDK1.8 版本后引入的一种锁机制,与 ReentrantReadWriteLock 类似,该锁可以用于控制读写访问。邮戳锁的定义形式:
public class StampedLock extends Object implements Serializable
从邮戳锁的定义可以看出,它是从类 Object 直接继承,与 ReentrantReadWriteLock 类似,它实现了 Serializable 接口。由于邮戳锁支持多种锁模式,所以这个类没有直接实现接口 Lock 和接口 ReadWriteLock。
在使用锁的时候,要注意避免死锁和活锁的问题,两者都会引起线程等待,降低程序的执行效率。
4.1、死锁
死锁是指两个或者多个线程在执行过程中,因竞争资源而相互等待的现象。处于死锁状态的线程无法继续运行,只有死锁解除才能继续。
4.2、活锁
活锁指程序在执行过程中,由于某些条件发送,会导致程序一直处于等待状态。与死锁类似,任务的处理一直处于等待状态,得不到解决,无法继续进行下去;与死锁不同的是,活锁有可能解开,但死锁不行。