并发

创建一个线程

  1. 将要执行的任务放入一个实现了Runnable的类的run()方法中.
  2. new 一个线程,将实现了runnable 接口的实例作为构造参数传入。
  3. 调用线程实例的start()方法

Java的线程状态比操作系统的线程状态多了一个。

操作系统的线程状态和java的线程状态比对:

状态 操作系统 java
创建状态 为线程分配资源的状态,还没进入就绪队列中。 就是new Thread(r)操作的时候,就是new状态,还没准备开时执行run方法里面的代码。
就绪态 线程等待cpu资源。 Runable状态,等待操作系统调度它占用cpu资源。
运行态 线程占用cpu资源执行程序。 Runable状态,就是说Runable状态指的是就绪态和运行态。
阻塞态 当用户继续执行下去的条无法被满足时。 当线程要求一个内在对象锁时,但是这个锁被其他线程支持,他就要blocked,当其他进程都放弃这个锁时,unbloked
waiting状态,一个线程等待另外一个线程通知线程调度器一个条件时,可以调度线程去执行。调用wait(),join()方法或者等待Lock或者条件时,将进入waitting。
timed waitting状态,有些方法又超时参数,调用这些方法线程将进入timed waitting状态,直到时限到期了,或者合适的通知已经到达。sleep()方法,实现版本的wait方法,join方法,Lock.tryLock,Condition.await将导致timed waitting。
借宿态 线程从系统中消失,但是tcb还没有被系统清除. terminated状态, 终止一个线程。1.当线程正常完成任务时。2.当线程发生不可处理的异常时。

Thread类的属性

中断线程

操作系统的中断是指,操作系统对外部事件的响应机制,中断正在执行的程序,调用相应的服务例程来处理中断事件。但是被中断的程序只是从运行态转变成了其他状态。

java的中断指的是通过一种优雅的机制给线程一个中断信号,这个就是线程的interrupted state。让线程自己判断要不要终止。通过调用interrupt()方法设置这个标志位。使用isInterrupted判断这个标志位是否被设置,如果被设置了就执行相应的代码,例如return终止线程。

前面也说了,线程终止的两种途径

  • 让线程自行完成相应的任务自己终止线程。
  • 线程发生不可捕获的异常终止线程。

interrupter()可以使得线程通过抛出InterruptedException异常达到终止线程的方法。

守护线程

在操作系统中,停留在后台等待请求到达才被唤醒的进程称为守护进程。

在java中,除了服务其他线程以外没有其他作用的线程称为守护线程。当虚拟机中只有守护线程时,虚拟机就退出运行,因为已经没有其他线程被守护线程服务了。

创建守护线程的方法,在线程实例调用start()方法前,调用setDeamon( true)方法。

线程命名

调用线程实例的setName方法为线程赋予一个名字。默认是线程名时Thread-n。

处理未捕获异常

线程不可以抛出任何检查异常,应该在run内部就处理掉所有检查异常,但是线程可以被非检查异常终止。

在线程终止时,将异常传递到一个未捕获异常处理器,

  1. 该处理器时实现了Thread.UncaughtExceptionHandler接口的类,通过setUncaughtException()方法设置自定义的未捕获异常处理器,
  2. 也可以时默认的未捕获异常处理器,通过setDefaultUncaughtDefaultException方法设置。
  3. 如果即没有设置默认的未捕获异常处理其或自定义未捕获异常处理器,那么将使用ThreadGroup对象的处理器,ThreadGroup是一个管理线程集合。

同步

Java解决竞争条件的方法。

Lock对象

