JUC学习笔记及拓展

2023-11-11

本文为自己整理的学习笔记及学习心得,大纲取自尚硅谷的JUC视频,感兴趣的小伙伴可以去B站自学。

JUC学习笔记及拓展

Java JUC

1 Java JUC简介

在 Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用 的实用工具类,用于定义类似于线程的自定义子 系统,包括线程池、异步 IO 和轻量级任务框架。 提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。


2 volatile 关键字-内存可见性

2.1 内存可见性

Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。

在这里插入图片描述

JVM 模型规定:1) 线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写; 2) 不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。这样的规定可能导致得到后果是:线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。这就引出了内存可见性。

内存可见性(Memory Visibility)是指当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

public class TestVolatile {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();
		
		while(true){
			if(td.isFlag()){
				System.out.println("------------------");
				break;
			}
		}
		
	}
}

class ThreadDemo implements Runnable {

	private boolean flag = false;

	@Override
	public void run() {
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;
		
		System.out.println("flag=" + isFlag());

	}
	public boolean isFlag() {
		return flag;
	}
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}
//输出:
//flag=true

2.2 volatile 关键字

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其它线程。当把共享变量声明为 volatile 类型后,线程对该变量修改时会将该变量的值立即刷新回主内存,同时会使其它线程中缓存的该变量无效,从而其它线程在读取该值时会从主内中重新读取该值(参考缓存一致性)。因此在读取 volatile 类型的变量时总是会返回最新写入的值。

volatile屏蔽掉了JVM中必要的代码优化(指令重排序),所以在效率上比较低

//如果设置为	
private volatile boolean flag = false;
//输出结果:
flag=true
------------------

volatile关键字最主要的作用是:

  1. 保证变量的内存可见性
  2. 局部阻止重排序的发生

可以将 volatile 看做一个轻量级的锁,但是又与 锁有些不同:

  1. 对于多线程,不是一种互斥关系
  2. 不能保证变量状态的“原子性操作“

3 原子变量与CAS算法

3.1 原子变量

3.1.1 i++的原子性问题
public class TestAtomicDemo {
	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
}
class AtomicDemo implements Runnable{
//	private volatile int serialNumber = 0;
	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		System.out.print(getSerialNumber()+" ");
	}
	public int getSerialNumber(){
		return serialNumber.getAndIncrement();
	}
}
//运行结果
//1 3 2 0 4 6 5 7 8 9 ——> 不会重复
//如果改为:
class AtomicDemo implements Runnable{
	private volatile int serialNumber = 0;
//	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		System.out.print(getSerialNumber()+" ");
	}
	public int getSerialNumber(){
		return serialNumber++;
//		return serialNumber.getAndIncrement();
	}
}
//运行结果:
//0 4 3 2 1 0 5 6 7 8 ——> 会产生重复
3.1.2 原子变量

实现全局自增id最简单有效的方式是什么?java.util.concurrent.atomic包定义了一些常见类型的原子变量。这些原子变量为我们提供了一种操作单一变量无锁(lock-free)的线程安全(thread-safe)方式。

实际上该包下面的类为我们提供了类似volatile变量的特性,同时还提供了诸如boolean compareAndSet(expectedValue, updateValue)的功能。

不使用锁实现线程安全听起来似乎很不可思议,这其实是通过CPU的compare and swap指令实现的,由于硬件指令支持当然不需要加锁了。

核心方法:boolean compareAndSet(expectedValue, updateValue)

  • 原子变量类的命名类似于AtomicXxx,例如,AtomicInteger类用于表示一个int变量。

  • 标量原子变量类

    AtomicInteger,AtomicLong和AtomicBoolean类分别支持对原始数据类型int,long和boolean的操作。

    当引用变量需要以原子方式更新时,AtomicReference类用于处理引用数据类型。

  • 原子数组类

    有三个类称为AtomicIntegerArray,AtomicLongArray和AtomicReferenceArray,它们表示一个int,long和引用类型的数组,其元素可以进行原子性更新。

3.2 CAS算法

  • Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁

  • CAS 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

  • CAS 是一种无锁的非阻塞算法的实现。

  • CAS 包含了 3 个操作数:

    • 需要读写的内存值 V
    • 进行比较的值 A
    • 拟写入的新值 B

    当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V的值,否则不会执行任何操作。

在这里插入图片描述

3.2.1 ABA问题

CAS会导致ABA问题,线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。

解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号

3.2.2 CAS在JAVA中底层的实现

Unsafe

Unsafe类:Java 与 C/C++ 的一个非常明显区别就是,Java 中不可以直接操作内存。当然这并不完全正确,因为 Unsafe 就可以做到。

Unsafe在AtomicInteger中的应用:

class AtomicDemo implements Runnable{
	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		System.out.print(getSerialNumber()+" ");
	}
	public int getSerialNumber(){
		return serialNumber.incrementAndGet();
	}
}
public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Unsafe:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

运用:

package com.mashibing.jol;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class T02_TestUnsafe {

    int i = 0;
    private static T02_TestUnsafe t = new T02_TestUnsafe();

    public static void main(String[] args) throws Exception {
        //Unsafe unsafe = Unsafe.getUnsafe();

        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        Field f = T02_TestUnsafe.class.getDeclaredField("i");
        long offset = unsafe.objectFieldOffset(f);
        System.out.println(offset);

        boolean success = unsafe.compareAndSwapInt(t, offset, 0, 1);
        System.out.println(success);
        System.out.println(t.i);
        //unsafe.compareAndSwapInt()
    }
}

jdk8u: unsafe.cpp:

cmpxchg = compare and exchange

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

jdk8u: atomic_linux_x86.inline.hpp

is_MP = Multi Processor

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

jdk8u: os.hpp is_MP()

  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

jdk8u: atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

最终实现:

cmpxchg = cas修改变量值

lock cmpxchg 指令   //记住这条指令

cmpxchg不能保证原子性,lock保证了原子性(当执行cmpxchg指令时,其他CPU不允许对里面的值进行修改)。

CAS在JAVA中底层的实现是通过 lock cmpxchg来实现的

volatile和synchronized的实现也都跟这条指令有关


4 ConcurrentHashMap

ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对于多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段” 机制替代 Hashtable 的独占锁,进而提高性能。

4.1 锁粒度

减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为 ConcurrentHashMap 的并发度。

4.2 锁分段

ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下 一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。

如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首 先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程 环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

4.3 其它

此包还提供了设计用于多线程上下文中的 Collection 实现:

ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、 CopyOnWriteArrayList 和 CopyOnWriteArraySet。

  • 当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap, ConcurrentSkipListMap 通常优于同步的 TreeMap。

  • 当期望的读数和遍历远远 大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

4.4 写入并复制

注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。

public class TestCopyOnWriteArrayList {

	public static void main(String[] args) {
		HelloThread ht = new HelloThread();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ht).start();
		}
	}
}

class HelloThread implements Runnable{
	
	private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
//	private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
	static{
		list.add("AA");
		list.add("BB");
		list.add("CC");
	}
	@Override
	public void run() {
		Iterator<String> it = list.iterator();		
		while(it.hasNext()){
			System.out.println(it.next());
			list.add("AA");
		}
	}
}

运行会造成并发修改异常ConcurrentModificationException。遍历的列表和添加的都是同一个。

//	private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
	private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

这样不会报错,正常运行。**因为在每次写入时,都会在底层完成一次复制,复制一份新的列表,然后再进行添加。每次写入都会复制。**不会造成并发修改异常,但是效率较低。

添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。


5 CountDownLatch(闭锁)

5.1 概念

CountDownLatch(闭锁)——一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDown(倒数)latch(锁)

用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。

闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活 动直到其他活动都完成才继续执行:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
  • 等待直到某个操作所有参与者都准备就绪再继续执行。
5.2 方法介绍

CountDownLatch最重要的方法是countDown()——倒数 和 await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。

public class TestCountDownLatch {

	public static void main(String[] args) {
		final CountDownLatch latch = new CountDownLatch(50);  //每次有个线程执行完-1,设置初始值50
		LatchDemo ld = new LatchDemo(latch);

		long start = System.currentTimeMillis();
        
		for (int i = 0; i < 50; i++) {
			new Thread(ld).start();
		}

		try {
			latch.await();   //50个线程执行完,才继续执行main线程
		} catch (InterruptedException e) {
		}
        
		long end = System.currentTimeMillis();
		System.out.println("耗费时间为:" + (end - start));
	}
}

class LatchDemo implements Runnable {
    
	private CountDownLatch latch;
	public LatchDemo(CountDownLatch latch) {
		this.latch = latch;
	}
    
	@Override
	public void run() {
		try {
			for (int i = 0; i < 50000; i++) {
				if (i % 2 == 0) {
					System.out.println(i);
				}
			}
		} finally {
			latch.countDown();    //每次执行完-1,放在finally里确保每次都执行
		}
	}
}

