使用Lock
本文主要讲述如何解决非线程安全问题,感谢java多线程核心编程一书,为本系列文章提供参考借鉴
一、使用ReentrantLock
1.使用ReentrantLock方法:
- lock():锁定
- unlock():解除锁定
- int getHoldCount():产讯当前线程保持此锁定的个数,也就是调用lock()方法的次数
- int getQueueLength():返回正在等待获取此锁定的线程估计数,例如:8个线程,3个线程调用了await()方法吧,那么在调用此方法后返回的值是5,那么就说嘛有5个线程同时在等待lock的释放。
- int getWaitQueueLength():顾名思义,其作用是返回等待与此锁定相关的给定条件Condition的线程估计数。例如:8个线程,每个线程都调用了同一个Condition的await()方法,则调用此方法返回的值是8.
- boolean hasQueuedThread():查询指定线程是否正在等待获取此锁定。ReentrantLock.hasQueuedThread(thread);
- boolean hasWaiters():查询是否有线程正在等待与此锁定有关的Condition条件。
- boolean isFair():判断是否是公平锁
- boolean isHeldByCurrentThread():查询当前线程是否保持此锁定。
- boolean isLocked():查询此锁定是否由任意线程保持。
- void lockInterruptibly():如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。
- boolean tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。
- boolean tryLock(long timeout,TimeUnit unit):如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。
2.使用ReentrantLock实现同步效果:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
public void printReentrantLock() {
try {
lock.lock();//获取锁
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + ",i=" + i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
public static void main(String[] args) {
final MyServer myServer = new MyServer();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
myServer.printReentrantLock();
}
});
thread.setName(String.valueOf(i));
thread.start();
}
}
}
运行结果为:
由结果分析知:当前线程获取锁对象后其他线程呈阻塞状态,直到当前线程打印完毕释放锁,其他线程才能获取到锁进行打印。
这个主要进行单个方法同一个锁多线程调用的同步效果,下面来看一下多个线程交叉调用不同方法同一个锁的例子:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
public void methodA() {
try {
lock.lock();//获取锁
System.out.println("method A begin ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
methodB();//调用methodB
Thread.sleep(5000);
System.out.println("method A end ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void methodB() {
try {
lock.lock();//获取锁
System.out.println("method B begin ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("method B end ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
String[] names = {"A", "AA", "B", "BB"};
final MyServer myServer = new MyServer();
Thread[] threads = new Thread[4];
for (int i = 0; i < 2; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
myServer.methodA();
}
});
}
for (int i = 2; i < 4; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
myServer.methodB();
}
});
}
for (int i = 0; i < 4; i++) {
threads[i].setName(names[i]);
threads[i].start();
}
}
结果如下:
分析结果:在线程A获取锁时其他线程呈阻塞状态直至线程A执行完毕且释放锁,其他线程才可以获取到锁,同时在线程A获取锁执行过程中又成功调用了methodB,这些说明reentrantlock和synchronized关键字一样线程之间是同步顺序执行的,也都具有锁重入特性。
二、使用Condition实现等待/通知
关键字synchronized于wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock也可以试想同样的功能,但需要借助于Condition对象。相对于notify和notifyAll,Condition具有跟高的灵活性,在使用notify和notifyAll时,被通知的线程是由JVM随机选择的,而ReentrantLock结合Condition类是可以“选择性通知的”。
1.Condition中通知/等待方法
- await():相当于Object类中的wait()方法
- await(long time,TimeUtil unit):相当于Object类中的wait(long timeout)方法
- signal():相当于Object类中的notify()方法
- signalAll():相当于Object类中的notifyAll()方法
2.简单使用:
注意:调用condition.await()之前需要执行lock.lock()来获取同步监视器。
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println(" await 时间为 "+ System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println(" signal 时间为 "+System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
new Thread(new Runnable() {
public void run() {
myServer.await();
}
}).start();
Thread.sleep(3000);
myServer.signal();
}
}
运行结果为:
3.使用多个Condition实现”选择性“通知线程
代码如下:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
@SuppressWarnings("all")
public void awaitA() {
try {
lock.lock();
System.out.println("begin await A 时间为 "+ System.currentTimeMillis());
conditionA.await();
System.out.println("end await A 时间为 "+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void awaitB() {
try {
lock.lock();
System.out.println("begin await B 时间为 "+ System.currentTimeMillis());
conditionB.await();
System.out.println("end await B 时间为 "+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void signalAll_A() {
try {
lock.lock();
System.out.println(" signalall B 时间为 "+System.currentTimeMillis());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void signalAll_B() {
try {
lock.lock();
System.out.println(" signalall B 时间为 "+System.currentTimeMillis());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.awaitA();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.awaitB();
}
});
threadB.setName("B");
threadB.start();
Thread.sleep(3000);
myServer.signalAll_B();
}
}
在上述代码中我们定义了两个Condition,分别用于两个线程的等待和唤醒,而我们只唤醒线程B而线程A则继续等待。结果如下:
三、公平锁与非公平锁
公平锁就是线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出的顺序。非公平锁就是随机获取锁的,属于一种枪战机制。先来不一定先得到,谁抢到属于谁,所以是不公平的。
通过构造方法ReetrantLock(boolean isFair)来创建对应公平锁或不公平锁。无参默认为非公平锁。
四、使用ReentrantReadWriteLock类
使用ReetrantLock具有完全互斥排他的效果,即同一时间只有一个线程能够执行Reentrantlock.lock()方法后的代码。虽然这样能保持实例变量的线程安全性,但是效率确实非常低的。然而使用读写锁ReentrantReadWriteLock类,可以提高运行效率,在不需要操作实例变量的情况下,可以使用读写锁ReentrantReadWriteLock来提高任务的执行效率。
读写锁ReentrantReadWriteLock有两个锁,一个是读操作的锁即共享锁,一个是写操作锁即排它锁。在多少个读锁之间不互斥,读锁和写锁互斥,写锁和写锁互斥。在多个线程中,如果没有线程进行写操作的时,可以有多个线程同时进行读操作;而其中一个线程正在写操作时,其他线程的读操作或写操作都只能等当前的写线程执行完毕。
1.读写或写读互斥
public class MyServer {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
lock.readLock().lock();
System.out.println("获得读锁: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("获得写锁: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadB.setName("B");
threadB.start();
Thread threadC = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadC.setName("C");
threadC.start();
}
}
结果如:
由结果可知,读写和写读是互斥的
2.读读共享
更改上面的main方法:
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadB.setName("B");
threadB.start();
}
结果如:
由结果可知:读和读共享
3.写写互斥
更改上面的main方法:
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadB.setName("B");
threadB.start();
}
结果如:
由结果知:写写互斥