1、线程同步的概念
同步的概念是在发出一个功能调用后,在没有得到结果之前,该调用就不返回,也就是事情要一件一件做,等前一件做完了才能做下一件事。线程同步指的是同一时刻只有一个线程能进入临界区(访问共享数据的代码块),当前线程执行完成,并且释放了对象锁,其他等待进入临界区的线程才能执行。
与同步相对的概念是异步,异步是指在发出一个功能调用后,被调用的对象不能立刻返回结果,在没有得到返回结果之前,调用者还可以执行别的操作,被调用者执行完成后,通过状态、通知和回调来通知调用者。异步线程指的是,当程序要执行一个比较耗时的任务时(IO操作、网络请求),程序会开启一个子线程执行这个耗时的任务,主线程继续执行其他的操作,等子线程执行完成后,再通知主线程,异步操作能提高程序的效率。
2、线程同步-synchronized
synchronized是Java中的一个关键字,用于实现线程同步,synchronized常用来:
2.1、修饰代码块
synchronized(this) { },作用的范围是{ }括起来的代码,作用的对象是当前对象。一个线程在访问当前对象的synchronized代码块时,其他线程会被阻塞。
public class SyncThread implements Runnable {
private int count;
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
}
}
public int getCount() {
return count;
}
}
调用:
public static void main(String[] args) {
SyncThread syncThread = new SyncThread();
new Thread(syncThread,"thread1").start();
new Thread(syncThread,"thread2").start();
}
结果:
thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9
两个线程thread1和thread2同时访问同一个对象(syncThread)的synchronized修饰的代码块,同一时刻只能有一个线程进入,另一个线程受阻塞,被阻塞的线程必须等待当前线程执行完synchronized代码块以后才能执行该代码块。我们把调用代码改成如下形式,再测试一下:
public static void main(String[] args) {
new Thread(new SyncThread(),"thread1").start();
new Thread(new SyncThread(),"thread2").start();
}
结果:
thread2:0
thread1:0
thread2:1
thread1:1
thread2:2
thread1:2
thread2:3
thread1:3
thread2:4
thread1:4
这时创建了两个SyncThread对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized(this) { },而线程thread2执行的是syncThread2对象中的synchronized(this) { };我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
此外,当一个线程访问对象的synchronized代码块时,另一个线程仍然可以访问该对象中的非synchronized代码块,如下:
public class SyncThread implements Runnable {
private int count;
public void countAdd() {
synchronized(this) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":countAdd:" + (count++));
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void printCount() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":printCount:" + count);
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("thread1")) {
countAdd();
} else if (threadName.equals("thread2")) {
printCount();
}
}
}
调用:
public static void main(String[] args) {
SyncThread syncThread = new SyncThread();
new Thread(syncThread,"thread1").start();
new Thread(syncThread,"thread2").start();
}
结果:
thread1:countAdd:0
thread2:printCount:1
thread1:countAdd:1
thread2:printCount:2
thread2:printCount:2
thread1:countAdd:2
thread1:countAdd:3
thread2:printCount:4
thread2:printCount:4
thread1:countAdd:4
由测试结果可以看出,一个线程在访问对象的synchronized代码时,其他线程可以同时访问该对象的非同步代码。
2.2、synchronized修饰成员方法
public synchronized void method(){ },这种情况和修饰代码块类似,只是修饰范围是整个方法,作用的对象依然是当前对象。上面的例子可以改为下面的代码,效果是一样的:
public synchronized void countAdd() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":countAdd:" + (count++));
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
也就是public synchronized void method(){ }与public void method(){synchronized(this) { } }作用是一样的。
同步方法时,synchronized关键字被不能继承,如果父类中的方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上synchronized关键字才可以实现子类的相应方法是同步的,或者在子类方法中用super关键字调用父类同步的方法,子类的方法也就相当于同步了。
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
2.3、Synchronized修饰静态方法
public synchronized static void method() { },由于静态方法是属于类的,所有synchronized修饰的静态方法锁定的是该类的所有对象。
public class SyncThread implements Runnable {
private static int count;
public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
method();
}
}
调用:
public static void main(String[] args) {
new Thread(new SyncThread(),"thread1").start();
new Thread(new SyncThread(),"thread2").start();
}
结果:
thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁,当thread1进入method后,thread2再进入method时会被阻塞。
2.4、synchronized作用于类
synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁,形式如下:
class ClassName {
public void method() {
synchronized(ClassName.class) {
}
}
}
将3中的例子改写,将synchronized修饰静态方法改成修饰类,效果是一样的:
public class SyncThread implements Runnable {
private static int count;
public void method() {
synchronized(SyncThread.class) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void run() {
method();
}
}
调用:
public static void main(String[] args) {
new Thread(new SyncThread(),"thread1").start();
new Thread(new SyncThread(),"thread2").start();
}
结果:
thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9
synchronized修饰一个类,这种情况是给这个类加锁,加锁的类的所有对象用的是同一把锁。一个线程进入了加锁类任意一个实例的synchronized修饰的代码,其他线程就会阻塞,不能同时进入这个类其他实例的这段同步的代码。
2.5、synchronized修饰一个对象
这种情况是给这个对象加锁,一个线程拿到这个对象的锁之后,就可以访问加锁的代码,其他的线程想要同时访问使用这个对象锁锁住的代码则等待,当前线程释放对象的锁之后,其他线程才能获得这个对象的锁进而执行被锁住的代码。
/**
* 银行账户类
*/
class Account {
String name;
float amount;
public Account(String name, float amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(float amt) {
amount += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(float amt) {
amount -= amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public float getBalance() {
return amount;
}
}
/**
* 账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
public void run() {
synchronized (account) {
account.deposit(500);
account.withdraw(500);
System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
}
}
}
调用:
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}
结果:
Thread3:10000.0
Thread2:10000.0
Thread1:10000.0
Thread4:10000.0
Thread0:10000.0
在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束,也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
下面的例子,两个线程会同时执行,thread1拿到的是Object对象的锁,thread2拿到的是当前对象的锁,这是两把不同的锁,两把锁是互不干扰的,不形成互斥,所以两个线程会同时执行。
public class Main {
Object obj = new Object();
public void method1() {
synchronized(obj) {
for(int i=0;i<5;i++) {
System.out.println("method1:"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void method2() {
synchronized(this) {
for(int i=0;i<5;i++) {
System.out.println("method2:"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
final Main mainClass = new Main();
new Thread(new Runnable() {
public void run() {
mainClass.method1();
}
}).start();
new Thread(new Runnable() {
public void run() {
mainClass.method2();
}
}).start();
}
}
结果:
method2:0
method1:0
method1:1
method2:1
method2:2
method1:2
method2:3
method1:3
method2:4
method1:4
线上case举例:
2.6、synchronized底层实现原理
2.6.1、synchronized代码块底层原理
举例如下:
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代码块
synchronized (this){
i++;
}
}
}
通过javap -verbose反编译后得到字节码如下:
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此处,进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此处,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此处,退出同步方法
22: aload_2
23: athrow
24: return
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指向同步代码块的结束位置。当执行monitorenter指令时,当前线程将试图获取monitor对象的所有权,如果monitor对象的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有了monitor 的所有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1。倘若其他线程已经拥有monitor 对象的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor并设置计数器值为0 ,其他线程将有机会持有 monitor 。
值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
2.6.2、synchronized方法底层原理
举例如下:
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
javap反编译后得到的字节码:
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
2.7、Java虚拟机对synchronized的优化
Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方从JVM层面对synchronized进行了较大优化。Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。
2.7.1、偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
2.7.2、轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
2.7.3、自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
2.7.4、消除锁
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,不会被其他线程所使用,因此StringBuffer不可能存在多个线程同时访问的情形,JVM会自动将其锁消除。
2.8、synchronized总结
(1)synchronized作用于代码块、成员方法时取得的是当前对象的锁;synchronized作用于某个对象时,取得的是这个对象的锁;synchronized作用于静态方法、类时,取得的是这个类的锁,这个类的所有对象共用同一把锁。
(2)实现同步需要很大的系统开销,甚至可能造成死锁,应该尽量避免无谓的同步控制。
3、线程同步-lock
首先我们来说一说为什么会出现Lock接口,之所以出现Lock接口,是因为synchronized同步方式存在某些不足。
1、如果一个代码块被synchronized修饰,当一个线程获取了对应的锁,其他线程只能一直等待获取锁的线程释放锁,而获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了代码块,然后线程释放对锁的占有
2)线程执行发生异常,此时JVM会自动释放线程占有的锁
如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便就只能一直等待下去,这样会影响程序执行效率,因此就需要有一种机制可以不让等待的线程无期限地等待下去,、Lock接口可以实现,比如让线程只等待一定的时间或者让等待的线程能够响应中断。
2、当有多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但是读操作和读操作不会发生冲突。但是采用synchronized来实现同步的话,就会导致一个问题,如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程可以同时进行读操作,通过ReadWriteLock接口就可以办到。
3、通过Lock接口可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。
下面我们就来分析一下java.util.concurrent.locks包中常用的类和接口。
3.1、Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock接口中lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的。采用Lock接口同步线程,在发生异常时,JVM不会自动释放锁,必须由程序员主动去释放锁,因此使用Lock接口必须在try{ }catch{ }块中进行,并且将释放锁的操作放在finally{ }块中进行,以保证锁一定被被释放,防止死锁的发生。
3.1.1、lock()方法
用来获取锁,如果锁已被其他线程获取,则进行等待,通常使用lock()方法来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
3.1.2、tryLock()
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
3.1.3、lockInterruptibly()
lockInterruptibly()当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只能等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。因此当通过lockInterruptibly()方法获取某个锁时,只有等待锁的线程是可以响应中断的,线程获取了锁之后,是不会被interrupt()方法中断的,而用synchronized修饰的话,当线程等待锁时是无法被中断的,只能一直等待下去。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
3.2、ReentrantLock
ReentrantLock(可重入锁)是唯一实现了Lock接口的类,lock()方法使用如下:
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方,lock声明为成员变量
public static void main(String[] args) {
final Main test = new Main();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
结果:
Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁
tryLock()方法使用如下:
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方,lock声明为成员变量
public static void main(String[] args) {
final Main test = new Main();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"获取锁失败");
}
}
}
结果:
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
lockInterruptibly()使用如下:
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Main main = new Main();
MyThread thread1 = new MyThread(main);
MyThread thread2 = new MyThread(main);
thread1.start();
thread2.start();
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly();
try {
System.out.println(thread.getName()+"得到了锁");
}catch(Exception e) {
e.printStackTrace();
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private Main test = null;
public MyThread(Main test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
结果:
Thread-1被中断
Thread-0得到了锁
Thread-0执行finally
Thread-0释放了锁
Thread-0、Thread-1同时获取锁,但是Thread-0获取到了,接着执行任务,finally中释放锁;Thread-1未获取到锁,进入等待状态,随后被中断。
3.3、ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁,将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
3.4、ReentrantReadWriteLock
假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Main test = new Main();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public synchronized void get(Thread thread) {
for(int i=0;i<5;i++) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
}
}
结果:
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0读操作完毕
两个线程同时调用一个对象的synchronized方法,只能有一个线程拿到对象锁,另一个线程被阻塞。改成读写锁的话:
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Main test = new Main();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
for(int i=0;i<5;i++) {
Thread.currentThread().sleep(10);
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} catch(Exception e) {
}finally {
System.out.println(thread.getName()+"释放了锁");
rwl.readLock().unlock();
}
}
}
结果:
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0读操作完毕
Thread-0释放了锁
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-1释放了锁
Thread1和Thread2在同时进行读操作,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
4、synchronized、lock比较
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
5、锁的相关概念
5.1、可重入锁
如果锁具备可重入性,则称作为可重入锁,像synchronized和ReentrantLock都是可重入锁。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁,但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样线程A就会一直等待永远没法获取到锁,而由于synchronized和Lock都具备可重入性,所以这种情况不会发生。
5.2、可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。synchronized不是可中断锁,Lock是可中断锁,通过lockInterruptibly()实现。
5.3、公平锁
公平锁即尽量以请求锁的顺序来获取锁,比如有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样可能会导致某些线程永远获取不到锁。synchronized是非公平锁,它无法保证等待的线程获取锁的顺序;ReentrantLock和ReentrantReadWriteLock默认情况下是非公平锁,但是可以设置为公平锁。
5.4、乐观锁与悲观锁
5.4.1、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
5.4.2、乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁常见的两种实现方式:
1. 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被更新时version值会加一。线程A读取数据的同时也会读取version值,再操作完数据后version值加一,此时如果version值大于数据库中的version值,说明没有其它线程更新该数据,这是才能更新数据;如果此时的version值等于或者小于数据库中的version值,说明该数据已经被其他线程更新过一次或者多次,这时线程A应该放弃此次操作,应再次读取数据以及version值,直到更新成功。
举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果。
2.cas算法
CAS(Compare And Swap)就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。举个例子,内存值V、期望值A、更新值B,当V == A的时候将V更新为B
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
6、Java死锁
6.1、什么是死锁
两个或两个以上的线程情景下,线程A持有锁资源A,但是还想要资源B,于是请求B锁,线程B持有锁资源B,但是还想要资源A,于是请求A锁。两者互不释放锁,又想获得对方资源,导致两个线程永久阻塞的现象。
实例如下:
package com.sukang.sort;
public class ThreadDeadlock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
Thread t2 = new Thread(new SyncThread(obj2, obj1), "t2");
t1.start();
t2.start();
}
}
class SyncThread implements Runnable{
private Object obj1;
private Object obj2;
public SyncThread(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " acquiring lock on " + obj1);
synchronized (obj1) {
System.out.println(name + " acquired lock on " + obj1);
work();
System.out.println(name + " acquiring lock on " + obj2);
synchronized (obj2) {
System.out.println(name + " acquired lock on "+obj2);
work();
}
System.out.println(name + " released lock on "+obj2);
}
System.out.println(name + " released lock on "+obj1);
System.out.println(name + " finished execution.");
}
private void work(){
try {
Thread.sleep(3000);
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
}
每个线程中都能成功获取第一个对象的锁,最终却阻塞在了获取第二个对象,造成了线程之间的互相等待,形成了死锁。
6.2、死锁产生的4个必要条件
1、互斥条件:一个资源每次只能被一个线程使用。
2、请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
4、循环等待条件:若干线程之间形成循环等待资源关系。
6.3、如何预防死锁
1)尽量避免锁的嵌套使用,如必须使用多个锁的话,锁的顺序要一致。
//可能发生顺序死锁的代码
class StaticLockOrderDeadLock {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void a() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("function a");
}
}
}
public void b() {
synchronized (lockB) {
synchronized (lockA) {
System.out.println("function b");
}
}
}
}
应改为:所有需要多个锁的线程,都要以相同的顺序来获得锁。
//正确的代码
class StaticLockOrderDeadLock {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void a() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("function a");
}
}
}
public void b() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("function b");
}
}
}
}
(2)持有锁的方法避免调用外部对象的方法,可能外部对象也持有你的锁,会造成死锁。此种场景在实际开发中更为常见。
6.4、如何排查死锁
获取Java线程dump文件,dump文件记录了线程在jvm中的执行信息,可以看成是线程活动的日志。
jstack:jdk自带的工具jstack通过它,可以生成应用程序的线程转储文件,只需要两步即可完成:
1>找到应用程序的进程ID
ps -eaf | grep java
2> 输出线程转储信息到文件或控制台
jstack PID >> mydumps.tdump
jstack PID
jcmd: jdk8中介绍了jcmd工具,在jdk8及以上环境下,可以使用此命令生成线程转储文件。