Java提供两种机制保护代码块(临界区)受并发访问的影响:

  1. synchronized

    synchronized就是现代操作系统中提到的管程(monitor),即一个由过程(方法,在Java中被标注为synchronized关键字标注的方法),变量和数据结构组合成的一个数据结构,任何时刻,只能允许一个进程进入monitor中执行。当线程进入管程中,条件(Condition)没法被满足,那么线程就block在这个Condition上,直到它可以被使用,就是其他线程使用完这个条件,唤醒一个等待在这个条件上的线程。

  2. ReentrantLock

    使用ReentrantLock机制的临界区代码框架

    myLock.lock(); // a ReentrantLock object
    try{
    	critical section
    }
    /*注意一点就是解锁操作一定要放在finally语句,这个语句不管是发生异常还正常执行完都会调用,如果不放在finally语句中,当临界区发生异常时线程终止,但是线程没有释放锁,会导致等待在这个临界区上的线程永久等待状态。*/
    finally{  
     	myLock.unlock(); // make sure the lock is unlocked even if an exception is thrown
    }
    

    当有一个进程获得锁,进入临界区,当其他进程调用lock()方法后,就会阻塞在这个lock上面,直到其他线程释放锁,它才可以进入。

    ReentrantLock之所以叫reentrant,因为,一个类中可能存在多个临界区,就收多个需要互斥访问的方法,那么当一个线程进入一个方法时调用了ReentrantLock对象的lock方法将临界区进行加锁,这个锁时锁住整个类对象的。但是这个线程进入的临界区可能会调用相同对象的另外一个互斥方法,此时它还要再调用一次ReentrantLock对象的lock方法将类对象再锁一遍。当被调用方法执行结束时,解锁。调用方法结束后也会解锁。其中记录一个线程锁相同对象的次数由ReentrantLock对象的记录。

条件变量

用条件对象用来管理一个获得了锁的线程,但是没法有效工作的线程。

例如:

一个线程进入临界区,发现条件无法满足继续执行下去,那么就原地等待条件满足,但是它不释放锁,其他线程就无法进入临界区,满足它继续执行下去的条件。导致线程一直无法继续执行,其他进程一直阻塞在临界区外。

使用了条件变量,当线程进入临界区发现条件不满足时,线程阻塞在该条件变量上,等待其他线程释放该条件变量,通知其他等待该条件变量上的线程将他们唤醒,进入就绪队列中等待获得锁。

一个锁对象可以有多个关联的条件变量,通过newCondition方法获得。

class Bank{
 	private Condition sufficientFunds;
	public Bank(){
	 	sufficientFunds = bankLock.newCondition();
 	}
}

当线程进入临界区发现条件不满足时,调用sufficientFunds.await()方法,阻塞在这个条件变量上,并放弃锁。

在条件变量上阻塞和在锁对象上阻塞不同:

sufficientFunds.signalAll();
  • 条件变量上阻塞的线程,只有在其他在相同条件变量上的线程调用signalAll方法通知阻塞线程解除阻塞状态。在条件变量上阻塞的线程,即使lock可用,它也不会执行。
  • 在锁对象上阻塞的线程,一旦lock可用它就会执行。

当阻塞在条件变量上的线程被唤醒,他们最终会重新执行,从阻塞的地方开始继续执行。但是被唤醒的线程并不能保证条件一定满足它还要继续去测试条件。

//测试条件是否满足,如果不满足就阻塞的代码形式.
while (!(OK to proceed))
 condition.await();

当线程阻塞在条件变量上,它就完全相信其他线程去唤醒它,它没有办法去唤醒它自己,所以如果没有线程去唤醒阻塞在条件变量上的线程,最终将导致死锁,就是所有线程都无法执行,全部睡眠。

因此我们要决定什么时候去执行signalAll()方法去唤醒所有的线程,避免死锁。

public void transfer(int from, int to, int amount)
{
	bankLock.lock();
	try{
		while (accounts[from] < amount)
		sufficientFunds.await();
		// transfer funds
		. . .
		sufficientFunds.signalAll();
	}
	finally{
		bankLock.unlock();
	}
}

singal()方法只随机唤醒等待线程中的一个(可能会导致死锁),singalAll是唤醒所有等待在条件变量上的线程。

synchronized关键字

锁变量和条件变量赋予了程序员编写并发程序更高的灵活性,但是synchronized关键字使得我们实现并发程序更容易。

因为在java中每个对象都有自己固有锁,如果一个方法应用了synchronized那么固有锁将保护该方法的并发执行。

固有锁也有一个关联的条件变量,如果临界区执行的线程发现条件不满足可以调用wait()方法,调用notifyAll()唤醒阻塞在该条件变量上的线程。

