Java多线程编程(二)

本文主要分为两大章,线程间通信和Lock锁技术。
线程间通信主要通过wait和notify方法来实现。但effective java中推荐不要使用这两个方法,因为并发包的功能可以满足大部分的需求,而且实现起来比它们要简单的多。此外,介绍了中断、管道通信,以及深入学习了ThreadLocal的源码。
Lock锁是一个很重要的特性,相当于把synchronized解耦,提高了锁的灵活性。此外,还包括了Condition、ReentrantLocl和ReentrantReadWriteLock类的相关特性。

线程间通信

使用线程的场景很多,线程间通信是线程互相协作的必要技术。

wait()和notify()

wait()方法作用是使当前执行代码的线程进行等待。该方法是object方法,将当前线程放入“预执行队列”中,并在wait()处停止,直到接到通知或中断为止。调用wait()前,线程必须 要获得该对象的对象级别锁 —— 只能在同步方法或同步代码块 调用此方法。执行方法后,当前线程释放锁。如果调用wait()时没有得到合适的锁,就会抛出IllegalMonitorStateException(RuntimeException)异常。
notify()方法作用是 对象 通知等待的线程继续运行。若有多个线程等待,线程规划器随机挑选一个wait线程唤醒。与wait()类似,也必须持有对象的锁。需要注意的是,notify()方法执行后并未立刻释放锁,而且等待线程也不是立即被唤醒,而是在当前线程执行完毕,也就是退出synchronized代码块后当前线程才会释放锁,wait线程才可以获取锁。
总而言之,wait使线程停止运行,notify使停止的线程继续运行。
一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class _3SimpleWaitNotify {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread a = new MyThreadA(o);
a.start();
Thread b = new MyThreadB(o);
Thread.sleep(2000);
b.start();
}
static class MyThreadA extends Thread {
private Object o;
public MyThreadA(Object o) {
this.o = o;
}
@Override
public void run() {
try {
synchronized (o) {
System.out.println("start wait time is " + System.currentTimeMillis());
o.wait();
System.out.println("end wait time is " + System.currentTimeMillis());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class MyThreadB extends Thread {
private Object o;
public MyThreadB(Object o) {
this.o = o;
}
@Override
public void run() {
synchronized (o) {
System.out.println("start notify time is " + System.currentTimeMillis());
o.notify();
System.out.println("end notify time is " + System.currentTimeMillis());
}
}
}
}

interrupt()和wait()

当线程正在等待,调用线程对象的interrupt()方法时会出现InterruptedException。
总结一下线程释放锁的3种情况:

  1. 执行完同步代码块就会释放对象的锁;
  2. 执行代码块时遇到异常而线程终止,也会释放锁;
  3. 执行wait()方法,线程释放锁后进入线程等待池等待被唤醒。

wait(long timeout)方法

等待timeout毫秒时间内是否有线程对锁进行唤醒,超时自动唤醒。
观察源码,wait()其实就是wait(0)。

生产者消费者

生产者消费者是经典多线程问题。如果处理不好生产者和消费者的关系,很容易使系统进入假死状态,即两者都在无限等待,谁也不能执行。可以灵活的使用操作栈来执行业务逻辑。使用wait()和notify()方法可以写出合适的例子。但是这种方式对编程水平要求很高,需要避免假死。可以使用并发包里合适的类来编程。

管道线程通信

管道流(pipeStream)是一种特殊的流,用于在不同线程间传送数据。一个线程发送输入管道,一个线程在管道另一端接受数据。通过使用管道,可以实现不同线程间的通信,而无须借助类似临时文件之类的数据结构。
有两种管道,分别对应字节流和字符流:

  • PipedInputStream & PipedOutputStream
  • PipedReader & PipedWriter

管道通信IO流与常规IO流用法基本一样。下面是一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class _7TestPipe {
public static void main(String[] args) throws Exception {
WriteData writeData = new WriteData();
ReadData readData = new ReadData();
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream();
in.connect(out);
Thread readThread = new MyThreadA(writeData, out);
Thread writeThread = new MyThreadB(readData, in);
readThread.start();
Thread.sleep(1000);
writeThread.start();
}
static class WriteData {
public void writeMethod (PipedOutputStream out) {
try {
System.out.println("write:");
for (int i = 0; i < 300;i++){
String outData = "" + (i + 1);
out.write(outData.getBytes());
System.out.println(outData);
}
System.out.println();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class ReadData {
public void readMethod (PipedInputStream in) {
try {
System.out.println("read:");
byte[] byteArray = new byte[40];
int readLength = in.read(byteArray);
while (readLength != -1) {
String str = new String(byteArray, 0, readLength);
System.out.println(str);
readLength = in.read(byteArray);
}
System.out.println();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class MyThreadA extends Thread {
private WriteData write;
private PipedOutputStream out;
public MyThreadA(WriteData write, PipedOutputStream out) {
super();
this.write = write;
this.out = out;
}
@Override
public void run() {
write.writeMethod(out);
}
}
static class MyThreadB extends Thread {
private ReadData read;
private PipedInputStream in;
public MyThreadB(ReadData read, PipedInputStream in) {
super();
this.read = read;
this.in = in;
}
@Override
public void run() {
read.readMethod(in);
}
}
}

如果要使用字符流,只需将InputStream变成Reader,OutputStream变成Writer。

join()

方法join()的作用是使所述线程对象x能正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,直到x销毁后再执行线程z后面的代码。join使线程排队运行,类似同步的效果。join与synchronized的区别是:join内部使用wait()进行等待,而synchronized使用“对象监视器”原理进行同步。
join()的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

由此可见,join(long millis)方法语义是等待当前线程结束后或者等待millis时间后停止等待。如果当线程join()时被中断会抛出InterruptedException。
join()和sleep()的区别:

  1. join()最多等待xx时间,如果当前线程执行完毕,那么会立即执行;sleep()则是死等xx时间。
  2. join()内部调用wait()方法,释放锁,而sleep()不会释放锁。

ThreadLocal类

ThreadLocal类解决的是变量在不同线程间的隔离性,也就是不同线程永远自己的值,不同线程可以将值放入ThreadLocal类中保存。可以将其比作全局存放数据的盒子,盒子中可以存储每个线程的私有数据。

ThreadLocal类的使用

ThreadLocal用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
Thread a = new MyThreadA();
Thread b = new MyThreadB();
a.start();
b.start();
for (int i = 0; i < 50; i++) {
TLUtil.tl.set("Main" + (i + 1));
System.out.println("Main get value=" + TLUtil.tl.get());
Thread.sleep(200);
}
}
//static ThreadLocal variable
static class TLUtil {
public static ThreadLocal<String> tl = new ThreadLocal<String>();
}
static class MyThreadA extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
TLUtil.tl.set("ThreadA" + (i + 1));
System.out.println("ThreadA getValue=" + TLUtil.tl.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyThreadB extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
TLUtil.tl.set("ThreadB" + (i + 1));
System.out.println("ThreadB getValue=" + TLUtil.tl.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

将ThreadLocal变量放在静态工厂中,供别的程序存取。通过这个例子可以看出,ThreadLocal变量确实是在每一个线程中有自己的副本。

深入理解ThreadLocal类

ThreadLocal类是java.lang包中的类,足以表示他的重要性了。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。这些场景创建一次连接效率很低,创建副本明显比内部新建要好得多。
下面从源码来学习ThreadLocal。
首先是ThreadLocal的功能方法:

1
2
3
4
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。
来看一下ThreadLocal是如何为每一个线程创建副本。
get方法的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

首先取得当前线程的引用,然后通过getMap(t)方法取得一个ThreadLocalMap内部类对象map。接着,取到键值对, 注意这里获取键值对传进去的是this,而不是当前线程t 。若获取成功,返回value。如果map为空,调用setInitialValue()方法返回value。接着沿着脉络向下分析getMap(t)的实现:

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

getMap()方法返回的是当前线程的threadLocals变量。顺藤摸瓜:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

实际上指向ThreadLocal的内部类ThreadLocalMap。接下来看一看ThreadLocalMap的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/

static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
...

内部类ThreadLocalMap中还嵌套内部类Entry,而且继承自WeakReference,使用ThreadLocal作为键,Object作为值。
继续看setInitialValue()方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

首先在initialValue()中取得设置的起始值,若不重写initialValue()方法,那么就会返回null。如果键不为空,那么设置键值对(this, value),并返回value;否则,调用createMap(t, value)方法。下面来看createMap(t, value)的实现:

1
2
3
4
5
6
7
8
9
10
11
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

至此,我们已经可以明白ThreadLocal类是如何为每个线程创建副本:

  • Thread类有一个ThreadLocalMap类型变量threadLocals, 实际的变量副本正是在此保存
  • 初始Thread的threadLocals为null,调用get()或set()方法后就会对其初始化,以当前ThreadLocal为键,以initialValue()返回值为值。
  • 当前线程可以通过get()方法来获取当前线程的值。

总结一下:

  1. 实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
  2. threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量.
  3. 在进行get之前,必须先set,否则会报空指针异常,如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

ThreadLocal部分参考Java并发编程:深入剖析ThreadLocal

Lock锁

可以使用synchronized关键字实现线程的同步互斥,但java.util.concurrent.locks包中的类也能达到同样的效果,并且在扩展功能上也更加强大,具有嗅探锁定、多路分支通知等功能,而且更加灵活。例如,可以设定超时自动释放锁,“读”不加锁而“写”上锁这样的逻辑功能。但需要注意:

  1. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  2. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

Lock分析

查看源码知,Lock是一个接口:

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

这几个方法中,lock()、lockInterruptibly()、tryLock()、tryLock(long time, TimeUnit unit)是获取锁,unLock()方法是用来释放锁的。

  1. lock()使用得最多的一个方法, 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁。一般用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Lock lock = ...;
    lock.lock();
    try{
    //处理任务
    }catch(Exception ex){
    //异常
    }finally{
    lock.unlock(); //释放锁
    }
  2. tryLock(),尝试获取锁,如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false。这个方法不会等待,直接返回。

  3. tryLock(long timeout,TimeUnit unit),如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false。一般tryLock方法是这样使用的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Lock lock = ...;
    if(lock.tryLock()) {
    try{
    //处理任务
    }catch(Exception ex){

    }finally{
    lock.unlock(); //释放锁
    }
    }else {
    //如果不能获取锁,则直接做其他事情
    }
  4. lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够 响应中断 ,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

ReetrantLock重入锁

ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了锁投票,定时锁等候和中断锁等候。线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,如果使用synchronized,如果A不释放,B将一直等下去,不能被中断。如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。
需要 注意 的是,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就 必须将unLock()放到finally{}中
关于性能的问题,在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。但是,Lock锁需要手动管理,对编程技巧要求较高。我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
Lock()的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        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();
        }
    }
}

需要注意的是,必须在类中定义锁对象,这样在方法中才能正确执行锁操作。因为如果在方法中定义,那么每个线程自己锁住自己,没有实现我们想要的功能。
tryLock()的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        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()+"获取锁失败");
        }
    }
}

lockInterruptibly()响应中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {
            System.out.println(thread.getName()+"得到了锁");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}

Condition来控制线程等待or唤醒

类似于wait()和notify(),lock也有相应的机制来控制线程。wait()和notify()分别对应await()和signal()。Condition用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class _3TestCondition {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
final _3TestCondition tc = new _3TestCondition();
new Thread(){
public void run() {
tc.await();
};
}.start();
Thread.sleep(1000);
new Thread() {
@Override
public void run() {
tc.singal();
}
}.start();
}
public void await() {
try {
lock.lock();
System.out.println("begin wait");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("stop wait");
lock.unlock();
}
}
public void singal() {
try {
lock.lock();
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("send signal");
lock.unlock();
}
}
}

公平锁

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

1
ReentrantLock lock = new ReentrantLock(true);

此外,ReentrantLock还有一些别的工具方法:

1
2
3
4
5
6
7
8
getHoldCount()			//查询当前线程保持此锁的个数,也就是调用lock()的次数
hasQueuedThread(Thread thread)//是否有线程正在等待此锁定
getQueueLength() //返回正在等待此锁定线程的个数
isFair() //判断锁是否是公平锁
isHeldByCurrentThread() //是否被当前线程锁定
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁

ReentrantReadWriteLock类

ReentrantLock可以说是synchronize的解耦,完全互斥排他。这样做可以保持线程安全性,但效率非常低下。ReentrantReadWriteLock类进一步解耦,提高效率。读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作锁,也叫排它锁。共享锁不互斥,而排它锁互斥。
需要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class _4ReentrentRWL {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String args[]) {
final _4ReentrentRWL rrwl = new _4ReentrentRWL();
new Thread() {
@Override
public void run() {
rrwl.get();
}
}.start();
new Thread() {
@Override
public void run() {
rrwl.get();
}
}.start();
}
public void get() {
// rwl.readLock().lock();
rwl.writeLock().lock();
try {
System.out.println("go in get()");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("go out of get()");
// rwl.readLock().unlock();
rwl.writeLock().unlock();
}
}

}

小结

总结来说,Lock和synchronized有以下几点不同:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

Lock与synchronized 的区别
Java并发编程:Lock