常用方法 wait() 、notify()、notifyAll():
通过一个简单的例子来熟悉wait() 、notify()、notifyAll().有一个例子实现一个容器,提供两个方法 add size,写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到达5的时候,线程2给出提示并结束。我们可以先把大致的程序代码给出:
public class MyContainer1 {
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("线程2结束");
},"t2").start();
}
}
通过运行上述代码得到的结果很明显不能达到我们多期望的。我们可以想到 有可能是size到达5的时候 线程t2并不知晓,我们可以在List lists=new ArrayList();前加 volatile 进行尝试,加完我们发现程序运行时OK的,但是这样子任然存在两个问题,第一,由于我们没有使用锁,这个时候如果有另外一个线程也来添加元素,那么集合长度变成6了,可第一个线程却以为时5,第二,由于线程2使用while死循环来监控集合长度变化,非常浪费CPU。我们可以进行进一步的优化。我们可以使用 wait 跟notify 来实现,使用wait跟notify必须进行加锁,需要注意的是wait会释放锁,而notify不会释放锁,代码如下:
public class MyContainer3 {
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("线程2开始");
if (c.size() != 5) {
try {
lock.wait(); //释放锁,进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2结束");
}
}, "t2").start();
new Thread(() -> {
System.out.println("线程1开始");
synchronized (lock) {
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
if (c.size() == 5) {
lock.notifyAll();//唤醒该锁定对象的等待的线程 但是不会释放锁 sleep也不释放锁
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1结束");
}
}, "t1").start();
}
}
运行上述小程序,会发现当size等于5的时候,t2线程并没有立即结束,等到了t1运行完以后才结束,这是因为notify并不释放锁,虽然把t2线程叫醒了 ,可是此刻锁再t1线程的受伤,必须等到t1结束,我们进一步优化,当t1执行完notify之后 调用wait 使自己进入等待释放锁,然后t2运行,运行结束再调用notify 唤醒t1.这样才能得到题目一致的效果。由于本程序中使用了synchronized锁,所以其性能可能会有一定的降低,在这里我们可以通过其他手段来实现,并且能保证较好的性能嘛? 我们可以使用 由于此处不涉及同步,仅仅涉及线程之间的同步,synchronized就显得有点笨重,所以我们可以考虑使用 门闩 CountDownLatch来实现。CountDownLatch 的await 跟countDown方法来代替wait跟notify,CountDownLatch不涉及锁,在count等于0的时候程序继续运行,通讯简单,同时也可以指定时间。来看以下代码:
public class MyContainer5 {
//
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("线程2结束");
if (c.size() != 5) {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2结束");
}, "t2").start();
new Thread(() -> {
System.out.println("线程1开始");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
if (c.size() == 5) {
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1结束");
}, "t1").start();
}
}
CountDownLatch的概念:
CountDownLatch的用法:
CountDownLatch的不足
手动锁 ReentrantLock(重入):
先上一段小程序代码:
public class ReentrantLock1 {
synchronized void m1() {
for(int i=0;i<10;i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(i);
}
}
synchronized void m2() {
System.err.println("m2.....");
}
public static void main(String[] args) {
ReentrantLock1 r1 =new ReentrantLock1();
new Thread(r1::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1::m2).start();
}
}
上述代码很简单,起两个线程,分别执行两个同步方法,用synchronized锁定this对象,这里只有当m1方法执行完,m2方法才得以运行。在这里我们可以使用手动锁 ReentrantLock 实现同样的功能,直接上代码:
public class ReentrantLock2 {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();//synchrnized(this) 锁定
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.err.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
void m2() {
lock.lock();
System.err.println("m2.....");
lock.unlock();
}
public static void main(String[] args) {
ReentrantLock2 r1 = new ReentrantLock2();
new Thread(r1::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1::m2).start();
}
}
这里使用了 ReentrantLock 代替了 synchronized ,其中锁定采用的 lock() 方法,这里需要注意的是,它不会自动释放锁,也不像 synchronized 那样,在异常发生时jvm会自动释放锁,ReentrantLock 必须要必须要必须要手动释放锁,因此 ReentrantLock的释放锁通常会放到 finally 中去进行锁的释放。通过运行上述小程序会发现它可以达到用synchronized同样的效果。
在使用ReentrantLock 可以进行“尝试锁定” tryLock 这样无法锁定或者在指定时间内无法锁定,线程可以决定是否继续等待。进行“尝试锁定” tryLock 不管锁定与否,程序都会继续运行,也可以根据trylock的返回值来判断是否运行,也可以指定时间 由于trylock(time)抛出异常 所以unlock一定要在finally里面执行。直接上代码:
public class ReentrantLock3 {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();//synchrnized(this)
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.err.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 进行“尝试锁定” tryLock 不管锁定与否,程序都会继续运行
* 也可以根据trylock的返回值来判断是否运行
* 也可以指定时间 由于trylock(time)抛出异常 所以unlock一定要在finally里面执行
*/
void m2() {
boolean locked = lock.tryLock();//拿到锁返回true
System.err.println("m2....." + locked);
if(locked) lock.unlock();
}
public static void main(String[] args) {
ReentrantLock3 r1 = new ReentrantLock3();
new Thread(r1::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1::m2).start();
}
}
运行小程序发现m2方法执行后并未得到锁。这里由于是demo程序,m2方法中的逻辑并不完善,需要根据自己的业务需求来根据locked的值进行处理。这里还可以进行另外一种尝试锁定,修改m2方法如下:
void m2() {
boolean locked =false;
try {
lock.tryLock(5, TimeUnit.SECONDS);
System.err.println("m2....." + locked);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(locked) lock.unlock();
}
}
上述m2方法中的尝试锁定lock.tryLock(5, TimeUnit.SECONDS); 的意思是进行5秒钟的尝试锁定,当然这里跟上一步的没有参数的尝试锁定也是一样的,需要根据返回值进行下一步的业务逻辑。
ReentrantLock 还可以调用lockInterruptibly方法。可以对线程interrupt方法做出相应,在一个线程等待锁的过程中 可以被打断,来看以下代码:
public class ReentrantLock4 {
static boolean b =false;
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(()-> {
try {
lock.lock();//synchrnized(this)
System.err.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
System.err.println("t1 end");
} catch (InterruptedException e) {
System.err.println("interrupt");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(()-> {
try {
// lock.lock();
lock.lockInterruptibly();//可以对线程interrupt方法做出相应
b = lock.tryLock();
System.err.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.err.println("t2 end");
} catch (InterruptedException e) {
System.err.println("interrupt");
} finally {
if(b)lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();//打断线程的等待
}
}
上述代码中的t1线程sleep的时间是Integer的最大值,我们且当成它睡死了,也就是说它一直占用着这个锁不释放,此时t2线程也想锁定lock,但是一直无法得到这个锁,t2线程就会一直处于等待状态,但是我们现在不想让他等待了,想让t2终止,如果此时t2调用的是 lock.lock(); 方法,那么主线程中的 t2.interrupt(); 是起不了效果的 ,因为 ReentrantLock 的lock 方法没有对 interrupt 的支持。 所以我们会发现线程一直处于运行状态,我们将 lock 方法 替换成 lockInterruptibly,再运行程序会发现 t2 线程被终止了 。
ReentrantLock 可以指定为公平锁,synchronized 的锁全部都是不公平锁,假设多个线程同时在等待同一把锁,在原来的锁拥有者释放该锁的时候,接下去由谁来获得这把锁是不一定的,要看线程调度器去选择哪个了,也称竞争锁,公平锁就是接下去谁获得锁是由规律的,就是等待线程时间长的获得锁,就像排队买票时一样的道理。直接上代码:
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 r1 =new ReentrantLock5();
Thread thread = new Thread(r1);
Thread thread2 = new Thread(r1);
thread.start();
thread2.start();
}
}
上述代码 new ReentrantLock(true) ;中设定参数 true 表示该锁是一个公平锁,公平锁效率低,但是是公平的,运行上述小程序,控制台打印出的结果是每个线程都是循环去执行,一人执行一次,很公平。
下看来看一下非常经典的生产者消费者模型:写一个固定容量同步容器 有put get方法以及getCount方法 能够支持2个生产线程跟10个消费线程阻塞调用,我们先使用wait/notify来实现,代码如下:
public class MyContainer1<T> {
private volatile LinkedList<T> lists = new LinkedList<T>();
final private int MAX = 10;// 最多10个元素
private volatile int count = 0;
public synchronized void put(T t) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName() + " 获得锁 ");
while (lists.size() == MAX) {// 为什么用while? 因为在唤醒后 while会在执行一遍才执行wait下面的代码
try {
System.err.println(Thread.currentThread().getName() + " 进入等待 ");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
count++;
System.out.println("存储值" + t + "当前个数:" + count);
this.notifyAll();// 通知消费者进行消费 要用notifyAll 要使用notify 有可能叫醒一个生产者
}
public synchronized T get() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
T t = null;
System.err.println(Thread.currentThread().getName() + " 获得锁 ");
while (lists.size() == 0) {
try {
System.err.println(Thread.currentThread().getName() + " 进入等待 ");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count--;
System.err.println(Thread.currentThread().getName() + "取到的值:" + t + "" + "当前个数:" + count);
this.notifyAll();// 通知生产者生产
return t;
}
public static void main(String[] args) {
MyContainer1<String> c = new MyContainer1<>();
for (int i = 0; i < 4; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
c.get();
}
}, "c-" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 4; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) {
c.put(Thread.currentThread().getName() + "****** " + j);
}
}, "p-" + i).start();
}
}
}
上述代码是很经典的生产者消费者模型,在这个模型中为什么用while 而不是 if ?这是因为当 notifyAll 唤醒线程的时候要准备往集合中生产,这个时候如果使用的是 if 那么当此刻有两个生产者者线程被唤醒了,而集合中已经有9个元素,此刻只要有一个线程存进去一个值,另外一个线程一定报错,如果用的是 while 。那么在被唤醒的时候 线程会继续执行 lists.size() == MAX 这段代码进行判断。这就是为什么要使用while 而不是 if 的原因。还有一点是为什么使用notifyAll 而不是 notify ? 这是因为前者能唤醒所有等待的线程,而后者只能随机唤醒一个,假设此刻运行的生产者线程 put 了一个值后 ,集合数量达到10,而此刻是用 notify 的话,恰好又是唤醒一个生产者,此刻这个线程发现集合满了,也进入 wait 状态,导致程序无法运行。在这里我们会发现使用notifyAll的时候唤醒所有线程,当生产者往集合里 put 满了元素后还有可能继续唤醒生产者,是否能做到精准的就叫醒消费者线程呢?
ReentrantLock 中可以使用 lock 跟Condition来实现, Condition可以更加精准的指定哪些线程被唤醒。来看以下代码:
public class MyContainer2<T> {
final private LinkedList<T> lists = new LinkedList<T>();
final private int MAX = 10;// 最多10个元素
private static int count = 0;
private Lock lock = new ReentrantLock();
private Condition p = lock.newCondition(); //生产者
private Condition c = lock.newCondition(); //消费者
public void put(T t) {
try {
lock.lock();
while (lists.size() == MAX) {
p.await();
}
lists.add(t);
++count;
System.out.println("存储值" + t + "当前个数:" + count);
c.signalAll();
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while (lists.size() == 0) {
c.await();
}
t = lists.removeFirst();
count--;
System.err.println(Thread.currentThread().getName() + "取到的值:" + t + "" + "当前个数:" + count);
p.signalAll();
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
MyContainer2<String> c = new MyContainer2<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
c.get();
}
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 2; i++) {
System.out.println("生产者" + i + "启动");
new Thread(() -> {
for (int j = 0; j < 25; j++) {
c.put(Thread.currentThread().getName() + "****** " + j);
}
}, "p" + i).start();
}
}
}
ThreadLocal (线程局部变量):
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。我们先来看以下代码:
public class ThreadLocal1 {
volatile static Person p=new Person();
public static void main(String[] args) {
new Thread(()-> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(p.name);
}).start();
new Thread(()-> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.name = "lisi";
}).start();
}
}
class Person{
String name ="zhangsan";
}
上述小程序中两个线程之间是相互影响的,线程2修改了name ,线程一拿到的结果是 lisi 。如果我们要实现两个线程之间互不影响有什么好的方法呢? 这里我们可以使用 ThreadLocal 线程局部变量来实现:
public class ThreadLocal1 {
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) {
// TODO Auto-generated catch block
e.printStackTrace();
}
tl.set(new Person());
}).start();
}
}
class Person{
String name ="zhangsan";
}
运行上述小程序得到输出结果为 null ,这样就保证了线程2 里的 Person 对象与线程1是互不影响的。也就是自己只能用自己线程里的东西。因为ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。其他线程是没办法访问的 。ThreadLocal是使用空间换时间,无需上锁,提高了并发效率,就像Hibernate的session就存在于TreadLocal中,都由线程自己维护,这样子就不存在线程之间的等待问题。synchrnized是使用时间换空间,是要上锁的,只有一个线程访问完了另外一个线程才能访问,这样子拉长了程序运行时间。