java并发编程

本文详细介绍了Java并发编程的基础知识,包括线程的启动、线程间通讯、锁机制(synchronized、ReentrantLock、读写锁)以及Atomic类的使用。同时探讨了线程安全的单例实现、并发容器(同步类容器与并发类容器)的选择,并分析了线程池的工作原理。通过对这些内容的深入理解,读者可以更好地掌握Java并发编程的核心概念和实战技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

一、线程基础

Thread类:

start():当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

run():当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。

sleep(1000):sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

yield():调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

join():假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。

interrupt():单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;interrupt方法不能中断正在运行中的线程,但是可以通过interrupt方法和isInterrupted()方法来停止正在运行的线程(不推荐,使用标志位替代)。

setDaemon()和isDaemon():用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

Runnable 接口:

Runnable只有一个抽象的run()方法,此方法是在运行时由JVM调用。

Runnable 接口可以被任何想要被一个线程运行的接口继承实现;继承 Runnable 接口的类必须有一个 run() 方法;
Runnable 接口被设计的目的是为那些当其处于活跃期时期望运行代码的对象提供一个公共的协议;处于活跃期简单的说就是一个线程已经启动但还没有结束
继承 Runnable 接口实现线程,不需继承 Thread;而将类本身作为 Thread 中的目标 target
Runnable 接口最好不要继承,除非开发者想要更好的扩展此接口的功能

synchronized:

可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"

* 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
 * 所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),
 * 
 * 在静态方法上加synchronized关键字,表示锁定.class类,类级别的锁(独占.class类)。

使用synchronized代码块减小锁的粒度,提高性能

 

使用synchronized代码块加锁,如下:

