1. 应用背景
程序在设计当中如果采取多线程操作的时候,如果操作的对象是一个的话,由于多个线程共享同一块内存空间,因此经常会遇到数据安全访问的问题,下面看一个经典的问题,银行取钱的问题:
1)、你有一张银行卡,里面有5000块钱,然后你到取款机取款,取出3000,当正在取的时候,取款机已经查询到你有5000块钱,然后正准备减去300块钱的时候
2)、你的老婆拿着那张银行卡对应的存折到银行取钱,也要取3000.然后银行的系统查询,存折账户里还有6000(因为上面钱还没扣),所以它也准备减去3000,
3)、你的卡里面减去3000,5000-3000=2000,并且你老婆的存折也是5000-3000=2000。
4)、结果,你们一共取了6000,但是卡里还剩下2000。
不难发现,当多个线程访问同一数据并操作的时候非常容易出现类似的问题,。为了避免这样的事情发生,我们要保证线程同步互斥,所谓同步互斥就是:并发执行的多个线程在某一时间内只允许一个线程在执行以访问共享数据。
2.同步互斥锁
同步锁原理:Java会为每个对象内置同步锁,通过使用synchronized来获取一个对象的同步锁,synchronized的使用方式,是在一段代码块中,加上synchronized(object){ ... }
当线程首次执行到synchronized语句块时候会获得对象的同步锁(锁最开始属于对象,后被线程持有),在当前线程不释放同步锁时候,其他线程获取该对象同步锁的行为是被阻塞的,直到该锁被释放。以下几种情况下,线程才会释放掉对象的同步锁
1.线程执行完synchronized修饰的语句块。
2.线程主动执行wait()来释放同步锁。
同步锁虽然可以解决多并发引起的数据安全问题,但是会在一定程度上影响程序运行的效率,也会引起死锁问题。因此慎重使用。下面是别人描述的死锁,做引用。
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不能正常运行。简单的说就是:线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源。这里举一个通俗的例子:如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。
一个死锁的造成很简单,比如有两个对象A 和 B 。第一个线程锁住了A,然后休眠1秒,轮到第二个线程执行,第二个线程锁住了B,然后也休眠1秒,然后有轮到第一个线程执行。第一个线程又企图锁住B,可是B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线程执行。第二个线程又企图锁住A,可是A已经被第一个线程锁定了,所以第二个线程也进入阻塞状态。就这样,死锁造成了。
3.显式锁
为了符合Java面向对象的设计原则,在JDK1.5中,引入了显示锁的概念。
程序设计过程中如果提前发现可能会引起多线程数据访问的安全问题,可以通过Lock lock =newReentrantLock();来获取一个锁,并将该锁作为运行参数传入其中,在关键数据块之前使用lock.lock();来控制对竞争资源并发访问的控制,显式锁的优点是可以知道持有锁的对象,比同步锁也清晰好多。当然使用完之后也要主动释放(lock.unlock())。
4.读写锁
读写锁是在显示锁的基础上对读写进行分离的一种锁,可以认为是为了提高并发效率的一种优化。使用方法类比显式锁
初始化:ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
四种操作: rwl.readLock().lock(); rwl.readLock().unlock()
rwl.writeLock().lock(); rwl.writeLock().unlock()
关于读写锁之间的互斥:
1,读锁是排写锁操作的,读锁不排读锁操作,多个读锁可以并发不阻塞。即在读锁获取后和读锁释放之前,写锁并不能被任何线程获得,多个读锁同时作用期间,试图获取写锁的线程都处于等待状态,当最后一个读锁释放后,试图获取写锁的线程才有机会获取写锁。
2,写锁是排写锁、排读锁操作的。当一个线程获取到写锁之后,其他试图获取写锁和试图获取读锁的线程都处于等待状态,直到写锁被释放。
3,在写锁状态中,可以获取读锁 即线程持有写锁的状态下是可以继续申请读锁的。即一线程同时持有读锁和写锁
4,读锁是不能够获得写锁的,如果要加写锁,本线程必须释放所持有的读锁。
5.volatile
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作,可以看作是一种轻量级的synchronized,但是是尽量保证每次读取的是最新的,并不绝对保证。