lock锁
除了Synchronized关键字,JAVA还提供了lock锁,其性能差距不大,但是lock因为需要程序员手动持有锁,手动释放锁,但是Synchronized关键字是完全自动持有锁、释放锁的。这样看起来好像是synchronized要更胜一筹,但是因为自己参与到持有锁、释放锁的过程中,程序员的对线程的控制也就更加灵活。
锁的使用
public class lock_learning implements Runnable {
Lock lock = new ReentrantLock();
static int count = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
public static void main(String args[])throws InterruptedException{
lock_learning T1 = new lock_learning();
Thread t1 = new Thread(T1);
Thread t2 = new Thread(T1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(T1.count);
}
}
这里要注意lock.lock()一定不能写在try 代码块里面,如果在获取锁时发生异常,异常抛出的同时,也会导致锁直接释放。
lockInterruptibly
重入锁reentranlock还有一个特点,可以响应中断。也就是说如果该线程现在正持有锁,运行到一半时,该线程被别的线程中断,该线程会退出执行,并且释放锁,抛出中断异常。
public class LockInterruptibly implements Runnable {
static ReentrantLock lock1 = new ReentrantLock();
static ReentrantLock lock2 = new ReentrantLock();
int i;
public LockInterruptibly(int i) {
this.i = i;
}
@Override
public void run() {
try {
if (i == 1) {
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
public static void main(String args[]) throws InterruptedException {
LockInterruptibly l1 = new LockInterruptibly(1);
LockInterruptibly l2 = new LockInterruptibly(2);
Thread t1 = new Thread(l1);
Thread t2 = new Thread(l2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
这里有几点要注意:
1.isHeldByCurrentThread是Reentrantlock的方法,返回值为boolean 意为该锁是否被当前线程所持有。
2.static关键词,用于修饰变量或者方法,其随着类的加载而加载,放置于JVM的方法区中,并且static关键词修饰的变量为所有该类创建对象锁共同享用。因此这里的t1、t2两个线程都会拥有同一对lock1、lock2。
现在该程序的输出为一个异常,因为t2被打断,因此抛出一个InterruptException。如果去掉static关键词,那么该程序不会抛出任何异常。
3.一个重入锁是被中断的获取锁,如果在获取锁器件被中断,那么该锁会抛出一个InterruptException。
trylock()方法
public class trylock implements Runnable {
static ReentrantLock lock1 = new ReentrantLock();
static ReentrantLock lock2 = new ReentrantLock();
int i;
public trylock(int i) {
this.i = i;
}
public void run() {
while (true) {
if (i == 1) {
if (lock1.tryLock()) {
try {
try {
///让一个线程工作的时间变长,不然会以为两者线程并没有不断尝试
Thread.sleep(50);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + "My jbo done.");
//当完成一次之后就return退出。
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} else {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My job done.");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String args[]) {
trylock l1 = new trylock(1);
trylock l2 = new trylock(2);
Thread t1 = new Thread(l1);
Thread t2 = new Thread(l2);
t1.start();
t2.start();
}
}
跟lock锁比起来ReentrantLock的trylock()方法可以在尝试获取锁之后立马返回一个boolean值,所以出现trylock()函数的线程不会出现死锁,因为如过拿不回锁该线程会直接返回一个值,并不会一直等待,上面的代码尽管看起来T1、T2相互尝试获得对象的锁,但是因为使用trylock函数,会不停地尝试获得锁,因此以上代码执行一段时间之后会发现两个线程都结束了运行并且输出了值。
11My jbo done.
12:My job done.
public class ConditionLearning implements Runnable {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
@Override
public void run() {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
}
System.out.println("Job done");
lock.unlock();
}
public static void main(String[] args)throws InterruptedException {
ConditionLearning l1 = new ConditionLearning();
Thread t1 = new Thread(l1 );
t1.start();
///保证这里一定要让t1线程先运行一段时间拿到锁并且
Thread.sleep(1000);
lock.lock();
condition.signal();
lock.unlock();
}
}
在T1线程中condition对象被置为等待状态,并且该线程释放锁,没有执行下面的代码,
这里可以看到主线程中对lock的condition对象进行了唤醒,然后释放了lock锁,T1线程此时又一次尝试获得锁,获得之后继续执行下方代码。输出job done。
CountDownLatch
意为倒计时,应用的最常见场景是比如一个主线程的运行需要等待其他线程加载完成之后才可以执行,这时候可以把其他线程都扔进CountDownLatch,在程序运行的时候,主线程调用await()方法之后,该对象会维护一个Count,当一个线程完成任务之后,Count会-1,当count==0时,主线程会唤醒,然后开始执行。
CyclicBarrier
线程阻塞工具类
线程阻塞工具类LockSupport是一个工具类,其中的park(),unpark()方法可以实现suspend()以及resume()方法(在不会死锁的情况下实现),之前讲到过,suspend以及resume方法弃用的原因是不确定调用suspend已经resume方法的顺序,并且线程在suspend的情况下,其状态是runnable,难以排查。那么park、unpark是怎么实现不死锁的呢?
通过标记值,即使unpark()方法执行在park()方法之前,线程会把该线程的标记值记为已标记,当调用park()方法的时候,因为其标记值为已标记,会直接返回,并且消费该标记值。所以这样就不会出现某个线程无限挂起的情况。
public class ParkLearning implements Runnable {
public static Object u = new Object();
public void run() {
synchronized (u) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程挂起");
LockSupport.park(this);
System.out.println("线程结束");
}
}
public static void main(String[] args) {
ParkLearning t = new ParkLearning();
Thread t1 = new Thread(t, "t1");
t1.start();
System.out.println("线程继续");
LockSupport.unpark(t1);
}
}
线程继续
线程挂起
线程结束
这里可以看到即使是先执行了unpark()方法,后来当线程调用park()方法之后,仍然会立即返回,继续执行。
同时park()方法会响应中断,但是不会抛出异常,会直接返回,但是中断的标记我们是可以从isInterrupted获得的。
public class LockSupportDemo {
public static SupportThread s1 = new SupportThread("t1");
public static SupportThread s2 = new SupportThread("t2");
public static class SupportThread implements Runnable {
private String name;
private Object u = new Object();
public SupportThread(String name) {
this.name = name;
}
public void run() {
synchronized (u) {
System.out.println(getName() +"获得锁");
LockSupport.park();
if (Thread.interrupted()) {
System.out.println(getName() + "被中断了");
}
System.out.println(getName() + "执行结束");
}
}
public String getName() {
return name;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
Thread.sleep(1000);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}
}
t1获得锁
t1被中断了
t1执行结束
t2获得锁
t2执行结束
这里可以看出来首先t1获得锁,然后t1挂起,在没有unpark()的情况下t1还是可以输出"执行结束",是因为t1被主线程中断了,所以t1的park()直接返回,然后继续执行,同时也可以看到,t1的中断位标记并没有清除,所以得以继续执行。输出t1被中断了。执行结束之后释放锁,此时t2获得锁,然后park()、unpark()。结束执行。
读写锁
因为读操作是线程安全的,是不会对数据进行更改的,只有写操作才会对数据进行更改,因此当有大量的读数据的操作,写操作只占小部分的时候,那么这个系统的效率就偏低了,那么读写锁完成的就是这样一个工作,即读操作会发生在写操作之后,但如果数据并没有被更改,那么读操作就会一拥而上一起进行,使效率变高。