6 实现 Callable 接口

Java 5.0 在 java.util.concurrent 提供了一个新的创建执行线程的方式:Callable 接口

Callable 需要依赖FutureTask ,FutureTask 也可以用作闭 锁。

6.1 创建线程的四种方式

无返回:

  1. 实现Runnable接口,重写run();
  2. 继承Thread类,重写run();

有返回:

  1. 实现Callable接口,重写call(),利用FutureTask包装Callable,并作为task传入Thread构造函数;
  2. 利用线程池;

6.2 Callable的使用

public class TestCallable {
	
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		
		//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
		FutureTask<Integer> result = new FutureTask<>(td);
		
		new Thread(result).start();
		
		//2.接收线程运算后的结果
		try {
			Integer sum = result.get();  //FutureTask 可用于 闭锁
			System.out.println(sum);
			System.out.println("------------------------------------");
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}
}

class ThreadDemo implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for (int i = 0; i <= 100000; i++) {
			sum += i;
		}
		return sum;
	}
}

7 -Lock 同步锁

在 Java 5.0 之前,协调共享对象的访问时可以使用的机制只有 synchronized 和 volatile 。Java 5.0 后增加了一些新的机制,但并不是一种替代内置锁的方法,而是当内 置锁不适用时,作为一种可选择的高级功能。

ReentrantLock 实现了 Lock 接口,并提供了与 synchronized 相同的互斥性和内存可见性。但相较于 synchronized 提供了更高的处理锁的灵活性。

解决多线程安全问题的三种方式

  • jdk 1.5 前:

    • synchronized:隐式锁

      1.同步代码块

      2.同步方法

  • jdk 1.5 后:

    • 3.同步锁 Lock:显式锁

      注意:是一个显示锁,需要通过 lock() 方法上锁,必须通过 unlock() 方法进行释放锁

public class TestLock {
	
	public static void main(String[] args) {
		Ticket ticket = new Ticket();
		
		new Thread(ticket, "1号窗口").start();
		new Thread(ticket, "2号窗口").start();
		new Thread(ticket, "3号窗口").start();
	}

}

class Ticket implements Runnable{
	
	private int tick = 100;
	
	private Lock lock = new ReentrantLock();

	@Override
	public void run() {
		while(true){
			
			lock.lock(); //上锁
			
			try{
				if(tick > 0){
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
					}
					
					System.out.println(Thread.currentThread().getName() + " 完成售票,余票为:" + --tick);
				}
			}finally{
				lock.unlock(); //必须执行 因此放在finally中 释放锁
			}
		}
	}
	
}

7.1 Lock 与 Synchronized的区别

  • Lock是接口,Synchronized是关键字。
  • Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以必须要在finally中实现释放锁。
  • Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。

7.2 ReentrantLock 与 Synchronized的区别

  • ReentrantLock 是类(是Lock的实现类),Synchronized是关键字。
  • ReentrantLock 可以对获取锁的等待时间进行设置, 这样就避免了死锁
  • ReentrantLock必须手动释放锁,并且只能修饰代码块。而synchronized不用手动释放锁,除此之外可以修饰方法。
  • ReentrantLock 可以实现公平锁,而 Synchronized 不能。(两个默认都是非公平锁)

补充:ReentrantLock 与 Synchronized 都是可重入锁


8 Condition 控制线程通信

Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用 法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是 await、signal 和 signalAll

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

8.1 使用Condition

使用Condition控制线程通信:

  1. 如果不使用synchronized关键字保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait() notify() notifyAll()来进行线程通信了
  2. 当使用lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到lock对象却无法继续执行的线程释放lock对象,Condition对象也可以唤醒其他处于等待状态的进程。
  3. Condition实例被绑定在一个Lock对象上。要获得Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。

8.2 生产者和消费者案例

public class TestProductorAndConsumerForLock {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer con = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(con, "消费者 B").start();

//		 new Thread(pro, "生产者 C").start();
//		 new Thread(con, "消费者 D").start();
	}

}

class Clerk {
	private int product = 0;

	private Lock lock = new ReentrantLock();  //创建lock对象
	private Condition condition = lock.newCondition();  //获得Lock实例的Condition实例
	// 进货
	public void get() {
		lock.lock();

		try {
			if (product >= 1) { // 为了避免虚假唤醒,应该总是使用在循环中。
				System.out.println("产品已满!");
				try {
					condition.await();
				} catch (InterruptedException e) {
				}

			}
			System.out.println(Thread.currentThread().getName() + " : " + ++product);

			condition.signalAll();
		} finally {
			lock.unlock();
		}

	}

