一、LockSupport
1.传统方法的局限性
- 以前我们要阻塞和唤醒某一个具体的线程有很多限制
1、因为wait()方法需要释放锁,所以必须在synchronized中使用,否则会抛出异常IllegalMonitorStateException
2、notify()方法也必须在synchronized中使用,并且应该指定对象
3、synchronized()、wait()、notify()对象必须一致,一个synchronized()代码块中只能有一个线程调用wait()或notify()
2.LockSupport
LockSupport是一个比较底层的工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。java锁和同步器框架的核心 AQS:AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()的方法,来实现线程的阻塞和唤醒的。
- 举个栗子1
首先使用lombda表达式创建了线程对象 " t " ,然后,通过 " t " 对象调用线程的启动方法start(),然后我们再看线程的内容,在for循环中,当 i 的值等于5的时候,我们调用了LockSupport的.park()方法使当前线程阻塞,注意看方法并没有加锁,就默认使当前线程阻塞了,由此可以看出LockSupprt.park()方法并没有加锁的限制。
public class test {
public static void main(String[] args) {
//使用lombda表达式创建一个线程t
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
//使用LockSupport的park()方法阻塞当前线程t
LockSupport.park();
}
try {
//使当前线程t休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动当前线程t
t.start();
}
}
输出:012345
- 举个栗子2
LockSupport的unpark()方法(唤醒线程)可以先于LockSupport的park()方法执行。
public class test {
public static void main(String[] args) {
//使用lombda表达式创建一个线程t
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
//使用LockSupport的park()方法阻塞当前线程t
LockSupport.park();
}
try {
//使当前线程t休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动当前线程t
t.start();
//唤醒线程t
LockSupport.unpark(t);
}
}
输出:0123456789
- 举个栗子3
如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒,运行后发现只有当i
==
5时的park被唤醒,i==8时依然会阻塞。
原因是LockSupport的unpark()方法就像是获得了一个“令牌”,而park()方法就像是在识别“令牌”,当主线程调用了LockSupport.unpark(t)方法也就说明线程 " t " 已经获得了”令牌”,i等于5时候,线程 " t " 调用LockSupport的park()方法时,线程 " t " 已经有令牌了,这样他就会马上再继续运行,也就不会被阻塞了。
但是当i==8时线程 " t " 再次调用了LockSupport的park()方法使线程再次进入阻塞状态,这个时候“令牌”已经被使用作废掉了,主线程处于等待令牌的状态,也就无法阻塞线程 " t " 了。所以当主线程处于等待“令牌”状态时,线程 " t " 再次调用了LockSupport的park()方法,那么线程 " t "就会永远阻塞下去,即使调用unpark()方法也无法唤醒了。
public class test {
public static void main(String[] args) {
//使用lombda表达式创建一个线程t
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
//使用LockSupport的park()方法阻塞当前线程t
LockSupport.park();
}
if (i == 8) {
//使用LockSupport的park()方法阻塞当前线程t
LockSupport.park();
}
try {
//使当前线程t休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动当前线程t
t.start();
//唤醒线程t
LockSupport.unpark(t);
LockSupport.unpark(t);
}
}输出:012345678
- LockSupport中park()和unpark()方法的实现原理
park()和unpark()方法的实现是由Unsefa类提供的,而Unsefa类是由C和C++语言完成的,其实原理也是比较好理解的,它主要通过一个变量作为一个标识,变量值在0,1之间来回切换,当这个变量大于0的时候线程就获得了“令牌”,从这一点我们不难知道,其实park()和unpark()方法就是在改变这个变量的值,来达到线程的阻塞和唤醒的。
二、面试题1
- 实现一个容器,提供两个方法add、size,写两个线程:线程1,添加10个元素到容器中,线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束。
1.实现方式1
结论:第一这个方案没有加同步,第二while(true)中的c.size()方法永远没有检测到,没有检测到的原因是线程与线程之间是不可见的,导致无法break跳出循环。
public class test {
List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
test c = new test();
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();
}
}
输出:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
2.实现方式2
用volatile修饰了一下List集合,实现线程间信息的传递,但是还是有不足之处,程序还是无法运行成功,而且我们还得出,volatile一定要尽量去修饰普通的值,不要去修饰引用值,因为volatile修饰引用类型,这个引用对象指向的是另外一个new出来的对象对象,如果这个对象里边的成员变量的值改变了,是无法观察到的,所以这个实现也是不理想的。
- 注释掉睡眠1秒将会持续输出,未注释时候输出满足。可以理解为睡眠时候线程去检测list里面的数是否达到5。
public class test {
//添加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) {
test c = new test();
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();
}
}
输出:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
3.实现方式3
用了锁的方式(利用wait()和notify()),通过给object对象枷锁然后调用wait()和notify()实现。
- 这种写法也是行不通的,原因是notify()方法不释放锁,当t1线程调用了notify()方法后,并没有释放当前的锁,所以t1还是会执行下去,待到t1执行完毕,t2线程才会被唤醒接着执行,这个时候对象已经不只有5个了。
public class test {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList