Java 高并发系列2-并发锁
接着上一篇并发文章我们继续
Java 高并发系列1-开篇
本篇的主要内容是以下几点:
- wait 、notify 的简单使用
- Reentrantlock的简单使用
- synchronized 与Reentrantlock的区别
- ThreadLocal的简单使用
看一个面试题:
曾经的面试题:(淘宝?)
实现一个容器,提供两个方法,add,size
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
public class MyContainer1 {
/* volatile */ List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer1 c = new MyContainer1();
new Thread(() -> {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
while(true) {
if(c.size() == 5) {
break;
}
}
System.out.println("t2 结束");
}, "t2").start();
}
}
看完大概实现方法就是 使用while(true) 死循环 进行读取容器的大小,如果不添加volatile 关键字 t2 没有办法跳出while循环,因为容器的改变对 t2不可见。
添加volatile 字段可见之后,当然可以实现对容器大小的监听。
但是也存在两个问题,
第一、浪费CPU,在t1执行到5之前CPU都是在空转。
第二、不够精准,在循环判断容器大小==5时 跳出循环的时候 可能容器的大小已经添加到了6 或者7 。
为了解决这个问题, 我们再来看一条程序,
public class MyContainer3 {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer3 c = new MyContainer3();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2启动");
if(c.size() != 5) {
try { 挂起程序 释放锁 lock 等待。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized(lock) {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
lock.notify();
获取锁 lock 等待, 当size ==5 lock.notify 唤醒 t2
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
看完程序 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
也可以 在 t1 notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
缺点: 整个通信过程比较繁琐
需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。
再看一条程序
public class MyContainer5 {
// 添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer5 c = new MyContainer5();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2启动");
if (c.size() != 5) {
try {
latch.await();
//也可以指定等待时间
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);
if (c.size() == 5) {
// 打开门闩, 拉闸放水, 让t2得以执行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
使用CountDownLatch (门闩)替代wait notify来进行通知 好处是通信方式简单,同时也可以指定等待时间 使用await和countdown方法替代wait和notify CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了
这时应该考虑 countdownlatch/cyclicbarrier/semaphore
接下来再看一下Reentrantlock
程序1
public class ReentrantLock1 {
synchronized void m1() {
for(int i=0; i<10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
synchronized void m2() {
System.out.println("m2 ...");
}
public static void main(String[] args) {
ReentrantLock1 rl = new ReentrantLock1();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
Reentrantlock用于替代synchronized , 由于m1锁定this,只有m1执行完毕的时候,m2才能执行 这也是synchronized最原始的意义。
程序2
先简单注释一下
public class ReentrantLock2 {
/// 声明锁
Lock lock = new ReentrantLock();
void m1() {
try {
/// 上锁 ,相当于synchronized(this)
lock.lock(); //synchronized(this)
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
/ 这里的try{}finally{} // 一定要加 , 在finally块中 释放锁。
lock.unlock();
}
}
void m2() {
lock.lock();
System.out.println("m2 ...");
lock.unlock();
}
public static void main(String[] args) {
ReentrantLock2 rl = new ReentrantLock2();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
这里不再过多解释, 看注释。
由于synchronized 是碰见异常 jvm自动释放锁。 而 ReentrantLock不行, 需要手动释放。 所以一般情况下 放在finally语句里。
重要的事情说三遍, 必须手动释放, 手动释放。必须手动释放。
程序3.
public class ReentrantLock3 {
/// 声明
Lock lock = new ReentrantLock();
void m1() {
try {
/// 锁
lock.lock();
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
/// 释放
lock.unlock();
}
}
/**
* 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
* 可以根据tryLock的返回值来判定是否锁定
* 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中
*/
void m2() {
/*
boolean locked = lock.tryLock();
System.out.println("m2 ..." + locked);
if(locked) lock.unlock();
<!--locked 后边这里可以根据是否锁定来选择执行相关逻辑-->
*/
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
可以尝试锁定,等待5秒钟, 超时后锁定失败,返回false
System.out.println("m2 ..." + locked);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLock3 rl = new ReentrantLock3();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
看起来是不是ReentrantLock 是不是高级很多,继续
下一条程序
public class ReentrantLock4 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
try {
lock.lock();
System.out.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
1. 睡眠不释放锁, 2. 睡眠这么长时间,相当于睡死了都, 看下个线程。
System.out.println("t1 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(()->{
try {
//lock.lock();
/ 顾名思义就是 把锁打断, 打断线程1的等待
lock.lockInterruptibly(); //可以对interrupt()方法做出响应
System.out.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt(); //打断线程2的等待
}
}
lockInterruptibly()获取锁是以排他的模式获取,一旦被中断就放弃等待获取, 可以对线程interrupt方法做出响应,在一个线程等待锁的过程中,可以被打断。
ReentrantLock 除了是可重入锁 还可以设置公平锁和非公平锁。再来一条程序
public class ReentrantLock5 extends Thread {
private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁,请对比输出结果
public void run() {
for(int i=0; i<100; i++) {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLock5 rl=new ReentrantLock5();
Thread th1=new Thread(rl);
Thread th2=new Thread(rl);
th1.start();
th2.start();
}
}
根据参数true 或者 false ,是否可以设定为公平锁。
所谓的公平锁设定算法为 调度器优先选择等待时间长的线程执行。
而非公平锁则没有该设定。
再来看看ThreadLocal , 顾名思义, ThreadLocal线程局部变量
来一条程序,
public class ThreadLocal1 {
声明person 对象 volatile ,
volatile static Person p = new Person();
public static void main(String[] args) {
new Thread(()->{
try {
睡2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
/// 打印
System.out.println(p.name);
}).start();
new Thread(()->{
try {
/// 睡1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 赋值
p.name = "lisi";
}).start();
}
}
class Person {
String name = "zhangsan";
}
打印结果 lisi , 线程1 睡2秒, 睡醒后根据 volatile关键字特性, 线程修改 对其他线程可见, 既可以读取到 线程2 修改后的值。
再来一条,
public class ThreadLocal2 {
//volatile static Person p = new Person();
/ 将声明 封装了Person的ThreadLocal对象
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tl.get());
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tl.set(new Person());
}).start();
}
static class Person {
String name = "zhangsan";
}
}
由于线程1 线程2 都是读取自己Thread 对应的ThreadLocalMap 对象。
所以 打印结果 就是 当前线程取到的值 null 。
在Handler消息机制中的应用就是 Looper 通过 ThreadLocal 与currentThread 绑定了,所以才实现了通过Handler sendMessage 到指定线程。 如果想要详细了解ThreadLocal的使用原理 移步我以前写的一篇文章。
ThreadLocal源码详细解析 还有 在hibernate中session就存在与ThreadLocal中,避免synchronized的使用
好了, 啰里啰嗦,说了一大通,看的云里雾里。 其实我觉得如果能把代码拿出来 敲一下,跑一跑,应该就会明白使用多线程和锁的妙处。 东西比较多,如果有什么不对的,请批评指正。 这篇就先说到这里,下篇我们再见。