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的使用

好了, 啰里啰嗦,说了一大通,看的云里雾里。 其实我觉得如果能把代码拿出来 敲一下,跑一跑,应该就会明白使用多线程和锁的妙处。 东西比较多,如果有什么不对的,请批评指正。 这篇就先说到这里,下篇我们再见。