	//售货
	public void sale() {
		lock.lock();
		try {
			if (product <= 0) {
				System.out.println("缺货!");

				try {
					condition.await();
				} catch (InterruptedException e) {
				}
			}

			System.out.println(Thread.currentThread().getName() + " : " + --product);

			condition.signalAll();

		} finally {
			lock.unlock();
		}
	}
}

// 生产者
class Productor implements Runnable {

	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.get();   //调用店员进货方法
		}
	}
}

// 消费者
class Consumer implements Runnable {

	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();	//调用店员售货方法
		}
	}

}

在这里插入图片描述


9 线程按序交替

要求:编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要 求输出的结果必须按顺序显示。 如:ABCABCABC…… 依次递归

public class TestABCAlternate {
	
	public static void main(String[] args) {
		AlternateDemo ad = new AlternateDemo();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.loopA(i);
				}
				
			}
		}, "A").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.loopB(i);
				}
				
			}
		}, "B").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.loopC(i);
					
					System.out.println("-----------------------------------");
				}
				
			}
		}, "C").start();
	}

}

class AlternateDemo{
	
	private int number = 1; //当前正在执行线程的标记
	
	private Lock lock = new ReentrantLock();
	private Condition condition1 = lock.newCondition();
	private Condition condition2 = lock.newCondition();
	private Condition condition3 = lock.newCondition();
	
	/**
	 * @param totalLoop : 循环第几轮
	 */
	public void loopA(int totalLoop){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 1){
				condition1.await();   //线程A等待
			}
			
			//2. 打印
			for (int i = 1; i <= 1; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}
			
			//3. 唤醒
			number = 2;
			condition2.signal();  //唤醒B线程
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void loopB(int totalLoop){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 2){
				condition2.await();		//线程B等待
			}
			
			//2. 打印
			for (int i = 1; i <= 1; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}
			
			//3. 唤醒
			number = 3;
			condition3.signal();		//唤醒C线程
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void loopC(int totalLoop){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 3){
				condition3.await();		//C线程等待
			}
			
			//2. 打印
			for (int i = 1; i <= 1; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}
			
			//3. 唤醒
			number = 1;
			condition1.signal();		//唤醒A线程
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
}

在这里插入图片描述


10 ReadWriteLock 读写锁

ReadWriteLock 维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作时,必须独占方式来获取锁。对于读取操作占多数的数据结构。 ReadWriteLock 能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性 可以完全不需要考虑加锁操作。

  • 写写/读写 需要“互斥”
  • 读读 不需要互斥
public class TestReadWriteLock {

	public static void main(String[] args) {
		ReadWriteLockDemo rw = new ReadWriteLockDemo();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				rw.set((int)(Math.random() * 101));
			}
		}, "Write:").start();
		
		
		for (int i = 0; i < 100; i++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					rw.get();
				}
			}).start();
		}
	}
	
}

class ReadWriteLockDemo{
	
	private int number = 0;
	
	private ReadWriteLock lock = new ReentrantReadWriteLock();
	
	//读
	public void get(){
		lock.readLock().lock(); //上锁
		
		try{
			System.out.println(Thread.currentThread().getName() + " : " + number);
		}finally{
			lock.readLock().unlock(); //释放锁
		}
	}
	
	//写
	public void set(int number){
		lock.writeLock().lock();
		
		try{
			System.out.println(Thread.currentThread().getName());
			this.number = number;
		}finally{
			lock.writeLock().unlock();
		}
	}
}

11 线程8锁

判断打印的 “one” or “two” ?

    1. 两个普通同步方法,两个线程,标准打印, 打印结果?
    1. 新增 Thread.sleep() 给 getOne(),打印结果?
    1. 新增普通方法 getThree() , 打印结果?
    1. 两个普通同步方法,两个 Number 对象,打印结果?
    1. 修改 getOne() 为静态同步方法,打印结果?
    1. 修改两个方法均为静态同步方法,一个 Number 对象,打印结果?
    1. 一个静态同步方法,一个非静态同步方法,两个 Number 对象,打印结果?
    1. 两个静态同步方法,两个 Number 对象,打印结果?

要想知道上面线程8锁的答案,需要知晓关键所在:

  • ① 非静态方法的锁默认为 this(实例对象), 静态方法的锁为对应的 Class 对象(类对象)。
  • ② 某一个时刻,同一个对象,只能有一个线程持有锁,无论几个方法。
  • ③ 锁静态方法,某一个时刻,不同实例对象也只能有一个对象持有锁。