public void method1(){
		synchronized (this) {	//加当前对象锁
			try {
				System.out.println("do method1..");
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void method2(){		//加类锁
		synchronized (ObjectLock.class) {
			try {
				System.out.println("do method2..");
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	private Object lock = new Object();
	public void method3(){		//对任何对象加锁
		synchronized (lock) {
			try {
				System.out.println("do method3..");
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

 一个线程想要执行synchronized修饰的方法里的代码步骤:
         1 、尝试获得锁
         2 、如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,
                     而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

t1和t2两个线程访问synchronized修饰的方法是同步访问,没有被synchronized修饰的方法是异步访问:

         t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
         t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步

--------------------------------------------------------------------------------------------------------------------------------------------------------------

业务整体需要使用完整的synchronized,保持业务的原子性。例如get方法和set方法都需要加 synchronized修饰,保证原子性,防止出现脏读

---------------------------------------------------------------------------------------------------------------------------------------------------------------

synchronized(obj),obj对象作为锁,在程序中改变锁对象使obj指向另一个实例,锁对象改变导致不能同步。但同一对象属性的修改不会影响锁的情况,例如对obj对象的某个属性赋值后,不会影响锁的情况

------------------------------------------------------------------------------------------------------------------------------------------------------------------

死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况,以下代码会死锁:线程t1获得锁lock1,线程t2获取lock2,然后线程t1又想要获得锁lock1,线程t2想要获得锁lock2造成死锁,如下死锁代码:

    private static Object lock1 = new Object();
	private static Object lock2 = new Object();

	@Override
	public void run() {
		if(tag.equals("a")){
			synchronized (lock1) {
				try {
					System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock1执行");
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (lock2) {
					System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock2执行");
				}
			}
		}
		if(tag.equals("b")){
			synchronized (lock2) {
				try {
					System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock2执行");
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (lock1) {
					System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock1执行");
				}
			}
		}
	}

-----------------------------------------------------------------------------------------------------

 

二、线程间通讯

wait(),notify(),notifyAll() :

wait()、notify()、notifyAll() 都是Object的方法,需要配合synchronized关键字使用,wait()方法使线程等待,并且释放锁,notify()方法唤醒一个等待的线程,但是不释放锁,notifyAll()方法唤醒所有等待的线程。被notify()、notifyAll()方法唤醒的线程再次参与锁的竞争

volatile:

首先解释一下java内存模型如下:

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,此外每条线程还有自己的工作内存(Working Memory)。

线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。

并且,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成,线程、主内存、工作内存关系如下图:

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值

在读写volatile类型的变量发生的过程如下:

  • 第一:使用volatile关键字会强制将修改的值立即写入主存;
  • 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  • 第三:由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。

  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

总结volatile变量:

  • 可见性(最重要的特性)。被volatile修饰的变量是共享的,是对所有线程都可见的,对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性,另外任何复合操作都不能保证原子性,如a++,a = a+1, a = b

 

ThreadLocal:

为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

示例:


public class ConnThreadLocal {

    //ThreadLocal 变量
	public static ThreadLocal<String> th = new ThreadLocal<String>();
	
	public void setTh(String value){
		th.set(value);
	}
	public void getTh(){
		System.out.println(Thread.currentThread().getName() + ":" + this.th.get());
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		final ConnThreadLocal ct = new ConnThreadLocal();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				ct.setTh("张三");
				ct.getTh();
			}
		}, "t1");
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(1000);
					ct.getTh();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t2");
		
		t1.start();
		t2.start();
	}
	
}

运行结果:

从运行结果可以知道,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。正如t1线程赋值为“张三”,而不影响t2线程,因为t2线程获取值还是为null

 

三、重入锁ReentrantLock

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。

  • 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

  • 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁

ReentrantLock相比synchronized的额外功能

  • ReentrantLock可以实现公平锁。公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

             ReentrantLock lock = new ReentrantLock(true);

 

  • ReentrantLock可响应中断。当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

  • 获取锁时限时等待。ReentrantLock还给我们提供了获取锁限时等待的方法tryLock()锁嗅探,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

结合Condition实现等待通知机制

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

如:

    private ReentrantLock lock = new ReentrantLock();
	private Condition c1 = lock.newCondition();
	private Condition c2 = lock.newCondition();

ReentrantLock使用示例如下:


public class UseManyCondition {

	private ReentrantLock lock = new ReentrantLock();
	private Condition c1 = lock.newCondition();
	private Condition c2 = lock.newCondition();
	
	public void m1(){
		try {
			lock.lock();//加锁
			System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m1等待..");
			c1.await();//等待
			System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m1继续..");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();//释放锁
		}
	}
	
	public void m2(){
		try {
			lock.lock();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m2等待..");
			c1.await();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m2继续..");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void m3(){
		try {
			lock.lock();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m3等待..");
			c2.await();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m3继续..");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void m4(){
		try {
			lock.lock();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
			c1.signalAll();//唤醒所有等待
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void m5(){
		try {
			lock.lock();
			System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
			c2.signal();//唤醒一个等待
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		
		
		final UseManyCondition umc = new UseManyCondition();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				umc.m1();
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				umc.m2();
			}
		},"t2");
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				umc.m3();
			}
		},"t3");
		Thread t4 = new Thread(new Runnable() {
			@Override
			public void run() {
				umc.m4();
			}
		},"t4");
		Thread t5 = new Thread(new Runnable() {
			@Override
			public void run() {
				umc.m5();
			}
		},"t5");
		
		t1.start();	// c1
		t2.start();	// c1
		t3.start();	// c2
		

		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		t4.start();	// c1
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		t5.start();	// c2
		
	}
	

	
}

 

四、读写锁ReentrantReadWriteLock

ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

  读写锁内部维护了两个锁,一个用于读操作,一个用于写操作。所有 ReadWriteLock实现都必须保证 writeLock操作的内存同步效果也要保持与相关 readLock的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。

  ReentrantReadWriteLock支持以下功能:

    1)支持公平和非公平的获取锁的方式

    2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;

    3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;

    4)读取锁和写入锁都支持锁获取期间的中断

    5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。 

口诀:读读共享、读写互斥、写写互斥

使用示例如下:


public class UseReentrantReadWriteLock {

	private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
	private ReadLock readLock = rwLock.readLock();
	private WriteLock writeLock = rwLock.writeLock();
	
	public void read(){
		try {
			readLock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			readLock.unlock();
		}
	}
	
	public void write(){
		try {
			writeLock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writeLock.unlock();
		}
	}
	
	public void write1(){
		try {
			writeLock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writeLock.unlock();
		}
	}
	
	public static void main(String[] args) {
		
		final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
		
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				urrw.read();
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				urrw.read();
			}
		}, "t2");
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				urrw.write();
			}
		}, "t3");
		Thread t4 = new Thread(new Runnable() {
			@Override
			public void run() {
				urrw.write1();
			}
		}, "t4");		
		
//		t1.start();
//		t2.start();
//		
	//	t1.start(); // R 
	//	t3.start(); // W
		
		t3.start();
		t4.start();

		
	}
}

