Java线程同步-synchronized与lock

时间:2022-08-06 19:29:03

一、线程同步的概念、为什么需要线程同步

        同步的概念是在发出一个功能调用后,在没有得到结果之前,该调用就不返回,也就是事情要一件一件做,等前一件做完了才能做下一件事。线程同步指的是同一时刻只有一个线程能进入临界区(访问共享数据的代码块),当前线程执行完成,并且释放了对象锁,其他等待进入临界区的线程才能执行。

        与同步相对的概念是异步,异步是指在发出一个功能调用后,被调用的对象不能立刻返回结果,在没有得到返回结果之前,调用者还可以执行别的操作,被调用者执行完成后,通过状态、通知和回调来通知调用者。异步线程指的是,当程序要执行一个比较耗时的任务时(IO操作、网络请求),程序会开启一个子线程执行这个耗时的任务,主线程继续执行其他的操作,等子线程执行完成后,再通知主线程,异步操作能提高程序的效率。

那么为什么会出现线程同步呢?请看之前总结的帖子:http://blog.csdn.net/qq_29078329/article/details/73611110


二、线程同步-synchronized

synchronized是Java中的一个关键字,用于实现线程同步,synchronized常用来:

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、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();   }} 


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时会被阻塞。


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修饰的代码,其他线程就会阻塞,不能同时进入这个类其他实例的这段同步的代码。


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

总结:

(1)synchronized作用于代码块、成员方法时取得的是当前对象的锁;synchronized作用于某个对象时,取得的是这个对象的锁;synchronized作用于静态方法、类时,取得的是这个类的锁,这个类的所有对象共用同一把锁。  
(2)实现同步需要很大的系统开销,甚至可能造成死锁,应该尽量避免无谓的同步控制。

三、线程同步-lock

首先我们来说一说为什么会出现Lock接口,之所以出现Lock接口,是因为synchronized同步方式存在某些不足。

1、如果一个代码块被synchronized修饰,当一个线程获取了对应的锁,其他线程只能一直等待获取锁的线程释放锁,而获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了代码块,然后线程释放对锁的占有

2)线程执行发生异常,此时JVM会自动释放线程占有的锁

    如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便就只能一直等待下去,这样会影响程序执行效率,因此就需要有一种机制可以不让等待的线程无期限地等待下去,、Lock接口可以实现,比如让线程只等待一定的时间或者让等待的线程能够响应中断。

2、当有多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但是读操作和读操作不会发生冲突。但是采用synchronized来实现同步的话,就会导致一个问题,如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程可以同时进行读操作,通过ReadWriteLock接口就可以办到。

3、通过Lock接口可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。

下面我们就来分析一下java.util.concurrent.locks包中常用的类和接口。

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{ }块中进行,以保证锁一定被被释放,防止死锁的发生。


1)lock()方法用来获取锁,如果锁已被其他线程获取,则进行等待,通常使用lock()方法来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){

}finally{
lock.unlock(); //释放锁
}
2)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)当两个线程同时通过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();
}
}

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、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接口。


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在同时进行读操作,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。


四、synchronized、lock比较

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。


五、锁的相关概念

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都具备可重入性,所以这种情况不会发生。

2、可中断锁

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。synchronized不是可中断锁,Lock是可中断锁,通过lockInterruptibly()实现。

3、公平锁

公平锁即尽量以请求锁的顺序来获取锁,比如有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样可能会导致某些线程永远获取不到锁。synchronized是非公平锁,它无法保证等待的线程获取锁的顺序;ReentrantLock和ReentrantReadWriteLock默认情况下是非公平锁,但是可以设置为公平锁。