public class TestThread8Monitor {
	
	public static void main(String[] args) {
		Number number = new Number();
		Number number2 = new Number();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			} 
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
//				number.getTwo();
				number2.getTwo();
			}
		}).start();
		
//		new Thread(new Runnable() {
//			@Override
//			public void run() {
//				number.getThree();
//			}
//		}).start();
		
	}

}

class Number{
	
	public static synchronized void getOne(){
		try {
			Thread.sleep(3000);
            System.out.println("--过了3秒--");
		} catch (InterruptedException e) {
		}
		
		System.out.println("one");
	}
	
	public static synchronized void getTwo(){
		System.out.println("two");
	}
	
	public void getThree(){
		System.out.println("three");
	}
	
}

答案:

  1. 两个普通同步方法,两个线程,一个 Number 对象,标准打印, 打印结果? //one two
  2. 新增 Thread.sleep() 给 getOne() ,打印结果? // --过了3秒-- one two
  3. 新增普通方法 getThree() , 打印结果? //three --过了3秒-- one two
  4. 两个普通同步方法,两个 Number 对象,打印结果? //two --过了3秒-- one
  5. 修改 getOne() 为静态同步方法,打印结果? //two --过了3秒-- one
  6. 修改两个方法均为静态同步方法,一个 Number 对象,打印结果? //–过了3秒-- one two
  7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象,打印结果? //two --过了3秒-- one
  8. 两个静态同步方法,两个 Number 对象,打印结果? //–过了3秒-- one two

12 线程池

12.1 线程池介绍

第四种获取线程的方法:线程池。线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。通常使用 Executors 工厂方法配置

线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。

12.2 线程池的体系结构

/*
 * java.util.concurrent.Executor : 负责线程的使用与调度的根接口
 *    |--ExecutorService 子接口: 线程池的主要接口
 *       |--ThreadPoolExecutor 线程池的实现类
 *       |--ScheduledExecutorService 子接口:负责线程的调度
 *          |--ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
 */

12.3 工具类 : Executors

为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用较为方便的 Executors 工厂方法 :

  • Executors newCachedThreadPool()(缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量,可以进行自动线程回收)

  • Executors newFixedThreadPool(int)(创建固定大小的线程池)

  • Executors newSingleThreadExecutor()(线程池中只有一个线程)

  • ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。

public class TestThreadPool {
	
	public static void main(String[] args) throws Exception {
		//1. 创建线程池
		ExecutorService pool = Executors.newFixedThreadPool(5);
		
		List<Future<Integer>> list = new ArrayList<>();
		
		for (int i = 0; i < 10; i++) {
			Future<Integer> future = pool.submit(new Callable<Integer>(){

				@Override
				public Integer call() throws Exception {
					int sum = 0;
					
					for (int i = 0; i <= 100; i++) {
						sum += i;
					}
					return sum;
				}
				
			});

			list.add(future);
		}
		
		pool.shutdown();
		
		for (Future<Integer> future : list) {
			System.out.println(future.get());
		}
		
		
		/*ThreadPoolDemo tpd = new ThreadPoolDemo();
		
		//2. 为线程池中的线程分配任务
		for (int i = 0; i < 10; i++) {
			pool.submit(tpd);
		}
		
		//3. 关闭线程池
		pool.shutdown();*/
	}
	
//	new Thread(tpd).start();
//	new Thread(tpd).start();
}

//class ThreadPoolDemo implements Runnable{
//
//	private int i = 0;
//	
//	@Override
//	public void run() {
//		while(i <= 100){
//			System.out.println(Thread.currentThread().getName() + " : " + i++);
//		}
//	}
//	
//}

12.4 线程调度

  • ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务
public class TestScheduledThreadPool {

	public static void main(String[] args) throws Exception {
		ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
		
		for (int i = 0; i < 5; i++) {
			Future<Integer> result = pool.schedule(new Callable<Integer>(){

				@Override
				public Integer call() throws Exception {
					int num = new Random().nextInt(100);//生成随机数
					System.out.println(Thread.currentThread().getName() + " : " + num);
					return num;
				}
				
			}, 1, TimeUnit.SECONDS);   //延迟线程,延迟时间,时间单位
			
			System.out.println(result.get());
		}
		
		pool.shutdown();
	}
	
}

13 ForkJoinPool 分支/合并框架 工作窃取

13.1 Fork/Join 框架

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成 若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进 行 join 汇总。