五、Atomic类:

在多线程或者并发环境中,我们常常会遇到这种情况 int i=0; i++ 稍有经验的同学都知道这种写法是线程不安全的,使用volatile关键字也不能保证符合操作的原子性。因此,为了达到线程安全的目的,我们通常会用synchronized来修饰对应的代码块。现在我们有了新的方法,就是使用J.U.C包下的atomic类。

atomic类是通过自旋CAS操作volatile变量实现的。

在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。

而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一种实现方式。这种方式就避免了直接使用内核状态的重量级锁。

下面是使用AtomicInteger示例:

/**
 * volatile关键字不具备synchronized关键字的原子性(同步)
 * @author alienware
 *
 */
public class VolatileNoAtomic extends Thread{
	//private static volatile int count;//volatile关键字不具备synchronized关键字的原子性(同步)
	private static AtomicInteger count = new AtomicInteger(0);
	private static void addCount(){
		for (int i = 0; i < 1000; i++) {
			//count++ ;//volatile的复合操作
			count.incrementAndGet();
		}
		System.out.println(count);
	}
	
	public void run(){
		addCount();
	}
	
	public static void main(String[] args) {
		
		VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
		for (int i = 0; i < 10; i++) {
			arr[i] = new VolatileNoAtomic();
		}
		
		for (int i = 0; i < 10; i++) {
			arr[i].start();
		}

        try{
			Thread.sleep(2000);
		}catch(InterruptedException e){
			
		}
		
		System.out.println("最终结果:"+count);

	}
	
	
	
	
}

执行代码后结果:

Atomic类的缺点

ABA问题

  对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。atomic包下AtomicStampedReference类实现了这种思路。Mysql中Innodb的多版本并发锁也是这个原理。

自旋问题

  atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

 

六、同步类容器、并发类容器:

同步类容器:

同步类容器:如古老的Vector、HashTable。这些容器的同步功能其实都是由JDK的Collections.synchronized***等工厂方法去创建实现的。其底层的源码中主要就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不能满足今天互联网时代高并发的需求,在保证线程安全的同时,也必须要有足够好的性能。

使用Collections.synchronizedMap可以给容器添加一把锁,保证返回的myMap是线程安全的。HashMap不是线程安全的,但是经过处理后就是线程安全的。例如:

Map<String,String> myMap=Collections.synchronizedMap(new HashMap<String,String>());

并发类容器:

jdk5.0以后提供了多种并发类容器来替代同步类容器从而改善性能。同步类容器的状态都是串行化的。它们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用的吞吐量。

并发类容器是专门针对并发设计的,使用ConcurrentHashMap来代替传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持,使用CopyOnWriteArrayList代替Vector,并发的CopyOnWriteArraySet、并发的Queue。ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,后者是阻塞形式的队列,具体实现Queue还有很多,例如:ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

ConcurrentMap接口下有两个重要的实现:ConcurrentHashMapConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)

ConcurrentHashMap容器内部使用段(segment)来表示这些不同的部分,每个段实际就是一个小的HashTable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成16个段,也就是最高支持16个线程的并发修改操作。这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能是非常好的。
 

CopyOnWrite容器

CopyOnWrite简称COW,是一种用于程序设计中的优化策略。JDK中的COW容器有两种:CopyOnWriteArrayListCopyOnWriteArraySet。COW容器非常有用,可以在非常多的并发场景中使用到。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行拷贝,复制出一个新的容器,然后在新的容器中添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。在写方法上加锁了,不会出现数据不一致的问题,多个写不能并发只能加锁。

适合于读多写少的场景。对于写多的场景,大量的内存数据拷贝,反而可能降低性能,此时可以考虑加锁的方式。
 

并发Queue

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪一种都继承自Queue。