class Bank{
	private double[] accounts;
	public synchronized void transfer(int from, int to, int amount) 
	throws InterruptedException{
		while (accounts[from] < amount)
		wait(); // wait on intrinsic object lock's single condition
 		accounts[from] -= amount;
 		accounts[to] += amount;
		notifyAll(); // notify all threads waiting on the condition
	}
 	public synchronized double getTotalBalance() { . . . }
}

synchronized Block

另外一种获得对象固有锁的机制

synchronized (obj) // this is the syntax for a synchronized block
{
	critical section
}

这个不推荐使用,需要obj确保它执行的方法使用了synchronized关键字。

Volatile 关键字

用于读写操作的变量。

这个问题说的是,如果只针对对象中的一个域做读访问和写访问,从而对整个对向进行加锁操作这样做太过繁琐,和开销大,因为如果一个线程获得了该对象的锁,但是他仅仅只是做读操作,那么加下来要做读操作的进程都需要阻塞在这对象上。

因此Volatile是一个面锁机制,但是不会造成线程读写数据的混乱,就是说一个线程可能读了,另外一个写之前的数据。volatile提供一种,当一个线程写入数据时,确保其他读进程都能读到写入的数据。

Atomics

java.util.concurrent.atomic包下的类是提供原子操作的方法。

例如AtomicInteger类,他的AtomicInteger方法或者accumulateAndGet方法都提供原子操作。但是如果由大量的线程要访问AtomicInteger对象并更新它,那么性能会下降,可以通过LongAdder类和LongAccumulator类去解决这个问题。

这个我不太懂,是不是为每个线程赋予一个变量。每个线程在自己的变量上增加值,当所有线程结束时,对线的值就是所有变量增加值后的总和。

Thread-Local Variables

不共享变量变量了, 每个线程拥有自己实例,这个就是ThreadLocal类的作用。

例如,SimpleDateFormat类是一个线程不安全的类,

public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果有两个线程并发执行了一下初始化代码。

String dateStamp = dateFormat.format(new Date());

那么就会出错,因为dateFomat是全局静态变量,所有线程只有这个一个变量,一个线程的赋值会修改另外一个线程的结果。

使用ThreadLocal类为每一个线程初始话一个dateFormat。

public static final ThreadLocal<SimpleDateFormat> dateFormat
 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

//通过一下代码范文真正的格式
String dateStamp = dateFormat.get().format(new Date());

每当一个线程调用get方法的时候,lambda里面的构造器new SimpleDateFormat("yyyy-MM-dd")就返回一个属于当前线程的实例。

弃用stop和suspend的原因

  1. stop会破坏对象,因为线程还没运行完就会被停止,例如一个线程正在向锁对象写入数据,让下一个进程读这个数据,当线程写时被调用stop终止线程,那么下一个线程读的数据就是错误的。
  2. suspend会导致死锁,当一个线程在临界区挂起时,它不会释放lock,直到线程被resume。

线程安全集合

阻塞队列

java提供的一种实现线程安全的生产者和消费者模型的数据结构,生产者线程可以向阻塞队列中添加数组,消费者线程向阻塞队列中取数据。

当队列满时,生产者线程就会阻塞,当队列为空时消费者线程就会阻塞。这时候就可以唤起生产者线程来向消费者阻塞队列中添加数据。

使用阻塞队列时我们不用在管任何并发问题,这写都有阻塞队列为我们实现,我们只要向使用队列那般使用它就好了。

Tasks And Thread Pools

构造一个新的线程开销是比较大的,虽然操作系统创建一个线程的开销比创建一个进程的开销要小,因为至于要给线程分配一块内存空间(分给进程中的一块)和一些寄存器就足够了。但是如果频繁的创建线程,就需要操作系统频繁的从用户态切换到内核态,这时候即使是创建线程,开销也是大的,因为主要的开销是从用户态到内核态的切换。主要因素是确保操作系统的安全性,对用户态所有的请求和数据持有怀疑。因此对用户态的空间和内核态的空间做了明确的界定(用户态不能对硬件做出任何操作,只有内核态有权利)。

至于Java创建线程是通过操作系统对Java虚拟机提供的接口实现的,也就是说Java虚拟机是没办法自己直接创建线程的。当Java虚拟机向操作系统发起创建线程的请求时,操作系统就要进行状态的转变。

因此,如果是短生命周期的任务,就通过线程池中获取一个线程取执行而不为它分配一个新的线程。