在这里插入图片描述

13.2 Fork/Join 框架与线程池的区别

采用 “工作窃取”模式(work-stealing):

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中, 如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了 线程的等待时间,提高了性能。

public class TestForkJoinPool {
	
	public static void main(String[] args) {
		Instant start = Instant.now();
		
		ForkJoinPool pool = new ForkJoinPool();
		
		ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 5000000000L);
		
		Long sum = pool.invoke(task);
		
		System.out.println(sum);
		
		Instant end = Instant.now();
		
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//2709  拆分也需要时间
	}
	
	@Test
	public void test1(){
		Instant start = Instant.now();
		
		long sum = 0L;
		
		for (long i = 0L; i <= 5000000000L; i++) {
			sum += i;
		}
		
		System.out.println(sum);
		
		Instant end = Instant.now();
		
		System.out.println("for耗费时间为:" + Duration.between(start, end).toMillis());//2057
	}
	
	//java8 新特性
	@Test
	public void test2(){
		Instant start = Instant.now();
		
		Long sum = LongStream.rangeClosed(0L, 5000000000L)
							 .parallel()
							 .reduce(0L, Long::sum);
		
		System.out.println(sum);
		Instant end = Instant.now();
		System.out.println("java8 新特性耗费时间为:" + Duration.between(start, end).toMillis());//1607
	}
}

class ForkJoinSumCalculate extends RecursiveTask<Long>{

	/**
	 * 
	 */
	private static final long serialVersionUID = -259195479995561737L;
	private long start;
	private long end;
	private static final long THURSHOLD = 10000L;  //临界值
	
	public ForkJoinSumCalculate(long start, long end) {
		this.start = start;
		this.end = end;
	}