ConcurrentLinkedQueue://高性能无阻塞无界队列:ConcurrentLinkedQueue

它是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则,头是最先加入的,尾是最近加入的,该队列不允许null元素。
ConcurrentLinkedQueue重要方法:
add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中,这两个方法没有任何区别)
poll()和peak()都是取头元素节点,区别在于前者会删除元素,后者不会

 

BlockingQueue接口:实现该接口的队列是阻塞队列

ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没有实现读写分离,也就意味着生产和消费不能同时进行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合非常适合使用。


LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行,它是一个无界队列。

PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,它也是一个无界的队列。
 

SynchronousQueue:它是一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并消费。(先某个线程调用take方法才能调用add方法),如下示例代码:

public static void main(String[] args) throws Exception {
	final SynchronousQueue<String> q = new SynchronousQueue<String>();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					System.out.println(q.take());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		t1.start();//t1线程先执行,因目前队列中无元素,处于阻塞状态
		Thread t2 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				q.add("asdasd");
			}
		});
		t2.start();	//t2线程后执行,向队列中添加元素,因为SynchronousQueue无缓存,所以直接被线程t1给take获取
	}

 

七、线程池:

Executors

通过Executors提供四种线程池,newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool。

1.newFixedThreadPool(int nThreads) 创建固定数目线程的线程池。

2.newCachedThreadPool()  创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线 程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

3.newSingleThreadExecutor()  创建一个单线程化的Executor。

4.newScheduledThreadPool(int corePoolSize)  创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

通过Executors提供四种线程池,newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool,其在java底层代码都是通过new 一个ThreadPoolExecutor对象,ThreadPoolExecutor如下构造函数:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
) 
{
    ....
}

 

corePoolSize(核心线程数或者可以说最小线程数),maximumPoolSize(最大线程数),workQueue(任务队列)之间关系。

  • 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  • 当线程池中线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 。

  • 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务。

  • 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理。

  • 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。

  • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

  • tip:workQueue是有界队列和无界队列都按上述过程(无界队列workQueue永远不会满需要注意

因此,通过ThreadPoolExecutor,也可以创建我们自定义的线程池。

 

八、多线程设计模式

 

Future模式:

Future模式是多线程开发中非常常见的一种设计模式。它的核心思想是异步调用。当我们需要调用一个函数方法时。如果这个函数执行很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。因此,我们可以让被调用者立即返回,让他在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获取需要的数据。

目前JDK已经有相应的实现,可以参考JDK中Future接口下的相关实现:
JDK中Future接口:

public interface Future<V> {

    /**
     * 用来取消任务,取消成功则返回true,取消失败则返回false。
     * mayInterruptIfRunning参数表示是否允许取消正在执行却没有执行完毕的任务,设为true,则表示可以取消正在执行过程中的任务。
     * 如果任务已完成,则无论mayInterruptIfRunning为true还是false,此方法都返回false,即如果取消已经完成的任务会返回false;
     * 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
     * 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * 表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true
     */
    boolean isCancelled();

    /**
     * 表示任务是否已经完成,若任务完成,则返回true
     */
    boolean isDone();

    /**
     * 获取执行结果,如果最终结果还没得出该方法会产生阻塞,直到任务执行完毕返回结果
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * 获取执行结果,如果在指定时间内,还没获取到结果,则抛出TimeoutException
     */
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

从上面源码可看出Future就是对于Runnable或Callable任务的执行进行查询、中断任务、获取结果。

 

Future因为是接口不能直接用来创建对象,就有了下面的FutureTask。

FutureTask的实现为:

public class FutureTask<V> implements RunnableFuture<V>

可以看到FutureTask类实现了RunnableFuture接口,接着看RunnableFuture接口源码:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

可以看到RunnableFuture接口继承了Runnable接口和Future接口,也就是说其实FutureTask既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。

 

FutureTask使用实例:

public class FutureTest {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Calc task = new Calc();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        System.out.println("----------------");
        Integer integer = futureTask.get();
        System.out.println(integer);
        executor.shutdown();
    }

    public static class Calc implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return cal(100000000);
        }

        public static int cal (int num) {
            int sum = 0;
            for (int i = 0; i < num; i++) {
                sum += i;
            }
            return sum;
        }
    }
}

