一、Synchronized关键字
首先贴上大神的博客:
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
-
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
-
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
接下来我们依次介绍synchronized关键字在这四种情形下的表现与区别:
1.修饰代码块
被修饰的代码块叫做同步语句块,我们先放demo,再来总结:
例一:
class MySync {
public int count1;
public int count2;
public MySync(){
count1=0;
count2=0;
}
public void youMethod( ){
synchronized (this){
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"--Synchronized:"+count1++);
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
for (int j=0;j<5;j++){
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--Non:"+count2++);
}
}
}
如上,我们在测试类中,写了一个测试方法。方法里,一部分代码块用synchronized关键字修饰为同步代码块。另一部分是非同步代码块。
调用:
public static void main(String[] args){
// lockCodeBlock();
myLockBlock();
}
...
public static void myLockBlock(){
MySync mySync=new MySync();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
mySync.youMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
mySync.youMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
也就是两个线程同时调用同一个实例的测试方法,这里注意,调用的是同一个实例的方法。
输出结果:(当然非同步代码块运行结果不唯一)
thread--1--Synchronized:0
thread--1--Synchronized:1
thread--1--Synchronized:2
thread--1--Synchronized:3
thread--1--Synchronized:4
thread--2--Synchronized:5
thread--1--Non:0
thread--2--Synchronized:6
thread--2--Synchronized:7
thread--1--Non:1
thread--2--Synchronized:8
thread--1--Non:2
thread--2--Synchronized:9
thread--1--Non:3
thread--1--Non:4
thread--2--Non:5
thread--2--Non:6
thread--2--Non:7
thread--2--Non:8
thread--2--Non:9
例二:
public static void myLock2(){
MySync mySync=new MySync();
MySync mySync1=new MySync();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
mySync.youMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
mySync1.youMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
执行结果:
thread--1--Synchronized:0
thread--2--Synchronized:0
thread--2--Synchronized:1
thread--1--Synchronized:1
thread--1--Synchronized:2
thread--2--Synchronized:2
thread--1--Synchronized:3
thread--2--Synchronized:3
thread--2--Synchronized:4
thread--1--Synchronized:4
注意:
此方法与例一不同处在于声明了Mysync两个对象。
我们需要知道的是,synchronized锁住的是对象,一个对象相关联的只有一把锁;
这里我们声明的是两个对象,相当于两个锁,这两个锁毫不相干,互不影响。所以thread1和thread2是同时进行的,谁也不锁谁。
summarize:
① 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
从上面可以看出,当线程一在访问同步代码块的时候,线程二只能进入阻塞状态,等待线程一执行完才开始执行。
②当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
从上面可以看出,当线程一的同步代码块执行完毕后,线程二的同步代码才开始执行,执行期间,线程一仍然可以访问对象的非synchronized代码块。
③synchronized锁住的是对象,一个对象对应一把锁,不同对象之间的锁互不影响。
2.修饰方法
demo:
class MySync2{
int count=0;
public MySync2(){
}
public synchronized void syncMethod(){
for (int i=0;i<5;i++){
try {
System.out.println(Thread.currentThread().getName()+":"+count++);
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
调用:
public static void synMethodTest(){
MySync2 sync=new MySync2();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
sync.syncMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
sync.syncMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
输出:
thread--1:0
thread--1:1
thread--1:2
thread--1:3
thread--1:4
thread--2:5
thread--2:6
thread--2:7
thread--2:8
thread--2:9
正如之前所说的,synchronized锁住的是当前对象;调用同一个对象的同步方法,有严格的执行顺序,当前会阻塞直到前面的调用完成,释放锁才可以开始调用。
如果是不同对象之间的:
public static void syncMethodTest2(){
MySync2 sync=new MySync2();
MySync2 sync2=new MySync2();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
sync.syncMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
sync2.syncMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
因为通过同步代码块的分析,这里调用的是两个对象,所以持有两把锁,应该是互不影响的:
thread--2:0
thread--1:0
thread--1:1
thread--2:1
thread--2:2
thread--1:2
thread--2:3
thread--1:3
thread--2:4
thread--1:4
结果如我们所料。
那如果我们在调用一个对象的同步方法时,同时调用起非同步方法呢?两个对象乃至多个对象之间的同步方法/非同步方法执行顺序又是怎样的呢?
我们做个测试:
在上例的Mysync类中,新增一个非同步方法:
public void notSyncMethod(){
for (int i=0;i<5;i++){
try {
System.out.println(Thread.currentThread().getName()+"--notSync:"+count2++);
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
接着在thread1和thread2中分别调用:
MySync2 sync=new MySync2();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
sync.notSyncMethod();
sync.syncMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
sync.syncMethod();
sync.notSyncMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
这里需要注意的是,我在两个thread中,同步方法和非同步方法调用顺序是不一样的。看一下执行结果:
thread--2:0
thread--1--notSync:100
thread--2:1
thread--1--notSync:101
thread--1--notSync:102
thread--2:2
thread--2:3
thread--1--notSync:103
thread--2:4
thread--1--notSync:104
thread--2--notSync:105
thread--1:5
thread--1:6
thread--2--notSync:106
thread--1:7
thread--2--notSync:107
thread--1:8
thread--2--notSync:108
thread--2--notSync:109
thread--1:9
根据结果,我们可以看出,thread1先拿到了锁,在thread1执行同步方法的时候,并不会阻塞其他调用去执行非同步方法。所以可以看出当thread2拿到锁之后,thread1的非同步方法是可以和thread2的同步方法并行的,但是thread1的同步方法则必须等到thread1的同步方法调用完成后,才开始调用。这里我们还要说明一点,类中的方法都是同步的!,所以在thread1中我们先调用了类的非同步方法,那么thread1的同步方法,必须在非同步方法执行完成后才开始执行!同理,thread2中的同步方法执行完成后,才开始调用非同步方法。然后又因为在访问同步方法时候,不阻塞对非同步方法的调用。所以thread1的同步方法还是可以和thread2的非同步方法并行。
这里建议大家可以多换几次顺序,多测几次记忆的更加深刻。
除了上面的,同步方法,还有一些要说的:
synchronized关键字不能继承。
如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法(也就是super.fun()),这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
3.修饰静态方法
静态方法是属于类而不是对象的。
调用方法:
public synchronized static void method() {
// todo
}
无论这个类有多少个对象,他们都使用同一把锁。
demo:
在Mysync类中增加一个同步静态方法:
public synchronized static void stataicSyncMethod(){
for (int i=0;i<5;i++){
try {
System.out.println(Thread.currentThread().getName()+":"+count3++);
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
接着调用:
public static void staticSyncTest(){
MySync2 sync=new MySync2();
MySync2 sync2=new MySync2();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
sync.stataicSyncMethod();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
sync2.stataicSyncMethod();
}
},"thread--2");
thread1.start();
thread2.start();
}
可以看到我们这里是两个对象,如果是非静态方法,那么就是两把锁了。而静态方法,就共用一把。
结果:
thread--1:0
thread--1:1
thread--1:2
thread--1:3
thread--1:4
thread--2:5
thread--2:6
thread--2:7
thread--2:8
thread--2:9
执行结果,也按照预期执行了。静态方法和非静态方法锁机制一定要搞清。
4.修饰类
使用方法:
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
demo:
新增类锁:
public void syncClass(){
synchronized (MySync2.class){
for (int i=0;i<5;i++){
try {
System.out.println(Thread.currentThread().getName()+":"+count++);
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
接着我们在三个线程中调用:
public static void syncClass(){
MySync2 sync=new MySync2();
MySync2 sync2=new MySync2();
MySync2 sync3=new MySync2();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
sync.syncClass();
}
},"thread--1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
sync2.syncClass();
}
},"thread--2");
Thread thread3=new Thread(new Runnable() {
@Override
public void run() {
sync3.syncClass();
}
},"thread--3");
thread1.start();
thread2.start();
thread3.start();
}
结果如图:
thread--1:0
thread--1:1
thread--1:2
thread--1:3
thread--1:4
thread--3:0
thread--3:1
thread--3:2
thread--3:3
thread--3:4
thread--2:0
thread--2:1
thread--2:2
thread--2:3
thread--2:4
结果如我所愿。(坑的是,我把class写成了this...然后耽误了两个小时也没查出来为啥,唉,果然该换眼镜了.)
还有两点我们需要说一下:
一、synchronized使用方式不光上面那些种,我们在项目中应该还会看到synchronized(object)这种方式:
public final Object lock=new Object();
synchronized (lock){
//todo
}
实际上,这得看我们的变量是什么类型的了:
①非静态变量,对象锁
②静态变量,类锁
跟静态方法/非静态方法一个样。
二、DCL单例模式
我们知道,单例模式有六种,最完美的有两种:静态内部类和枚举,这两种是没有缺陷的,而且写法也简单。仅次于两者的,就是双重锁机制了,使用方式如下:
private static volatile DCLSingleton mInstance=null;
public static DCLSingleton getInstance(){
if (mInstance==null){//节省效率
synchronized (DCLSingleton.class){
if (mInstance==null){//可能多个线程同时进入外部的检查,不锁上可能会创建多个实例
mInstance=new DCLSingleton();
//不是原子操作,jvm先给instance分配内存,第二步调用此类的构造方法
//来构造变量,第三步就将instance对象指向我们刚才分配的内存空间
//以上三步顺序不确定!!可能导致出错,解决方法给instance加上 volatile关键字
}
}
}
return mInstance;
}
需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,mInstance=new DCLSingleton(); 可以分解为3行伪代码
1.memory = allocate() //分配内存
- ctorInstanc(memory) //初始化对象
- instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从1-2-3排序为1-3-2。在多线程的情况下会出现以下问题。线程A在执行第5行代码时,B线程进来,而此时A执行了1和3,没有执行2,此时B线程判断instance不为null,直接返回一个未初始化的对象。使用的时候就会报错。
Summarize:
锁有对象锁和类锁两种;
synchronized实现对象锁方式有两种:
同步代码块,非静态方法
实现类锁的方式也有两种:
ClassName.class(跟同步代码块使用方式类似),静态方法
这里再提一下上面大神博客的总结:
A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
这里说了,可能造成死锁?其实我们手工制造一个死锁的方式有很多,参照博客:
(多种方式实现死锁
)
我们抽一种分析一下:
class DeadRunnable implements Runnable{
public DeadRunnable deadRunnable;
public String objName;
public DeadRunnable(String objName){
this.objName=objName;
}
public void setDeadRunnable(DeadRunnable dead){
this.deadRunnable =dead;
}
public synchronized void methodA(){
String name=Thread.currentThread().getName();
System.out.printf("线程%s:进入 methodA\n",name);
try{
Thread.sleep(1000);
}catch (Exception e){
}
System.out.printf("线程%s:尝试进入methodB\n",name);
deadRunnable.methodB();
System.out.printf("线程%s:离开methodB\n",name);
}
public synchronized void methodB(){
String name=Thread.currentThread().getName();
System.out.printf("线程%s:进入 methodB\n",name);
try{
Thread.sleep(100);
}catch (Exception e){
}
System.out.printf("线程%s:尝试进入methodA\n",name);
deadRunnable.methodA();
System.out.printf("线程%s:离开methodA",name);
}
@Override
public void run() {
if ("A".equals(objName)){
methodA();
}else{
methodB();
}
}
}
调用:
public static void deadLock(){
DeadRunnable runnableA=new DeadRunnable("A");
DeadRunnable runnableB=new DeadRunnable("B");
runnableA.setDeadRunnable(runnableB);
runnableB.setDeadRunnable(runnableA);
Thread thread1=new Thread(runnableA,"Thread--A");
Thread thread2=new Thread(runnableB,"Thread--B");
thread1.start();
thread2.start();
}
看一下输出结果:
线程Thread--A:进入 methodA
线程Thread--B:进入 methodB
线程Thread--B:尝试进入methodA
线程Thread--A:尝试进入methodB
我们这里就需要分析一下流程,看看到底为什么会这样?
可以看到我们在DeadRunnable里写了两个同步方法;同时,我们通过deadRunnable用户持有其他对象的引用,结果让其他对象来调用另一个方法。
这样,在我们的deadLock方法里,线程A访问DeadRunable的methodA,这时候,我们就获得了这个对象的锁,其他对象是不能够继续访问此对象的其他方法的,除非这个锁已经释放。在线程A获得对象runnableA的锁时,它内部要访问runnableB的methodB,但是我们线程B在访问metohdB的时候,也获得了runnableB的锁;此时,问题来了,线程A需要runnableB释放锁,来访问runnableB的methodB方法,而同时,线程B也需要runnableA释放锁来访问runnableA的methodA方法,两者都持有锁,也都依赖于对象锁释放来完成后续操作从而释放自身的锁。谁也不让谁,死锁也就来了。
所以还是要慎用synchronized。
二、volatile
再讲volatile之前,必须先了解一下Java内存模型: (JAVA内存模型
)
做一些简单的关键性信息摘取:
程序的执行过程:当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。(原因就在于内存和cpu处理速度的巨大差距)
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
并发编程主要有三个问题:
缓存一致性问题
多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
处理器优化和指令重排
那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化
。
除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排
。
针对以上问题,并发编程,为了保证数据的安全,需要满足以下三个特性:
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性即程序执行的顺序按照代码的先后顺序执行。
对比上面三个并发编程的问题,原子性对应的其实就是处理器优化,可见性对应的就是缓存一致性问题,有序性对应的就是指令重排。
Java内存模型(Java Memory Model):
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
Java内存模型的实现
在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
volatile的用法
volatile通常被比喻成”轻量级的synchronized“,也是Java并发编程中比较重要的一个关键字。和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
使用:
只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
需要说明的是:
synchronized可以保证原子性、有序性和可见性。而volatile却只能保证部分有序性和可见性。
可见性:
volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。
部分有序性:
volatile的禁止指令重排的特性从一定程度上保证了有序性。这里的有序性可以解释如下:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
ex:
int x=1;
int y=2;
volatile boolean ISMINE= false;
int z=3;
int m=4;
这里ISMINE的初始化,一定在x,y之后,z,m之前,不会说在指令重排的时候放到x或者y之前,z,m之后了。但是这里,x,y之间和z,m之间的顺序是不能保证的,所以,才说只能保证部分有序性。
原子性
CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
为什么volatile不能保证原子性呢?
Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
synchronized为了保证原子性,需要通过字节码指令monitorenter
和monitorexit
,但是volatile和这两个指令之间是没有任何关系的。
所以,volatile是不能保证原子性的。
三、CountDownLatch
用途:
比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CountDownLatch可以当做一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去减这个计数器里的值。
可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。
应用场景:
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。
例子:
举个例子,有三个工人在为老板干活,这个老板有一个习惯,就是当三个工人把一天的活都干完了的时候,他就来检查所有工人所干的活。记住这个条件:三个工人先全部干完活,老板才检查。所以在这里用Java代码设计两个类,Worker代表工人,Boss代表老板,具体的代码实现如下:
package org.zapldy.concurrent;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Worker implements Runnable{
private CountDownLatch downLatch;
private String name;
public Worker(CountDownLatch downLatch, String name){
this.downLatch = downLatch;
this.name = name;
}
public void run() {
this.doWork();
try{
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
}catch(InterruptedException ie){
}
System.out.println(this.name + "活干完了!");
this.downLatch.countDown();
}
private void doWork(){
System.out.println(this.name + "正在干活!");
}
}
package org.zapldy.concurrent;
import java.util.concurrent.CountDownLatch;
public class Boss implements Runnable {
private CountDownLatch downLatch;
public Boss(CountDownLatch downLatch){
this.downLatch = downLatch;
}
public void run() {
System.out.println("老板正在等所有的工人干完活......");
try {
this.downLatch.await();
} catch (InterruptedException e) {
}
System.out.println("工人活都干完了,老板开始检查了!");
}
}
package org.zapldy.concurrent;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(3);
Worker w1 = new Worker(latch,"张三");
Worker w2 = new Worker(latch,"李四");
Worker w3 = new Worker(latch,"王二");
Boss boss = new Boss(latch);
executor.execute(w3);
executor.execute(w2);
executor.execute(w1);
executor.execute(boss);
executor.shutdown();
}
}
输出结果:
王二正在干活!
李四正在干活!
老板正在等所有的工人干完活......
张三正在干活!
张三活干完了!
王二活干完了!
李四活干完了!
工人活都干完了,老板开始检查了!
网友评论