	@Override
	protected Long compute() {
		long length = end - start;
		
		if(length <= THURSHOLD){
			long sum = 0L;
			
			for (long i = start; i <= end; i++) {
				sum += i;
			}
			
			return sum;
		}else{
			long middle = (start + end) / 2;
			
			ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle); 
			left.fork(); //进行拆分,同时压入线程队列
			
			ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
			right.fork(); //进行拆分,同时压入线程队列
			
			return left.join() + right.join();
		}
	}
	
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JUC学习笔记及拓展 的相关文章

  • 在java中将StreamWriter转换为OutputStream?

    我正在尝试使用 System setOut 将 System out 重定向到字符串 它需要一个 PrintStream 有什么方法可以将 StringWriter 转换为 Stream 以便我可以将其传递给 setOut 吗 你不能完全这
  • JavaEE 8 教程,在 hello1 项目上部署失败

    我正在尝试学习 Java EE 8 我遵循了官方指南https javaee github io tutorial https javaee github io tutorial 但我有这个问题 cargo maven2 plugin 1
  • JPanel透明背景和显示元素[重复]

    这个问题在这里已经有答案了 我插入一个背景图e 变成 aJPanel但一些界面元素消失了 以下 Java Swing 元素不会出现 标签标题 标签 usuario 标签 密码 按钮加速器 你能否使图像透明或元素不透明 setOpaque f
  • 有效地查找正则表达式的所有重叠匹配项

    这是后续与 java 正则表达式匹配的所有重叠子字符串 https stackoverflow com q 11303309 244526 有没有办法让这段代码更快 public static void allMatches String
  • java“void”和“非void”构造函数

    我用 java 编写了这个简单的类 只是为了测试它的一些功能 public class class1 public static Integer value 0 public class1 da public int da class1 v
  • 在气球内显示带有照片的多个地标的最佳做法是什么?

    我有一个项目如下 从手机上拍摄几张照片 将照片保存在网络系统中 然后将照片显示在其中的谷歌地球上 我读过很多文章 但它们都使用 fetchKml 我读过的一篇好文章是使用 php 但使用 fetchKml 我不知道是否可以使用 parseK
  • firestore快照监听器生命周期和定价之间有什么关系?

    在我的活动中 我有一个字符串列表 这些字符串表示我想要附加快照侦听器的 Firestore 文档 我使用 Acivity ModelView 存储库结构 在活动的 onCreate 中 我向 ViewModelProvider 询问适当的
  • 如何模拟一个方面

    我目前正在使用aspectj 开发一些监控工具 因为这个工具应该是技术独立的 尽可能 所以我没有使用 Spring 进行注入 但我希望我的方面能够经过单元测试 方面示例 Aspect public class ClassLoadAspect
  • 在 Spring 中设置 WS https 调用超时 (HttpsUrlConnectionMessageSender)

    我正在尝试为 WS 调用设置超时 我延长了WebServiceGatewaySupport并尝试将发送者超时设置为如下 public Object marshalSendAndReceive Object requestPayload We
  • getClassLoader().getResource() 返回 null

    我有这个测试应用程序 import java applet import java awt import java net URL public class Test extends Applet public void init URL
  • 会话 bean 中的 EntityManager 异常处理

    我有一个托管无状态会话 bean 其中注入了 EntityManager em 我想做的是拥有一个具有唯一列的数据库表 然后我运行一些尝试插入实体的算法 但是 如果实体存在 它将更新它或跳过它 我想要这样的东西 try em persist
  • java JFileChooser 文件大小过滤器

    我知道我可以按文件类型进行过滤 但是可以按文件大小进行过滤吗 例如 JFileChooser 仅显示 3 MB 以内的图片 简短的回答应该是 你尝试过什么 长答案是肯定的 JFileChooser fc new JFileChooser f
  • .class 与 .java

    class 文件和 java 文件有什么区别 我正在尝试让我的小程序工作 但目前我只能在 Eclipse 中运行它 还不能嵌入 HTML 谢谢 编辑 那么如何使用 JVM 进行编译呢 class 文件是编译后的 java 文件 java 都
  • @TestPropertySource 不适用于 Spring 1.2.6 中使用 AnnotationConfigContextLoader 的 JUnit 测试

    似乎我在 Spring 4 1 17 中使用 Spring Boot 1 2 6 RELEASE 所做的任何事情都不起作用 我只想访问应用程序属性并在必要时通过测试覆盖它们 无需使用 hack 手动注入 PropertySource 这不行
  • 是什么原因导致“对象不是声明类的实例”? [复制]

    这个问题在这里已经有答案了 可能的重复 使用反射调用方法时 为什么会出现 对象不是声明类的实例 https stackoverflow com questions 7202988 why do i get object is not an
  • 使用 Maven 3 时 Cobertura 代码覆盖率为 0%

    读完这篇文章后 将 Cobertura 与 Maven 3 0 2 一起使用的正确方法是什么 https stackoverflow com questions 6931360 what is the proper way to use c
  • 将字符串中的字符向左移动

    我是 Stack Overflow 的新手 有一道编程课的实验室问题一直困扰着我 该问题要求我们将字符串 s 的元素向左移动 k 次 例如 如果输入是 Hello World 和3 它将输出 lo WorldHel 对于非常大的 k 值 它
  • 日期时间解析异常

    解析日期时 我的代码中不断出现异常错误 日期看起来像这样 Wed May 21 00 00 00 EDT 2008 这是尝试读取它的代码 DateTimeFormatter formatter DateTimeFormatter ofPat
  • 如何在Java中跨类共享变量,我尝试了静态不起作用

    类 Testclass1 有一个变量 有一些执行会改变变量的值 现在在同一个包中有类 Testclass2 我将如何访问 Testclass2 中变量的更新值 由 Testclass1 更新 试过这个没用 注意 Testclass1和Tes
  • 如何使用 Spring AOP 建议静态方法?

    在执行类的静态方法之前和之后需要完成一些日志记录 我尝试使用 Spring AOP 来实现这一点 但它不起作用 而对于正常方法来说它起作用 请帮助我理解如何实现这一点 如果可以使用注释来完成 那就太好了 也许您应该在使用 Spring AO