以上就是Future模式,有兴趣可以手动实现一个Future模式而不用JDK提供的Future

 

 

Master-Worker模式:

Master-Worker模式是一种使用多线程进行数据结构处理的结构。

Master-Worker模式的好处,它能够将一个大任务分解成若干个小任务,并且执行,从而提高系统的吞吐量。而对于系统请求者Client来说,任务一旦提交,Master进程会分配任务并立即返回,并不会等待系统全部处理完成后再返回,其处理过程是异步的。因此Client不会出现等待现象。

 

 

Master-Worker模式的简单实现如下:


//任务类
public class Task {

	private int id;
	private int price ;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	} 
	
}



//Master 类
public class Master {

	//1 有一个盛放任务的容器
	private ConcurrentLinkedQueue<Task> workQueue = new ConcurrentLinkedQueue<Task>();
	
	//2 需要有一个盛放worker的集合
	private HashMap<String, Thread> workers = new HashMap<String, Thread>();
	
	//3 需要有一个盛放每一个worker执行任务的结果集合
	private ConcurrentHashMap<String, Object> resultMap = new ConcurrentHashMap<String, Object>();
	
	//4 构造方法
	public Master(Worker worker , int workerCount){
		worker.setWorkQueue(this.workQueue);
		worker.setResultMap(this.resultMap);
		
		for(int i = 0; i < workerCount; i ++){
			this.workers.put(Integer.toString(i), new Thread(worker));
		}
		
	}
	
	//5 需要一个提交任务的方法
	public void submit(Task task){
		this.workQueue.add(task);
	}
	
	//6 需要有一个执行的方法,启动所有的worker方法去执行任务
	public void execute(){
		for(Map.Entry<String, Thread> me : workers.entrySet()){
			me.getValue().start();
		}
	}

	//7 判断是否运行结束的方法
	public boolean isComplete() {
		for(Map.Entry<String, Thread> me : workers.entrySet()){
			if(me.getValue().getState() != Thread.State.TERMINATED){
				return false;
			}
		}		
		return true;
	}

	//8 计算结果方法
	public int getResult() {
		int priceResult = 0;
		for(Map.Entry<String, Object> me : resultMap.entrySet()){
			priceResult += (Integer)me.getValue();
		}
		return priceResult;
	}
	
	
}

//Worker类

public class Worker implements Runnable {

    //任务队列,在Master类构造函数初始化
	private ConcurrentLinkedQueue<Task> workQueue;

    //返回结果集,在Master类构造函数初始化
	private ConcurrentHashMap<String, Object> resultMap;
	
	public void setWorkQueue(ConcurrentLinkedQueue<Task> workQueue) {
		this.workQueue = workQueue;
	}

	public void setResultMap(ConcurrentHashMap<String, Object> resultMap) {
		this.resultMap = resultMap;
	}
	
	@Override
	public void run() {
		while(true){
			Task input = this.workQueue.poll();
			if(input == null) break;
			Object output = handle(input);
			this.resultMap.put(Integer.toString(input.getId()), output);
		}
	}

	private Object handle(Task input) {
		Object output = null;
		try {
			//处理任务的耗时。。 比如说进行操作数据库。。。
			Thread.sleep(500);
			output = input.getPrice();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return output;
	}



}


//测试类,测试Master-Worker模式:

public class Main {

	public static void main(String[] args) {
		
		Master master = new Master(new Worker(), 20);
		
		Random r = new Random();
		for(int i = 1; i <= 100; i++){
			Task t = new Task();
			t.setId(i);
			t.setPrice(r.nextInt(1000));
			master.submit(t);
		}
		master.execute();
		long start = System.currentTimeMillis();
		
		while(true){
			if(master.isComplete()){
				long end = System.currentTimeMillis() - start;
				int priceResult = master.getResult();
				System.out.println("最终结果:" + priceResult + ", 执行时间:" + end);
				break;
			}
		}
		
	}
}






---------------------------------------------------------------------------------------------------------------------------------------------------------------

九、线程安全的单例:

静态内部类实现多线程单例:


public class Singletion {
	
	private static class InnerSingletion {
		private static Singletion single = new Singletion();
	}
	
