以下代码均通过使线程睡眠模拟实际业务场景来解释原理
Case1(synchronized 介绍):
public class T {
private int count = 10;
private Object o = new Object();
public void m() {
synchronized (o){ //这里的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
}
}
知识点:
- synchronized锁定的是一个对象,而不是代码块
- 对对象的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用
上面的代码是new出来一个Object的对象,用来充当一把锁,也可以使用下面的方式对对象进行加锁:
public class T1 {
private int count = 10;
private Object o = new Object();
public void m() {
synchronized (this){ //这里的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
}
}
知识点:
- 上面的代码写成synchronized (this),每次执行m函数时,就会锁定当前对象(new 出来的T1对象,而不是Object对象)
如果在函数开始的时候就要锁定,函数结束时才能释放的话,可以直接把锁加到函数上,如下:
public synchronized m(){
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
这种方式也不是锁定的代码,而是锁定的当前对象(函数所在的对象)。
Case2(对静态加锁):
package com.gmail.fxding2019;
public class T2 {
//当要锁定对象的是静态的时候
private static int count = 10;
//对m方法加锁,实际上是锁定的com.gmail.fxding2019.T2.class。
public synchronized static void m() {
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
}
知识点:
- 对静态的加锁,锁定的不是对象,所以上面的代码并不等同于synchronized (this),静态方法没有this
- 对静态方法m加锁,实际上是锁定的com.gmail.fxding2019.T2.class
其实锁定的是T类型的class对象。等同于下面的代码:
public static void m(){
synchronized (T2.class){
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
}
Case3(解决线程重入问题):
package com.gmail.fxding2019;
public class T3 implements Runnable {
private int count = 10;
public /*synchronized*/ void run(){
count--;
System.out.println(Thread.currentThread().getName() + "count:" + count);
}
public static void main(String[] args) {
T3 t = new T3();
for (int i = 0; i < 5; i++){
new Thread(t, "Thread" + i + ":").start();
}
}
}
/*====================output========================
Thread0:count:8
Thread1:count:8
Thread4:count:7
Thread2:count:6
Thread3:count:5
*/
上面的代码是通过实现Runnable接口,重写run方法,new了一个对象t。然后创建了5个线程,其操作的count--都是堆中的同一个对象。上面的代码会出现线程的重入问题。
出现的原因就是:当Thread0运行完count--时(这时count=9),还没执行打印语句,Thread1就已经执行完了count--(这时count=8),然后Thread0和Thread1再执行打印语句。就得到了两个8。
解决的方法就是对run方法前面加一个synchronized关键字(对堆中的T对象进行加锁)。
Case4(线程启动的三种写法):
package com.gmail.fxding2019;
public class T4{
public synchronized void m1(){
System.out.println(Thread.currentThread().getName() + ":m1 start ... ");
try {
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":m1 end ... ");
}
public void m2(){
try {
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":m2");
}
public static void main(String[] args) {
T4 t = new T4();
//lambda表达式写法
new Thread(()->t.m1()).start();
new Thread(()->t.m2()).start();
/*映射的写法
*new Thread(t::m1,"t1").start();
new Thread(t::m2,"t2").start();
* */
/*原始匿名对象的写法
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.m2();
}
}).start();
*/
}
}
/*====================output========================
Thread-0:m1 start ...
Thread-1:m2
Thread-0:m1 end ...
*/
上面的代码分别使用两个线程调用m1和m2方法,m1方法先启动,且m1是同步方法,m2方法后启动,m2方法是普通方法。
知识点:
- 当执行m1时,是需要锁定当前的对象的,当执行m2时,是不需要锁定当前的对象的。那么,当执行m1的过程中,m2能否被执行?
答案是可以的。只有加synchronized的方法才需要互斥等待。其他的方法不影响- 注意启动线程的三种写法:
- lambda表达式写法
- 映射的写法
- 原始匿名对象的写法
Case5(读脏数据):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T5_Account {
String name;
double balance; // 默认值为0.0
public synchronized void set(String name, double balance){
this.name = name;
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double getBalance(String name){
return this.balance;
}
public static void main(String[] args) {
T5_Account a = new T5_Account();
new Thread(()->a.set("zhangsan",100.0)).start();
//TimeUnit是工具类,功能相当于sleep
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
//第一次读
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
//第二次读
System.out.println(a.getBalance("zhangsan"));
}
}
/*====================output========================
0.0
100.0
*/
上面的代码对写的方法加锁,对读的方法不加锁,然后set改变Balance的值,通过睡眠的方式模拟两次读Balance,可以看到,结果是不一样的。
上面的代码只对set(写)设置了synchronized,没有对getBalance(读)设置,所以读不受同步的影响,这可就能能会导致读脏数据。脏读(dirtyRead)的定义:读到还没写成功的数据。解决的方法就是对读再进行加锁。
Case6(可重入锁):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T6 {
synchronized void m1(){
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在同步方法中调用另一个同步方法
m2();
}
synchronized void m2(){
try {
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("m2");
}
}
知识点:
- 一个同步的方法是可以调用另一个同步方法的。这是因为一个线程若已经有了某个对象的锁,再次申请时仍会得到该对象的锁(在堆中的操作就是在锁上面加了个数字,从1变成2,相当于锁定了2次)
- 以上的例子说明了:synchronized获得的锁是可重入的(可重入:获得锁之后还可以再获得一遍)
重入锁的另一种情形:子类的同步方法调用父类的同步方法,其锁定的还是TT(子类)的this对象。new TT时会先创建父类的对象:
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T7 {
synchronized void m(){
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("m end");
}
}
class TT extends T7{
synchronized void m(){
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
public static void main(String[] args) {
new TT().m();
}
}
/*====================output========================
child m start
m start
m end
child m end
*/
Case7(遇到异常释放锁):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T8 {
int count = 0;
synchronized void m(){
System.out.println(Thread.currentThread().getName() + "start");
while (true){
count++;
System.out.println(Thread.currentThread().getName() + "count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e ){
e.printStackTrace();
}
if(count == 5){
int i = 1/0; //此处会抛异常,锁会释放。要想不释放,就使用catch,使循环继续
}
}
}
public static void main(String[] args) {
T8 t = new T8();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(r, "t2").start(); //如果t1不释放锁的话,t2永远执行不起来,因为while是死循环
}
}
/*====================output========================
t1start
t1count = 1
t1count = 2
Exception in thread "t1" java.lang.ArithmeticException: / by zero
at com.gmail.fxding2019.T8.m(T8.java:23)
at com.gmail.fxding2019.T8$1.run(T8.java:33)
at java.lang.Thread.run(Thread.java:745)
t2start
t2count = 3
t2count = 4
t2count = 5
*/
上面代码中,m方法使用while循环对count进行加1操作,当count = 5时,就执行1/0操作(会抛出by zero异常)。启动两个线程,先启动t1,再启动t2。如果出现异常锁不会被释放的话,那么t2永远得不到执行。而结果是t2得到了执行,所以t1出现异常后释放了锁。
知识点:
- 程序在执行过程中,如果出现了异常,默认情况下锁是会被释放的(synchronized遇到异常锁会被释放)
- 所以,在并发处理的过程中,有异常要多加小心,不然可能会造成不一致的情况:例如:在一个web app 处理的过程中,多个servlet线程共同访问同一个资源,这时候如果异常处理不合适,在某一个线程中出现了异常,而这个线程可能只执行可一半,这会导致其释放同步锁,其他的线程就会进入同步代码区。其他线程就有可能访问到上个线程处理了一半的数据。
Case8(volatile关键字):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T9_volatile {
/*volatile*/boolean running = true;
void m(){
System.out.println("m start");
while (running){
}
System.out.println("m end");
}
public static void main(String[] args) {
T9_volatile t = new T9_volatile();
new Thread(t::m,"t1").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
t.running = false; //主线程中把running改为false
}
}
上面的代码就是通过设置running = true来是while出现死循环,即t1线程一直停不下来。虽然在主线程中设置了 t.running = false; 但是还是不起作用。如果在声明对running时加上volatile关键字,就可以使线程停下来。
产生这个现象的原因就是Java的JMM(java memory model)。可以这样理解:running保存在内存中,然后线程t1会把内存中的running copy到线程的缓存区中(每条线程还有自己的工作内存,可类比处理器的高速缓存类比,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成),所以while循环会一直执行。
这时候,主线程读取running的值到其线程缓冲区中,并把running的值改为false,由于running的值改变,所以主线程会把running=false再写回内存。但是t1线程并不会再从内存读数据了,所以t1会一直停不下来。
volatile关键字的作用就是:每当running在内存中的值发生改变时,会通知其他使用该变量的线程更新其缓存区。注意:是发生改变时才会从内存中读,而不是每一次都是从内存中读。volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题,也就是说volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性。
注意:多核CPU下,volatile也可以保证可见性,其他处理器通过嗅探总线上传播过来了数据监测自己缓存(每个CPU都有自己的缓存)的值是不是过期了,如果过期了,就会对应的缓存中的数据置为无效。而当处理器对这个数据进行修改时,会重新从内存中把数据读取到缓存中进行处理。
在这种情况下,不同的CPU之间就可以感知其他CPU对变量的修改,并重新从内存中加载更新后的值,因此可以解决可见性问题。
知识点总结:
- volatile关键字,使一个变量在多个线程间可见:
A、B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,使用volatile关键字,会让所有线程都读到该变量的修改值- 在上面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。使用volatile,当内存中的running改变时,将会强制所有线程都去堆内存读取running的值。
- volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性
下面通过小示例来演示volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性:
package com.gmail.fxding2019;
import java.util.ArrayList;
import java.util.List;
// volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题,也就是说volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性
public class T10 {
volatile int count = 0; //保证了线程之间的可见性
/*synchronized*/ void m(){
for (int i = 0; i<10000; i++){
count++;
}
}
public static void main(String[] args) {
T10 t = new T10();
List threads = new ArrayList();
for (int i = 0; i< 10; i++){
threads.add(new Thread(t::m,"Thread:" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
//Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行
o.join();
}catch (InterruptedException e){
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
/*====================output========================
37517
*/
从结果中可以看出,所有线程对count的操作结果应该为100000,但是最后的结果才为37517。这就说明了volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题。如果对m方法加上synchronized*关键字则可以解决这个问题。
Case9(原子类AtomXX):
package com.gmail.fxding2019;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T11_AtomXX {
//AtomicInteger是原子性的Integer类,用来进行简单的原子操作
//凡是Atomic开头的,里面的方法都是原子性的,用来做简单的同步
AtomicInteger count = new AtomicInteger(0);
void m(){
for (int i = 0; i < 10000; i++){
//incrementAndGet是原子操作,其是原子的方法,但并不是用synchronized实现的,而是用相当底层的方法,效率非常高
//用来替代count++
count.incrementAndGet();
}
}
public static void main(String[] args) {
T11_AtomXX t = new T11_AtomXX();
List threads = new ArrayList();
for (int i = 0; i< 10; i++){
threads.add(new Thread(t::m,"Thread:" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
}catch (InterruptedException e){
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
/*====================output========================
100000
*/
凡是Atomic开头的,里面的方法都是原子性的,用来做简单的同步,AtomicInteger是原子性的Integer类,用来进行简单的原子操作,上面的代码中,就是使用了AtomicInteger的incrementAndGet同步方法来代替count++。因为count++是原子性的,所以结果是100000,可以得到正确的结果。
知识点:
- incrementAndGet是原子操作,其是原子的方法,但并不是用synchronized实现的,而是用相当底层的方法,效率非常高
- 注意:Atomic的多个方法同时使用就不再具备原子性啦,比如:
if (count.get() < 1000){ count.incrementAndGet(); }
count.get()和count.incrementAndGet();都是原子性操作,但是在两条语句的中间还是有可能被其他线程打断
Case10(synchronized的简单优化 ):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T12 {
int count=0;
//锁住了整个方法,锁的粒度比较粗
synchronized void m1(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//业务逻辑中只有下面这句需要syn,这时不应给整个方法上锁
count++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end=System.currentTimeMillis();
}
void m2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 业务逻辑中只有下面这句需要syn,这时不应给整个方法上锁
* 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
*/
//只锁住了count++,细粒度的锁
synchronized (this){
count++;
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end=System.currentTimeMillis();
}
}
synchronized简单的优化原则:同步代码块中的语句越少越好,比较m1和m2,都是对count++操作进行加锁同步。m1锁住了整个方法,m2只锁住了关键的操作count++(注意:synchronized锁住的是对象,这里提到的是指,当执行当关键语句时,才对整个对象加锁,提高并发度)。我们称对m1加的锁是粗粒度锁,对m2加的锁是细粒度锁。
Case11(锁的引用发生改变):
package com.gmail.fxding2019;
import java.util.concurrent.TimeUnit;
public class T13 {
Object o=new Object();
void m(){
synchronized (o){
while (true){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
T13 t=new T13();
//启动第一个线程
new Thread(t::m,"t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2=new Thread(t::m,"t2");
t.o=new Object(); //锁发生改变,所以t2线程得以执行,如果注释掉这句话,线程2永远得不到执行机会
t2.start();
}
}
/*====================output========================
t1
t1
t1
t1
t2
t1
t2
t1
t2
t1
*/
上面代码中,t1锁住对象o后开始执行死循环,这时候把引用o执行的对象改变,然后执行t2再次锁定它。如果t1和t2锁定的是同一个对象的话,t2将得不到执行。从执行结果来看,t2锁定的和t1锁定的并不是同一个对象啦。
这个例子说明:锁定的并不是栈中的引用o,而是new出来的堆内存的对象,即锁的信息是记录在堆内存里面的。锁定某个对象o,如果o的属性发生改变,不影响锁的使用,但如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象。
Case12(不要以字符串常量作为锁定对象):
public class T14 {
String s1="Hello";
String s2="Hello";
void m1(){
synchronized (s1){
}
}
void m2(){
synchronized (s2){
}
}
}
上面代码锁定的是同一个对象。因为String类实现了常量池,hello存放在堆中的常量池中。常量池的概念见这篇文章。
在上面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串"Hello",但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。
Case13(wait && notify):
面试题:
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
public class T15 {
volatile List
上面的实现中,如果对list不加volatile。那么list的size不可见。所以t2不会结束。
虽然上面的这个程序可以完成上述要求的功能,但是也存在两个问题:
- 通知的不精确,可能已经add8了,t2结束才打印出来(原因是没有进行同步)
- 浪费cpu,t2一直用while进行检测
给lists添加volatile之后,t2能够接到通知,但是t2线程的死循环很浪费cpu,如果不用死循环怎么做呢?可以尝试使用wait和notify解决这个问题,如下面的代码:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class T16 {
//添加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) {
T16 t = new T16();
//声明一把锁
final Object lock=new Object();
new Thread(()->{
//t2锁住lock,并进入wait状态。注意:进入wait前必须加锁
//而wait后会释放lock锁,等待别的线程notify它。
//这里必须先写t2的等待。再写t1
synchronized (lock){
System.out.println("t2启动");
if (t.size()!=5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2结束");
}
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1对lock进行加锁。然后在t.size = 5时,再唤醒t2
new Thread(()->{
synchronized (lock){
System.out.println("t1启动");
for (int i = 0; i < 10; i++) {
t.add(new Object());
System.out.println("add "+i);
//注意:notify不用指定线程。也无法指定线程。对lock的notify cpu会自己找一个阻塞在lock上面的线程进行唤醒
if (t.size()==5){
lock.notify();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1结束");
}
},"t1").start();
}
}
/*====================output========================
t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
t1结束
t2结束
*/
上面的代码中,先使用wait让t2线程进行等待,然后启动t1不断向里面添加对象,等待size=5时,唤醒t2线程,打印t2结束。
注意:
- wait后会释放lock锁,等待别的线程notify它
- 上面的代码必须先写t2的等待,再写t1
- notify不用指定线程。也无法指定线程。对lock的notify cpu会自己找一个阻塞在lock上面的线程进行唤醒
然后上面这个程序还是存在一些问题:这个程序在t1唤醒t2时,t2并不会打印。而是等到t1运行退出时,t2才会打印。因为t2运行需要对lock进行加锁,而这把锁t1没运行完之前是不会释放的。所以上面的打印结果才出现t1结束后t2得到了打印。下面代码可以解决这个问题:
package com.gmail.fxding2019;
import java.util.ArrayList;
import java.util.List;
public class T17 {
//保持list的size可见性
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) {
T17 t = new T17();
//声明一把锁
final Object lock = new Object();
new Thread(()->{
System.out.println("t2启动");
synchronized (lock){
if (t.size() != 5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2结束");
//注意:必须在退出该锁之前唤醒其他阻塞在该锁上的线程
//t2打印完,释放lock,唤醒t1继续运行
lock.notify();
}
},"t2").start();
new Thread(()->{
synchronized (lock){
System.out.println("t1启动");
for (int i = 0; i<10; i++){
t.add(new Object());
System.out.println("add" + i);
if(t.size() == 5){
//通知t2
lock.notify();
try {
//阻塞t1,释放lock,等待t2拿到lock后打印
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
},"t1").start();
}
}
/*====================output========================
t2启动
t1启动
add0
add1
add2
add3
add4
t2结束
add5
add6
add7
add8
add9
*/
上面的程序逻辑为:
- t2先执行,遇到size!=5时调用wait等待,并释放对象的锁
- t1拿到锁后再执行,不断向数组中添加,等到size=5时,使用notify唤醒t2,并且自己调用wait进行阻塞,释放对象锁(注意:一定要先唤醒t1后再进行自我阻塞)
- 由于只有t2阻塞在该信号量上,所以notify会唤醒t2,进行打印。打印结束后,使用notify再唤醒t1
- t1得到锁之后,继续执行,直到结束
Case14(CountDownLatch——门闩):
package com.gmail.fxding2019;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class T18_interviewFinal {
//添加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) {
T18_interviewFinal c=new T18_interviewFinal();
//CountDownLatch就是门闩,当CountDownLatch为0时,门闩就开啦
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 e) {
e.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得以执行
//只要调用一次CountDownLatch,其值就会减一
latch.countDown();
}
//门闩打开之后,t1是可以向下正常运行的
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1结束");
},"t1").start();
}
}
使用Latch(门闩)替代wait notify来进行通知,好处是通信方式简单,同时也可以指定等待时间,使用await和countdown方法替代wait和notify,CountDownLatch不涉及锁定,当count的值为零是当前线程继续运行。当不涉及同步,只是涉及线程通信的时候,用synchronized+wait/notify就显得太重了的时候,时应该考虑CountDownLacth/cyclicbarrier/semaphore。
知识点:
CountDownLatch latch=new CountDownLatch(1);
CountDownLatch就是门闩,当CountDownLatch为0时,门闩就开啦,可以设置其初始值。- 门闩的等待是不需要锁定任何对象的,只要调用一次CountDownLatch,其值就会减一
参考:马士兵老师java多线程高并发编程