多线程编程(二)----锁
二、锁
Java中的锁主要用于保障并发线程情况下数据的一致性
(一)锁分类
1.从悲观和乐观的角度:悲观锁和乐观锁
2.从获取资源的公平性:公平锁和非公平锁
3.从是否共享资源角度:共享锁和独占锁
4.从锁的状态角度:偏向锁、轻量级锁和重量级锁
5.JVM中使用了自旋锁以更快地使用CPU资源
(三)乐观锁和悲观锁
1.乐观锁:每次读取数据时都认为别人不会修改数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法
具体过程如下: 比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作
CAS:Java中的乐观锁大部分是通过CAS(Compare And Swap 比较和交换)操作实现的。
CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态
2.悲观锁:在每次服务数据时都认为别人会修改数据,所以每次读写数据时都会上锁,这样别人想读写这个数据时,都会被阻塞,等待知道拿到锁。
AQS:Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized 抽象的队列同步器)架构实现
AQS定义一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如Synchronized、ReentrantLock、Semaphore、CountDownLatch等,该框架下的锁会先尝试以CAS乐观锁获取锁,如果获取不到,则会转为悲观锁
(四)自旋锁
操作:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起的状态,只需要等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗
自旋时间:线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,见鬼产生CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。JDK1.5为固定的自旋时间,1.6引入了适应性自旋锁,他的时间将不再是固定的,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,可以节本认为一个线程上下文切换的时间就是一个最佳的时间
(五)synchronized
1.定义:它用于Java对象,方法,代码块提供线程安全的操作,synchronized属于独占式的悲观锁,同时属于可重入锁
在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问
Java中的每个对象都有个monitor对象,加锁即使在竞争monitor对象。对代码块加锁是通过对在前后分别加上monitorenter和monitorexit指令来实现的,对方法是否加锁是通过一个标记来判断的
2.作用范围
(1)成员变量和非静态方法:锁住的是对象实例,即this对象
(2)静态方法:锁住的是Class实例,因为静态方法属于Class而不属于对象
(3)代码块:锁住的是所有代码块中配置的对象
public class B5 {
public static void main(String[] args) {
}
}
class SynchronizedDemo {
public static void main(String[] args) {
//一个对象 线程1会等待线程2执行完成才能执行。
//因为syn锁住了当前的对象实例
final SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
new Thread(new Runnable(){
@Override
public void run() {
synchronizedDemo.generalMethod1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
synchronizedDemo.generalMethod2();
}
}).start();
}
//修饰普通的同步方法,锁住的是当前实例对象
//如果加注释 static,那么锁住的是当前类Class对象 和一个实例对象 结果一致 它属于全局锁 会锁住所有调用该方法的线程
String lockA="lockA";
public synchronized void generalMethod1() {
//synchronized(lockA){
try {
for (int i = 1; i < 3; i++) {
System.out.println("方法1执行" + i + "次!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// }
}
//修饰普通的同步方法,锁住的是当前实例对象
public synchronized void generalMethod2() {
//synchronized(lockA){//锁住同一个对象,因此,虽然是多个实例对象,还是先后执行 不会争抢
try {
for (int i = 1; i < 3; i++) {
System.out.println("方法2执行" + i + "次!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//}
}
}
public class B6 {
public static void main(String[] args) {
final SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
final SynchronizedDemo synchronizedDemo1=new SynchronizedDemo();
new Thread(new Runnable(){
@Override
public void run() {
synchronizedDemo.generalMethod1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
synchronizedDemo1.generalMethod2();
}
}).start();
}
}
当把方法改成静态后,synchronized就相当于全局锁,会锁住所有调用该方法的线程,因此即是多个对象也不会争抢
3.总结
synchronized是一个重量级操作,需要操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间。
JDK1.6后对synchorized做了很多优化,引入了自旋,锁消除,锁粗化,轻量级锁及偏向锁等以提高锁的效率。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级叫锁膨胀,在1.6中默认开启了偏向锁和轻量级锁
(六)死锁
1.定义:死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,弱无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁,这些永远在互相等待的进程成为死锁进程
public class DeadLock {
public static void main(String[] args) {
R1 r = new R1();
R1 r1 = new R1();
new Thread(new Runnable() {
@Override
public void run() {
r.run1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
r1.run2();
}
}).start();
}
}
class R1 {
private static String text1 = "text1";
private static String text2 = "text2";
public void run2() {
synchronized (text2) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (text1) {
}
}
}
}
public void run1() {
synchronized (text1) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (text2) {
}
}
}
}
}
2.线程同步synchronized
经典问题-------银行取钱
(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。
(2)用户输入取款金额
(3)系统判断账户余额是否大于取款金额
(4)如果余额大于取款金额,取款成功,反之取款失败
public class Account
{
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}
}
public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account ,
double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及到数据安全问题。
public void run()
{
//使用account作为同步监视器,任何线程进入下面同步代码块之前,
//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
//这种做法符合:加锁-->修改完成-->释放锁 逻辑
synchronized (account)
{
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
//吐出钞票
System.out.println(getName() +
"取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
}
public class TestDraw
{
public static void main(String[] args)
{
//创建一个账户
Account acct = new Account("NO1234567" , 800);
//模拟两个线程对同一个账户取钱
new DrawThread("甲" , acct , 800).start();
new DrawThread("乙" , acct , 800).start();
}
}
同步核心方法使其同步
public class Account
{
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public double getBalance()
{
return this.balance;
}
//使用同步方法,仅有该方法是控制款项是否有变化
public synchronized void draw(double drawAmount)
{
//账户余额大于取钱数目
if (balance >= drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName() +
"取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName() +
"取钱失败!余额不足!");
}
}
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account ,
double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及到数据安全问题。
public void run()
{
//调用账户中的同步方法,先锁定,
//使仅有一个一线程处理,再解锁,再有其他线程进行处理
account.draw(drawAmount);
}
}
public class TestDraw
{
public static void main(String[] args)
{
//创建一个账户
Account acct = new Account("1234567" , 1000);
//模拟两个线程对同一个账户取钱
new DrawThread("甲" , acct , 800).start();
new DrawThread("乙" , acct , 800).start();
}
}
(七)ReentrantLock
1.定义:继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。
ReentrantLock通过自定义队列同步器(Abstract Queued Synchorized,AQS)来实现锁的获取与释放。
独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。
ReentrantLock支持公平锁和非公平锁的实现。公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。
ReentrantLock不但提供了synchronized对锁的操作功能还提供了诸如可响应中断锁,可轮询锁请求、定时锁等避免多线程死锁的方法
2.用法
ReentrantLock之所以被称为可重入锁,是因为他可以反复进入。即允许连续两次获取同一把锁,两次释放同一把锁,也就是可以两次释放,两次加锁。但是如果不对称,会抛出异常
public class D implements Runnable{
//1 定义一个ReentrantLock
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
@Override
public void run() {
for(int j=0;j<10;j++){
lock.lock();//2 加锁 可重入锁
//lock.lock();
try{
i++;
}finally{
lock.unlock();//3释放锁 可重入锁
// lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
D d=new D();
Thread t1=new Thread(d);
t1.start();;
t1.join();
System.out.println(i);
}
}
3.ReentrantLock如何避免死锁
方案一 响应中断
在等待锁的过程中,线程可以根据需要取消对锁的请求
public class D {
private Lock lock=new ReentrantLock();//可重入锁
public void doBussiness(){
String name=Thread.currentThread().getName();
try{
if(!lock.tryLock(2, TimeUnit.SECONDS)){
//可轮询锁
System.out.println(Thread.currentThread().getName()+"没有获得锁!");
return ;
}
//lock.lock();//正常可重入锁
System.out.println(name + " 开始获取锁");
//lock.lockInterruptibly();//可中断响应锁
System.out.println(name + " 得到锁");
System.out.println(name + " 开工干活");
for(int i=0;i<5;i++){
Thread.sleep(1000);
System.out.println(name +"干活" + i);
}
}catch(InterruptedException e){//可中断异常 wait join sleep
System.out.println(name + " 被中断");
System.out.println("因此"+name + "线程可以做些别的事情");
}finally{
try {
lock.unlock();
System.out.println(name + " 正常释放锁");
} catch (Exception e) {
System.out.println(name + " : 在没有得到解锁的线程而中断解锁了,它终止了运行");
}
}
}
public static void main(String[] args) {
D lockTest = new D();
Thread ta = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
ta.setName("A:");
Thread tb = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
tb.setName("B:");
// // 启动线程t1
ta.start();
//Thread.sleep(10);
// 启动线程t2
tb.start();
//tb.interrupt();
}
}
}
lockInterruptibly()方法能够中断等待获取锁的线程:当两个线程同时通过lock.lockInterruptibly()获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程,而不是非得自己通过unlock正常释放锁。要执行doBussiness()中的代码首先需要得到锁lock,线程通过使用lockInterruptibly()方法获取锁,如上实例中,ta得到了锁,tb等待。在tb等待时,调用tb.interrupt(),中断tb的等待,而不需要通过tb的unlock释放。但是如果我们通过lock()方法获取锁的时候,我们通过interrupt()中断tb的等待,但是tb还会调用unlock方法来释放锁。这就是lockInterruptibly和lock方法的区别,从而突出了响应中断是可以避免死锁的。
方案二 可轮询锁
通过boolean tryLock()获取锁,如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false
方案三 定时锁
通过Boolean try(long time,TimeUnit unit)获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程
lock方法是一直等待获取锁,知道把锁等来
实例如上
4.公平锁和非公平锁
ReentrantLock支持公平锁和非公平锁
ReentrantLock默认支持非公平锁,我们都使用非公平锁
5.tryLock、lock和lockInterruptibly区别
tryLock: 若有可用锁,则获取锁并返回true,否则发挥false,不会有延迟或等待;tryLock还可以增加时间限制,如果超过了指定的时间还没有获得锁,则返回false
Lock: 若有可用锁,则获取锁并返回true,否则会一直等待知道获取可用锁。
LockInterruptibly: 在锁中断实惠抛出异常,lock不会
6.synchorized和ReentrantLock的异同:
共同点:
(1) 都用于控制多线程对共享对象的访问;
(2) 都是可重入锁
(3) 都保证了可见性和互斥性
不同点:
(1) R显示获取和释放锁,必须在finally控制块中进行解锁;S 隐式获取和释放锁
(2) R可响应中断,可轮回,为处理锁提供了更多的灵活性;
(3) R是API级别的,S是JVM级别的
(4) R可以定义公平锁
(5) 二者底层实现不同,S是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
(6) Lock是一个接口,而S是java中的关键字,S是由内置的语言实现的;
(7) 我们通过Lock可以知道有没有成功获取锁,通过S却无法知道;
(8) lock可以通过分别定义读写锁提高多个县城读操作的效率
(八)Semaphore
1.定义: 是一种基于技术的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体业务逻辑,业务逻辑在执行完成后释放该信号许可;在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,知道有其他许可信号被释放
public class D2 {
public static void main(String[] args) {
//1创建一个计数阈值为5的信号量,即只能有5个线程同时访问
Semaphore semp=new Semaphore(5);
try{
//2申请许可
semp.acquire();
try{
//3执行业务逻辑
}catch(Exception e){
e.printStackTrace();
}finally{
//4释放许可
semp.release();
}
}catch(Exception e){
e.printStackTrace();
}finally{
}
}
}
Semaphore对锁的申请和释放同ReentrantLock类似,通过acquire和release进行获取和释放资源,其中acquire和R总的lockInterruptibly方法一样,是一种可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对信号许可的申请;
Semaphore也实现了可轮询的锁请求,定时锁的功能,以及公平锁与非公平锁的机制;
Semaphore的释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成
(九)AtomicInteger
1.定义: 在多线程程序中,++i、i++,等运算法都不具有原子性,因此不是安全的线程操作。JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效。常见的还有AtomIcBoolean、AtomicLong等等类型不同的原子操作类,还可以通过AtomicReference讲一个对象的所有操作都转化为原子操作。Atomicxx的性能是S和R的若干倍
public class D3 implements Runnable{
//1定义一个原子的操作数。
static AtomicInteger safeCounter=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
D3 d3=new D3();
Thread t1=new Thread(d3);
Thread t2=new Thread(d3);
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(d3.safeCounter.get());//两个线程累计的和
}
@Override
public void run() {
for(int m=0;m<1000000;m++){
safeCounter.getAndIncrement();//2.对原子操作数执行自增操作
}
}
}
(十)可重入锁----公平锁和非公平锁
1.公平锁: 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程;
2.非公平锁: 指在分配锁时不考虑线程排队等到的情况,直接尝试获取锁,在获取不到锁时在排到队尾等待;
因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多
java中syn和Reen的lock方法都是非公平锁
(十一)可重入锁----读写锁
1.定义: 读写锁分为读锁和写锁两种,多个读锁不互斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的;
如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求你共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取该共享数据,则需要使用写锁
public class D4 {
private final Map<String,Object> cache=new HashMap<String,Object>();
private final ReentrantReadWriteLock rwLock=new ReentrantReadWriteLock();
private final Lock readLock=rwLock.readLock();//1.定义读锁
private final Lock writeLock=rwLock.writeLock();//1.定义写锁
//3 在读数据时加读锁
public Object get(String key){
readLock.lock();;
try{
return cache.get(key);
}finally{
readLock.unlock();
}
}
//4 在写数据时加写锁
public Object put(String key,Object value){
writeLock.lock();
try{
return cache.put(key,value);
}finally{
writeLock.unlock();
}
}
}
(十二)共享锁和独占锁
1.定义:
(1)独占锁: 也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现;
(2)共享锁: 允许多个线程同时获取该锁,并发访问共享资源。ReentrantWriteLock中的读锁为共享锁的实现。ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法;
独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程获取锁资源,限制了读操作的并发性;但是并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源;
(十三)重量级锁和轻量级锁、偏向锁
1.重量级锁:
基于操作系统的互斥量实现的锁,会导致进程在用户态和内核态之间切换,相对开销很大
(Syn在内部基于监视器锁实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此Syn属于重量级锁,重量级锁需要在用户态和核心态之间切换,所以Syn的运行效率不高)
为减少获取锁和释放锁带来的性能消耗及提高性能,引入了轻量级锁和偏向锁
2.轻量级锁:
是相对于重量级而言的。他的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以及提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。
3.偏向锁:
偏向锁用于在某个线程获取某个所之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向
他的主要目的就是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
在出现多线程竞争锁的情况是,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时
总结:轻量级锁用于提高线程交替执行同步块时的性能,偏向锁在某个线程交替执行同步块时进一步提高性能;因此,锁的状态总共有四种:无锁、偏向锁、轻量级锁和重量级锁
(十四)其他锁
1.分段锁
分段锁并非是一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,加锁进一步细化,以提高并发效率
分段锁详解
(ConcurrentHashMap在内部即使使用分段锁实现的)
2.同步锁和死锁
在有多个线程同时被阻塞时,他们之间相互等待对方释放锁资源,就会出现死锁;
为了避免死锁的出现,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。
(十五)锁优化
1.减少锁的持有时间:
减少锁的持有时间是指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间;
2.减少锁粒度:
指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用效率才会提高。减少锁粒度的最典型的案例就是ConcurrentHashMap中的分段锁;
3.锁分离:
指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),他根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,保证了安全,又提高了性能;
4.锁粗化:
为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分的太细,将会导致系统频繁获取和释放锁,反而影响性能的提升,在这种情况下,建议将关联性抢的锁操作集中起来处理,以提高整体系统的效率
5.锁消除:
消除不必要的锁操作,来提高系统性能