随机推荐

  • Elasticsearch 实现分组统计

    之前有个查询es分组求和的需求 类似关系型数据库 select a b sum c from table group by a b 当时用DSL查询语句实现 这边记录下 GET my index my type search from 0
  • C++中new与delete问题学习

    C 中new与delete问题学习 一 new char与delete问题 1 问题程序 include
  • 普歌-云言团队-Spring的AOP简介

    什么是AOP AOP 为 Aspect Oriented Programming 的缩写 意思为面向切面编程 是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术 AOP 是 OOP 的延续 是软件开发中的一个热点 也是Spri
  • 【ES实战】索引mapping的动态设置

    动态mapping 文章目录 动态mapping 动态mapping规则 语法规则 match mapping type match and unmatch match pattern path match and path unmatch
  • signature=8a03839902ac8eb66fcf33ab62032d86,swch-20200612

    0001710583 20 000020 txt 20200618 0001710583 20 000020 hdr sgml 20200618 20200618161953 ACCESSION NUMBER 0001710583 20 0
  • 了解Nginx配置文件结构与配置上下文

    提供 ZStack云计算 系列教程 本教程为如何在Ubuntu 14 04上实现Nginx与LEMP系列四篇中的第四篇 内容介绍 Nginx是一套高性能Web服务器 负责处理互联网上各大型站点的日常负载 其特别擅长处理高并发连接与大量静态内
  • c++语法

    文章目录 0 0 编译运行 单个程序编辑调试 库文件编译调试 1 变量 1 1 变量的声明和定义 1 2 变量的作用域 1 3 namespace命名空间 标准空间std 2 关键字 2 1 extern 3 常量 1 define 定义
  • 插值1算法

    一 基本概念 插值是指通过对数据进行线性 非线性或其他类型的逼近 将一组离散数据映射到连续的函数值 在数学中 插值通常用于将数据点连接起来 以形成连续的函数图像 特别是在数值计算和图像处理中 插值可以用于在空间中预测对象的位置 速度和加速度
  • Unity&Shader案例篇—绘制雨滴

    一 前言 转载请注明出处凯尔八阿哥专栏 惯例先上效果图 本文不只是简单的绘制雨滴 同时处理了摄像机不同朝向看到的雨滴下落的方向也不一样 二 方法 1 绘制雨线 绘制雨使用的是C 脚本绘制的 脚本为 using UnityEngine usi
  • 测试之自动化测试

    详细Python教程见 http www liaoxuefeng com wiki 0014316089557264a6b348958f449949df42a6d3a2e542c000 0014316090478912dab2a3a9e8f
  • 【金九银十】软件测试中的高频面试题梳理(内附答案)

    写数据库语句 一个老师表 一个学生表 1 查李老师班的小明 2 并将小明的年纪改成26 select t1 from 学生表 t1 jion 老师表 t2 on t1 班级 t2 班级 where t1 姓名 小明 and t2 姓名 李老
  • vue阻止弹窗_vue 弹窗禁止底层滚动

    原因 底层视图高度超出百分百 加入弹窗后再苹果浏览器隐藏上下栏的情况下遮罩层没有完全遮住底层 处理 打开弹窗后禁止底层滚动调用stop事件 关闭则开启底层滚动调用move事件 let mo function e e preventDefau
  • 实时流协议(RTSP) 来自 维基百科

    https zh wikipedia org wiki E5 8D B3 E6 99 82 E4 B8 B2 E6 B5 81 E5 8D 94 E5 AE 9A 目录 协议指令 OPTIONS 请求 DESCRIBE 请求 SETUP 请
  • stat()/lstat()的使用

    stat 函数和lstat 函数都是用于获取文件或目录的信息的函数 它们可以返回包含文件或目录的各种属性的结构体 这里是关于这两个函数的使用方法的简要说明 stat 函数 include
  • Boostrap对HTML的表格的设计和优化

    目录 01 Bootstrap的默认表格风格 02 没有边线 边界的表格 03 行与行的背景颜色交替变换 条纹样式 04 给表格加上边框效果 05 鼠标移到行上时该行的颜色加深 06 把表格的padding值缩减一半 使表格看起来更紧凑 0
  • 评分模型应用案例_FLUENT太阳辐射模型应用简单案例

    正文共 897字 11图 预计阅读时间 3分钟 1 前言 FLUENT自带了一个太阳辐射模型 solar load model 可以用来计算太阳光线进入计算域带来的辐照 其所谓光线追踪法 ray tracing approach 可以高效地
  • springBoot 整合shiro

    1 springBoot 整合思路 2 环境搭建 2 1创建springBoot项目并导入依赖 a 基本依赖 shiro spring boot starter spring web lombok b shiro依赖
  • 网络编程问题

    数据发送 假设应用程序要发送40KB数据 但是OS的TCP发送缓冲区只有25KB剩余空间 那么剩下的15KB数据怎么办 如果等待OS缓冲区可用 会阻塞当前线程 因为不知道对方什么时候收到并读取数据 因此网络库应该把这个15KB数据缓存起来
  • The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar

    今天用eclipse在maven项目里测试jsp页面时报了如下错误 The absolute uri http java sun com jsp jstl core cannot be resolved in either web xml
  • JUC学习笔记及拓展

    本文为自己整理的学习笔记及学习心得 大纲取自尚硅谷的JUC视频 感兴趣的小伙伴可以去B站自学 JUC学习笔记及拓展 Java JUC 1 Java JUC简介 2 volatile 关键字 内存可见性 2 1 内存可见性 2 2 volat