Callables and Futures

主要讲的是Java并发框架为合作并发任务的支持。

Runnable包含的任务是异步执行的,不包含返回值和参数(泛型的参数)的方法。所谓异步指的是,任务执行时,不是从头执行到尾的,而是走走停停,一方面是分配给线程的时间片用完了,另一方面是线程等待资源阻塞了。

Callables接口的call方法包含返回值和参数(泛型的参数),和runnable接口相似。

public interface Callable<V>
{
 	V call() throws Exception;
}

Future接口,用于获取线程返回值的一个接口。

/*
	第一个get方法:当future对象调用get()方法, 它会阻塞直到线程完成任务.
	第二个get方法:如果线程未能在超时前完成计算,那么会抛出异常TimeoutException.
	如果线程执行时被中断,那么两个方法都会抛出InterruptedException异常.
	如果线程执行结束,get立即返回.
*/
V get()
V get(long timeout, TimeUnit unit)
//取消执行任务,当线程还没开始的时候。如果开始执行了,可以被中断,mayInterrupt=true
void cancel(boolean mayInterrupt)
boolean isCancelled()
boolean isDone()

执行Callable接口的方法:

  1. 用FutureTask类(实现了Runnable和Future)封装Callble接口的对象,讲FutureTask类对象作为参数创建线程,执行,通过FutureTask类获得线程执行完返回值。

    Callable<Integer> task = . . .;
    var futureTask = new FutureTask<Integer>(task);
    var t = new Thread(futureTask); // it's a Runnable
    t.start();
    . . .
    Integer result = futureTask.get(); // it's a Future
    
  2. 将Callable接口传递给一个executor。

Executor

Executor有很多静态的工厂方法,用来创建线程池的。

  1. newCachedThreadPool

    创建一个立即执行每一个线程的线程池,使用一个可用的空闲线程或者创建一个新的线程.
    

    使用在段生命周期的任务和用于大量时间阻塞的任务。

  2. newFixedThreadPool

    创建一个固定大小的线程池,如果提交上来的任务比线程池的size要大,那么将多余的任务放入一个队列中,等待其他线程执行结束他们才执行.
    

    当有线程不阻塞并且不想并发执行很多线程的时候。为了最优的执行速度,线程的数量应该和cpu的核心数一样多。

  3. newSingleThreadExecutor

只有一个线程的线程池,提交上来的任务,一个接一个的执行.

用于性能分析,就是分析使用并发给程序带来多大的性能提升。

这些方法方法一个实现了ExecutorService接口的ThreadPoolExecutor类对象。

newScheduledThreadPool和newSingleThreadScheduledExecutor工厂方法放回一个实现了ScheduledExecutorService接口的对象,它含有调度和重复执行一个任务的方法,可以通过这个对象调度任务执行或者周期性的执行一个任务。

使用

可以通过以下方法将Runnable接口和Callable接口传递给线程池。

Future<T> submit(Callable<T> task)
//调用Future的get返回null
Future<?> submit(Runnable task)
//调用Future的get放回result
Future<T> submit(Runnable task, T result)

当所有任务执行完时,调用shutdown关闭线程池。并且线程池将不会接收任何新的任务。

实现线程池的总结:

  1. 创建一个线程池
  2. 通过submit方法提交任务
  3. submit放回future对象,你可以通过future对象对任务执行一些操作
  4. 关闭线程池

Controlling Groups of Tasks

终止一组相关的任务(完成一件事的不同方式),一块提交到线程池中,只要有一个任务完成了就关闭线程池。不管顺序的。

List<Callable<T>> tasks = . . .;
List<Future<T>> results = executor.invokeAll(tasks);
for (Future<T> result : results)
 processFurther(result.get());

有序的:ExecutorCompletionService对象管理一个阻塞队列。

var service = new ExecutorCompletionService<T>(executor);
for (Callable<T> task : tasks) service.submit(task);
for (int i = 0; i < tasks.size(); i++)
 processFurther(service.take().get());

Fork-Join框架

Fork-Join框架用来支持一些应用为每一个cpu核心创建一个线程来执行计算机型的任务。

posted @ 2021-10-03 16:57  _LittleBee  阅读(63)  评论(0编辑  收藏  举报