多线程的小入门

进程与线程

  • 进程:系统进行资源分配的最小单元,每个进程都有独立的代码和数据空间--进程的上下文
  • 上下文切换:cpu从一个进程切换到另一个进程的动作
  • 线程:cpu调度的对消单位,是进程的一部分,只能由进程创建(分为用户线程和守护线程)
  • 每个线程共享进程的数据空间,它们分别有独立的栈和程序计数器

并发与并行

  • 并行:同一时间在多台计算机上同时运行多个任务(类似:10个小朋友分两组,一组抢玩具车玩,另一组抢玩具熊玩,这两组小朋友就是并行玩耍)
  • 并发:同时执行多个任务(类似:10个小朋友抢一个文具玩,每人玩一会儿)
public class MyThread extends Thread{
	@Override
	public void run(){
		// 当run()方法执行完毕并返回时,当前线程将终止
		for(int i = 0; i < 1000; i++){
			try{
				Thread.sleep(10L);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		Thread t = new MyThread();
		t.start();
	}
}

tips:

  • 调用start()方法后,run()方法中的代码并不一定立即开始执行,start()方法只是将县城变为可就绪状态,什么时候运行由操作系统决定的

wait和sleep的区别

  • wait()方法必须在synchronized同步块或方法中使用
  • wait()方法形成的阻塞,可以通过针对同一个对象锁的synchronized作用域调用notify()/notifyAll()来唤醒;而sleep()则无法被唤醒,其只能定时醒来或被interrupt()方法中断

sleep和yield的区别

  • 调用sleep()方法后转入阻塞状态,并在睡眠一段时间后自动醒来回到就绪状态
  • 调用yield()方法后,当前线程转入就绪状态
  • sleep()方法是,线程无论优先级高低都有机会运行,而yield()只会给那些相同优先级或更高优先级的线程运行的机会

线程池

面试常问:如何创建线程池

  • 根据《阿里开发手册》线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式创建
  • Executors返回的线程池对象的弊端
1.FixedThreadPool 和 SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2.CachedThreadPool 和 ScheduleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

常问参数:

int corePoolSize 核心线程数
int maximumPoolSize 最大线程数
keepAliveTime 线程最大空闲时长

保证线程同步的手段(简单列举)

CAS操作

  • 原理:在更新某个变量之前,检查变量的当前值是够符合期望值,如果相符就用新值代替旧值,否则循环重试(自旋)直到成功
  • 用cas简单实现一个计数器(包括线程安全/不安全)
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
	final Counter cas = new Counter();
	List<Thread> ts = new ArrayList<Thread>(600);
	long start = System.currentTimeMillis();
	for (int j = 0; j < 100a; j++) {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					cas.count();
					cas.safeCount();
				}
			}
		});
		ts.add(t);
	}
	for (Thread t : ts) {
		t.start();
	}
// 等待所有线程执行完成
	for (Thread t : ts) {
		try {
			t.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	System.out.println(cas.i);
	System.out.println(cas.atomicI.get());
	System.out.println(System.currentTimeMillis() - start);
}
/** * 使用CAS实现线程安全计数器 */
private void safeCount() {
	for (;;) {
		int i = atomicI.get();
		boolean suc = atomicI.compareAndSet(i, ++i);
		if (suc) {
			break;
		}
	}
}
/**
* 非线程安全计数器
 */
private void count() {
	i++;
}
}

cas三大问题

1.ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化
则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它
的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面
追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从
Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个
类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是
否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(
	V expectedReference, // 预期引用
	V newReference, // 更新后的引用
	int expectedStamp, // 预期标志
	int newStamp // 更新后的标志
)
  1. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如
    果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第
    一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间
    取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候 因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而
    提高CPU的执行效率。
  2. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循
    环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
    性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来
    操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,
    JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对
    象里来进行CAS操作。

Lock自旋锁

  • java.util.concurrent.locks包中提供了锁Lock接口及其实现类ReentrantLock
 // ReentrantLock基本用法
private static final Lock lock = new ReentrantLock();
public void run() {
	lock.lock();
	try{
		// do something
	}catch(InterruptedException e) {
		e.printStackTrace();
	}finally {
		lock.unlock();      //一定要解锁鸭!!!
	}
}
  • ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。
  • ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态

源码


protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();    // 获取锁的开始,首先读volatile变量state
	if (c == 0) {
	if (isFirst(current) &&
		compareAndSetState(0, acquires)) {
		setExclusiveOwnerThread(current);
			return true;
	}
}
else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) 
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

CountDownLatch 计数器

  • 多个线程通过调用它们所共享的计数器(CountDownLatch对象)的countDown()方法来让计数器减1
  • 可以通过CountDownLatch对象的await方法来阻塞当前线程,直到计数器的值为0;

public class CountDownLatchTest {
	staticCountDownLatch c = new CountDownLatch(2);
	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(1);
				c.countDown();
				System.out.println(2);
				c.countDown();
			}
		}).start();
		c.await();
		System.out.println("3");
	}
}
  1. 当我们调用CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的await()方法
    会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个
    点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个
    CountDownLatch的引用传递到线程里即可。
  2. 如果有某个解析sheet的线程处理得比较慢,我们不可能让主线程一直等待,所以可以使
    用另外一个带指定时间的await()方法——await(long time,TimeUnit unit),这个方法等待特定时
    间后,就会不再阻塞当前线程。join也有类似的方法。

CyclicBarrier 柵栏

  • CyclicBarrier 是一种可重用的线程阻塞器,通过调用await()方法在代码中形成"柵栏",率先执行到"柵栏"(await()方法)阻塞,知道制定数量的线程也都达到"柵栏"处。
posted @ 2021-05-10 10:52  xiaoff  阅读(61)  评论(0编辑  收藏  举报