	public static Singletion getInstance(){
		return InnerSingletion.single;
	}
	
}

多线程单例另一个懒汉模式实现:


public class DubbleSingleton {

	private static DubbleSingleton ds;
	
	public  static DubbleSingleton getDs(){
		if(ds == null){
			try {
				//模拟初始化对象的准备时间...
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (DubbleSingleton.class) {
				if(ds == null){
					ds = new DubbleSingleton();
				}
			}
		}
		return ds;
	}
	
	/*public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		},"t1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		},"t2");
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		},"t3");
		
		t1.start();
		t2.start();
		t3.start();
	}*/
	
}

 

十、CountDownLatch、Semaphore和CyclicBarrier

CountDownLatch:

 

CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。

调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。

在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作;典型的应用如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。

例子:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class CountDownLatchExample1 {

    private final static int threadCount = 200;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        Thread.sleep(100);
        log.info("{}", threadNum);
        Thread.sleep(100);
    }
}

运行结果: main主线程是最后执行的

20:18:32.917 [pool-1-thread-7] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 6
20:18:32.917 [pool-1-thread-6] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 5
20:18:32.919 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 4
20:18:32.918 [pool-1-thread-1] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 0
20:18:32.918 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 2
20:18:32.916 [pool-1-thread-9] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 8
20:18:32.918 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 3
20:18:32.916 [pool-1-thread-10] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 9
20:18:32.916 [pool-1-thread-8] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 7
20:18:32.917 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - 1
20:18:33.032 [main] INFO com.mmall.concurrency.example.aqs.CountDownLatchExample1 - finish

 

 

Semaphore:

Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:

用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。

 

例子:

@Slf4j
public class SemaphoreExample1 {

    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();
       // 每次最多三个线程获取许可
        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    semaphore.acquire(); // 获取一个许可,获取多个semaphore.acquire(3);
                    test(threadNum);
                    semaphore.release(); // 释放一个许可,释放多个semaphore.release(3);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}

分析:new Semaphore(3)定义了3个信号量,每个线程执行的时候先获取信号量,获取到信号量的线程才会往下执行,没获取到信号量的线程就进行阻塞,用于限流的首选择

 

CyclicBarrier:

CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。类似于CountDownLatch,它也是通过计数器来实现的。当某个线程调用await方法时,该线程进入等待状态,且计数器加1,当计数器的值达到设置的初始值时,所有因调用await进入等待状态的线程被唤醒,继续执行后续操作。因为CycliBarrier在释放等待线程后可以重用,所以称为循环barrier。CycliBarrier支持一个可选的Runnable,在计数器的值到达设定值后(但在释放所有线程之前),该Runnable运行一次,注,Runnable在每个屏障点只运行一个。

 

使用场景类似于CountDownLatch与CountDownLatch的区别:

  • CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作,描述的是1个线程或N个线程等待其他线程的关系。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作,描述的多个线程内部相互等待的关系。
  • CountDownLatch是一次性的,而CyclicBarrier则可以被重置而重复使用。

使用如下:

@Slf4j
public class CyclicBarrierExample1 {

    private static CyclicBarrier barrier = new CyclicBarrier(5);

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}

运行结果: 每5个准备好之后,才各自往下执行自己的线程。

20:24:34.616 [pool-1-thread-1] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 0 is ready
20:24:35.610 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 1 is ready
20:24:36.610 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 2 is ready
20:24:37.611 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 3 is ready
20:24:38.612 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 4 is ready
20:24:38.612 [pool-1-thread-1] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 0 continue
20:24:38.612 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 1 continue
20:24:38.612 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 4 continue
20:24:38.612 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 3 continue
20:24:38.612 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 2 continue
20:24:39.614 [pool-1-thread-6] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 5 is ready
20:24:40.613 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 6 is ready
20:24:41.614 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 7 is ready
20:24:42.615 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 8 is ready
20:24:43.615 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 9 is ready
20:24:43.615 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 9 continue
20:24:43.615 [pool-1-thread-6] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 5 continue
20:24:43.615 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 6 continue
20:24:43.615 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 7 continue
20:24:43.615 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.CyclicBarrierExample1 - 8 continue

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值