同步控制是并发程序必不可少的重要手段。
重入锁可以完全替代关键字synchronized。重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i=0;
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
lock.lock();
try {
i++;
}finally {
lock.unlock();
}
}
}
}
与关键字synchronized相比,重入锁有着显示的操作过程。必须手动指示何时加锁,何时释放锁。因此,重入锁对逻辑控制的灵活性要远远优于synchronized。
重入锁是可以反复进入的,一个线程可以多次获得同一把锁,但是要记得释放相同次数。
重入锁的高级功能:
(1)中断响应
对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么获得这把锁继续执行,要么保持等待。而重入锁则使得线程在等待锁的时候可以被中断。这种情况对于处理死锁是有一定帮助的。
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();//这个请求锁的方法可以对中断进行相应
Thread.sleep(500);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (lock1.isHeldByCurrentThread())//查询当前线程是否保持该锁
lock1.unlock();
if (lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId()+"线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();t2.start();
Thread.sleep(1000);
t2.interrupt();//中断t2
}
}
线程t1和t2启动后,由于互相请求对方持有的锁造成死锁。如果不中断t2,那么会无限期等待下去。中断t2后,t2会放弃对lock1的申请,同时释放已获得的lock2,从而使得t1获得lock2顺利执行下去。
中断后两个线程都会退出,但真正完成工作的只有t1。
(2)锁申请等待限时
除了等待外部通知以外,要避免死锁还有另一种方法:限时等待。可以使用tryLock() 方法进行一次限时的等待。
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6000);
} else {
System.out.println("get Lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock t1 = new TimeLock();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t1);
thread1.start();
thread2.start();
}
}
tryLock() 方法接受两个参数,等待时长和计时单位。由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒之内获得锁,请求失败。
ReentrantLock.tryLock()不带参数时,线程会尝试获得锁,申请成功返回true,申请失败则会立即返回false,不在进行等待。
(3)公平锁
使用synchronized关键字进行锁控制时,产生的锁是非公平的,就是说会在等待队列中随机挑选一个获得锁。
重入锁允许对公平性进行设置,构造函数如下:
public ReentrantLock(boolean fair)
当参数为true时,表示锁是公平的。公平锁按照时间先后顺序,保证不会产生饥饿现象。但是实现成本较高,性能比较地下,默认状况下为非公平的。
与Object.wait() 和notify() 作用大致相同。
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
//通过newCondition方法生成一个与当前重入锁绑定的Condition实例。
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
//await方法会使当前线程等待在Condition上,同时释放当前锁
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition reenterLockCondition = new ReenterLockCondition();
Thread thread = new Thread(reenterLockCondition);
thread.start();
Thread.sleep(2000);
//在signal方法调用时,要求线程先获得相关的锁
lock.lock();
//调用signal方法,系统会从当前Condition对象的等待队列中唤醒一个线程
//同理,signalAll方法会唤醒所有线程
condition.signal();
//在signal方法调用后,需要释放相关的锁,让给被唤醒的线程
//若没有释放锁,线程thread无法继续执行
lock.unlock();
}
}
信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程,同时访问一个资源。
//信号量的构造函数,构造时必须制定准入数,即同时能申请多少个许可
public Semaphore(int permits)
public Semaphore(int permits,boolean fair) //第二个参数可以指定是否公平
//信号量的方法
public void acquire() //尝试获得一个准入许可
public void acquireUninterruptibly() //不响应中断
public boolean tryAcquire() //尝试获得一个许可,成功返回true,失败返回false,不等待
public boolean tryAcquire(long timeout, TimeUnit unit) //等待限时
public void release() //用于访问结束后释放一个许可
一个简单的例子:
public class SemapDemo implements Runnable {
//声明一个包含5个许可的信号量
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
//申请信号量
semp.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId()+":done!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//离开前要释放信号量
semp.release();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemapDemo demo = new SemapDemo();
//同时开启20个线程,但每次只能有5个线程进行输出
for (int i = 0; i < 20; i++) {
exec.submit(demo);
}
}
}
读写分离锁能有效地帮助减少锁竞争,提升系统性能。
一般来说,读与读之间不互斥,可以并行操作;读与写、写与写之间仍会互相阻塞。
public class ReadWriteLockDemo {
// private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock(); //读取锁
private static Lock writeLock = readWriteLock.writeLock(); //写入锁
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
try {
lock.lock(); //模拟读操作
Thread.sleep(1000); //读操作的耗时越多,读写锁的优势就越明显
return value;
}finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
try {
lock.lock(); //模拟写操作
Thread.sleep(1000); //写操作的耗时越多,读写锁的优势就越明显
value = index;
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunable=new Runnable() {
@Override
public void run() {
try {
demo.handleRead(readLock);
// demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunable =new Runnable() {
@Override
public void run() {
try {
demo.handleWrite(writeLock,new Random().nextInt());
// demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 18; i++) {
new Thread(readRunable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunable).start();
}
}
}
以上程序使用读写锁时2秒多就能执行完, 因为读操作是并行的,而使用重入锁时需要20多秒(读操作也会阻塞)。
通常用来控制线程等待,可以让某一个线程等待直到倒计数器结束再开始执行。
public class CountDownLatchDemo implements Runnable {
//创建一个倒计数器的实例,参数为计数器的计数个数
//参数为10表示需要10个线程完成任务后等待在CountDownLatch上的线程才能继续执行
static final CountDownLatch end = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();
@Override
public void run() {
try {
//模拟线程执行时间
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println("check complete");
//一个线程已经完成了任务,倒计数器减一
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.submit(demo);
}
//要求主线程等待所有检查任务全部完成后,才能继续执行
end.await();
System.out.println("go!");
exec.shutdown();
}
}
类似CountDownLatch,但功能更强大。比如,把计数器设置为10,那么在凑齐第一批10个线程后,一起开始执行任务,直到任务完成,然后计数器就会归零,接着凑齐下一批10个线程。
可以在线程内任意位置让线程阻塞。