多个线程在操作一个资源类的时候,因为操作时间大致相同,异步执行,会造成数据的写入异常。锁就是解决这个问题的,锁的本质就是队列
我举个例子,你去吃饭,但不排队,都不排队,很多学生(多线程)一拥而上一个窗口(资源类),食堂大妈也不知道谁要的什么菜,就听到谁喊要什么菜,就打这个菜(线程不安全)
加锁就是指在窗口设置了一个拦截线,只有排队(队列)才能打饭,这样就保证了每个人打的菜才是自己想要的(线程安全)
自旋锁 : 是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被成功获取,直到获取到锁才会退出循环。
乐观锁 : 假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改
悲观锁 :假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁
独享锁(写) : 给资源加上写锁,拥有该锁的线程可以修改资源,其他线程不能再加锁(单写)
共享锁(读) : 给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁 (多读)
可重入锁 :线程拿到一把锁后,可以自由进入同一把锁所同步的代码
不可重入锁 :线程拿到一把锁后,不可以自由进入同一把锁所同步的代码
公平锁 :争抢锁的顺序,按照先来后到的顺序
非公平锁 :争抢锁的顺序,不按照先来后到的顺序
1、实现方式
①synchronized
关键字
②Java.util.concurrent
包中的lock
接口和ReentrantLock
实现类
Synchronized
是Java 并发编程中很重要的关键字,另外一个很重要的是 volatile
。Syncronized
的目的是一次只允许一个线程进入由他修饰的代码段,从而允许他们进行自我保护。
Synchronized
很像生活中的锁例子,进入由Synchronized
保护的代码区首先需要获取 Synchronized 这把锁,其他线程想要执行必须进行等待。Synchronized
锁住的代码区域执行完成后需要把锁归还,也就是释放锁,这样才能够让其他线程使用。
它提供了⼀种独占的加锁⽅式。Synchronized
的获取和释放锁由JVM
实现,⽤户不需要显示的释放锁,⾮常⽅便。然⽽synchronized
也有⼀定的局限性:
synchronized
可以锁代码块、方法和对象。
PermGen
(jdk1.8 则是 metaspace
),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程; private int number;
public synchronized void numIncrease(){
number++;
}
Synchronized
关键字,表示只能有一个线程进入某个代码段。 public void numDecrease(Object num){
synchronized (num){
number++;
}
}
synchronized
后面括号里是一对象,锁住的是所有以该对象为锁的代码块,此时线程获得的是对象锁。 public void test() {
synchronized (this) {
// ...
}
}
Lock
是 Java并发编程中很重要的一个**(Lock interface)接口**,它要比 synchronized
关键字更能直译"锁"的概念,Lock需要手动加锁和手动解锁,一般通过 lock.lock()
方法来进行加锁, 通过 lock.unlock()
方法进行解锁。Lock
还有更强大的功能,例如,它的 tryLock
方法可以非阻塞方式去拿锁。
Lock
接⼝⽐同步⽅法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以⽀持多个相关类的条件对象。
它的优势有:
与 Lock
关联密切的锁有 ReentrantLock
和 ReadWriteLock
。
ReetrantLock
实现了Lock接口,它是一个可重入锁,内部定义了公平锁与非公平锁。
可重⼊锁是指同⼀个线程可以多次获取同⼀把锁。ReentrantLock和synchronized都是可重⼊锁。
可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。
synchronized
是不可中断锁,⽽ReentrantLock
则提供了中断功能。公平锁是指多个线程同时尝试获取同⼀把锁时,获取锁的顺序按照线程达到的顺序。
⾮公平锁则允许线程“插队”。
synchronized
是⾮公平锁,⽽ReentrantLock
的默认实现是⾮公平锁,但是也可以设置为公平锁。
ReentrantLock
它是JDK 1.5之后提供的API层⾯的互斥锁,需要lock()和unlock()⽅法配合try/finally语句块来完成。等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
公平锁与⾮公平锁多个线程等待同⼀个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁⾮公平锁,
ReentrantLock
默认的构造函数是创建的⾮公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
ReadWriteLock
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWirteLock
实现了ReadWirteLock
接口,并未实现Lock
接口。
synchronized
是Java内置的一个关键字,Lock
是是一个Java接口synchronized
无法判断获取锁的状态,而lock
锁可以判断是否获取到了锁synchronized
回自动释放锁,而lock
必须手动释放锁。如果不释放就会变成死锁synchronized
线程1(获得锁,阻塞)线程2(傻傻地等待),lock
就不一定会等待synchronized
可重入锁,不可以中断的,非公平。lock
锁 可重入锁,可以判断锁,公平不公平自己可以设置synchronized
适合锁少量的代码同步问题 Lock
适合锁大量的同步代码类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个接口,有自己的实现类 |
释放的锁 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个获取锁的方式,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入、不可中断、非公平 | 可重入、可判断、可公平(两者皆可,默认非公平) |
性能 | 少量同步 | 大量同步 |
公平:
是按照通过CLH等待线程按照先来先得的规则,线程依次排队,公平的获取锁,是独占锁的一种。Java中,ReetrantLock中有一个Sync类型的成员变量sync,它的实例为FairSync类型的时候,ReetrantLock为公平锁。设置sync为FairSync类型,只需——Lock lock = new ReetrantLock(true)。
就相当于轮询,比如A线程要跑一个小时,B线程只需要两秒,加锁得到时候,A排在B前面,为了公平只能等A线程跑完在跑B线程
非公平:
是当线程要获取锁时,它会无视CLH等待队列而直接获取锁。ReetrantLock默认为非公平锁,或——Lock lock = new ReetrantLock(false)。
可以想成看权重再加锁,先让B跑在跑A
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
说到这里,还是想提一下这2中机制的具体区别:
==synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。==独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而==Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。==乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
我也只是了解到这一步,具体到CPU的算法如果感兴趣的读者还可以在查阅下,如果有更好的解释也可以给我留言,我也学习下。
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
参考文章:深入研究 Java Synchronize 和 Lock 的区别与用法_蓝天下的牧童的博客-CSDN博客_java lock和synchronized区别