Java并发基础

并发

Posted by CHuiL on March 1, 2021

Runnable 与 Thread

java最基本的线程调用;实现Runable接口中的run方法,然后用该对象来创建一个Thread对象,start()即可进行多线程运行;

 *     class PrimeRun implements Runnable {
 *         long minPrime;
 *         PrimeRun(long minPrime) {
 *             this.minPrime = minPrime;
 *         }
 *
 *         public void run() {
 *             // compute primes larger than minPrime
 *              . . .
 *         }
 *     }
 *     PrimeRun p = new PrimeRun(143);
 *     new Thread(p).start();

Runable接口;提供run方法,不需要参数和返回值,以满足想要利用线程来执行任务的类;因为有该接口,所以不在需要再继承一个子类去实现线程调用,只要实例化一个Thread并将原来的类实例传递进去即可;

Thread类,实现Runable接口;可以用它来创建一个可执行的线程;通过继承它获得子类,实现run方法;线程有优先级,线程创建出来的线程拥有相同的优先级以及是否守护线程;

 *     class PrimeThread extends Thread {
 *         long minPrime;
 *         PrimeThread(long minPrime) {
 *             this.minPrime = minPrime;
 *         }
 *
 *         public void run() {
 *             // compute primes larger than minPrime
 *              . . .
 *         }
 *     }
  *     PrimeThread p = new PrimeThread(143);
 *     p.start();
join()方法

对Thread对象使用join方法,将等待该线程执行结束;(类似wait)

Executor(interface,execute方法)

An object that executes submitted Runnable tasks. 这个接口提供了一种将任务提交与每个任务如何运行的机制(线程使用和调度等细节)分离的方法;通常使用Executor来代替直接显示的创建Threads;

 Executor executor = anExecutor();
 executor.execute(new RunnableTask1());
void execute​(Runnable command); //由具体实现的Executor来决定使用什么Thrad,如新建一个,pool的,in the calling thread 以及在未来某个时刻执行;

ExecutorService

实现Executor接口;提供终止方法和追踪任务进度的Future;有shutdown和shutdownNow方法停止,submit方法来拓展execute方法,返回表示该任务的future;
详见 :https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/concurrent/ExecutorService.html

ThreadPoolExecutor

CachedThreadPool();

会创建与所需数量相同的线程;

FixedThreadPool

可以一次性预先执行线程分配,指定线程池内线程数量;

SingleThreadExecutor

线程数量为1的FixedThreadPool;

Callable

类实Runable,实现该接口的实例都可能用于在另外的thrad执行;不过Runbale没有返回值,Callable有返回值,并且可以抛出异常;Runable使用Execuot的execute方法执行,Callable使用ExecutorService的submit方法来执行(返回Future对象);

Future

表示异步计算的结果;提供方法来检查计算是否完成,取消计算,以及获取计算结果,如果结果还没计算完成,可以阻塞直到计算完成;如果只是为了可以取消计算,可以使用Future<?>并在任务中返回null;详见注释;

isDone()//是否计算完成
get()//获取计算结果,如果需要可以设置阻塞时间;
cancel()//可以用来终止任务

线程异常问题

由于线程的本质特性,我们无法从线程中捕获逃逸的异常; 我们可以实现Thread.UncaughtExeception接口中的void uncaughtException(Thread t, Throwable e);方法来处理Thread的异常;可以调用setUncaughtExceptionHandler和setDefaultUncaughtExceptionHandler==静态==方法来设置该异常处理对象,default则是设置全局默认的Thread异常处理;

synchronized关键字

被该关键字申明的域,同一时刻只能有一个任务拥有它;共享资源一般被包装为一个对象;调用它需要通过方法,然后在该方法上标记synchornzied;当在对象上调用任意一个被syncchornzied标记的方法时,其他同样被标记的方法也将无法访问;
该关键字可以申明在一个代码段,一个方法上;

  • 注意,该关键字无论是用在方法上,还是代码块上,都是只针对对象进行加锁,毕竟锁是对象的属性,所以默认不指定的情况下,是对当前实例进行加锁,所以即便是同一段代码,启动不同任务,针对不对实例调用同一个方法,该方法可能访问了共享资源,这样是无法起到互斥的;

synchronized(this){};代码块,表明锁上的对象,那么该对象的其他synchronized方法和临界区就不能被调用了;

Lock对象

显式的创建,加锁和释放;

volatile关键字

原子操作也不一定是安全得,在多核处理器中,因为写入得数据可能会先写入到工作线程的本地缓存,此时对于其他任务而言是“不可视”的,因为其他任务读取该值最新值是从主存中获取,所以会读取到旧值;加上该关键字,那么所有读操作就都可以看到修改,它会要求数据立即写入到主存中;

  • 如果一个域可能会被多个任务访问到,或者这些任务中至少有一个是写入任务;就应该将这个域设置为volatile

Brian的同步规则

  • 如果你正在写一个变量,他可能接下来被另一个线程读取,或者正在读取一个上一次以及被另一个线程写过的变量,那么你必须使用同步;并且读写线程都必须用相同的监视器锁同步;

ThreadLocal

这是一个类,提供线程局部变量;通常这个变量会被声明为类中的静态私域变量,通过getset方法获取,但是与普通变量不同的是,每个访问该变量的线程,都会有自己独立的变量副本;也就是获取同样一个ThreadLocal变量,每个thread都获取自己线程本地存储的值; 可以ThreadLocal来标识线程的状态或者id等;

中断

当需要中断一个线程时,那么可以调用Thread.interrupted()来中断该线程(该方法只对任务要进入阻塞或者已经在阻塞状态中的线程,并且可以被中断的阻塞有效),该线程将会接收到InterruptedExecption异常;不过现在一般很少直接通过Thread来中断线程,现在大多用Exector来控制线程,所以可以调用shutdownNow()来停止所有线程,也可以通过submit()来启动一个线程,利用返回的Future.cancel(true)来中断线程;

不过注意,并不是所有阻塞都可以被中断,如sleep阻塞可以被中断,而io阻塞,锁阻塞是不会被中断的,可以通过其阻塞方法是否抛出InterruptedExecption异常来判断;
对于不可中断的阻塞,一般就只能从阻塞源来解决,如将io资源关闭来中断阻塞;

对于不是阻塞,而是无限循环的run,我们可以通过调用Thread.interrupted()来检查是否中断,并清除中断状态。

思想

  • 当我们拥有一个非线程安全的类时,可以创建一个manager类来管理这个类,通过synchronized(同步方法)来控制该类的读写,保证线程安全;
  • 通常来说使用synchroinzed块来同步,时间效率会更快,因为这样对象不加锁的时间更长;使其他线程能更多的访问;

线程间的协作

wait() 和 notify(),notifyAll()

当我们任务在执行完自己的工作之后,可能需要等待其他任务执行完成才能继续执行,此时需要等待其他任务,并在其他任务执行成功之后通知我们,继续执行后面的任务;这就需要任务之间进行协作;

  • wait(),notify(),notifyAll()方法都是Objects的方法;因为锁也是对象一部分,这三个方法都是需要操作锁;
  • wait()方法期间对象锁是释放的;
  • wait()就相当于在说,我已经完成了能做的所有事情,因此我要在这里等待,但是我希望其他的Syncronized操作在条件合适得情况下能够执行;
  • 只能在控制方法或者同步控制代码块中调用wait() notify() notifyAll();
  • notify会唤醒某个任务,notifyAll会唤醒所有在该锁上等待的任务
  • 一般来说,wait()需要放在while(condition)语句块中,因为当调用了notifyAll()唤醒之后,是对所有wati线程进行唤醒,有可能唤醒了,但是并没有达到该线程唤醒得条件,所以需要在检查一遍;

关于为什么需要在synchronized中才能调用wait() notify() notifyAll()方法

首先是含义,正如前面说得;
其次是wait()得操作需要获取到锁,在非同步控制块中执行操作,会抛异常(IllegalMonitorStateExecption),原因就是没有获取到该对象锁;
最重要的是,condition的更新与wait的操作 , condition的检查与notify需要互斥;考虑一下代码

// 线程A 的代码
while(!condition){ // 不能使用 if , 因为存在一些特殊情况, 使得线程没有收到 notify 时也能退出等待状态
    wait();
}
// do something


// 线程 B 的代码
if(!condition){ 
	// do something ...
    condition = true;
    notify();
}
  • 假如不需要获取锁就可以调用wait和notify,那么在A线程中,进入while代码里,即将wait之前,线程被换下
  • 然后B的代码执行,condition被更新,并且调用了noify(此可并没有任何线程在wait)
  • 在回到A线程代码,现在进入wait,但是由于已经没有notify操作了,所以A线程将一直等待下去;
// 线程 A 的代码
synchronized(obj_A)
{
	while(!condition){ 
	    obj_A.wait();
	}
	// do something 
}


// 线程 B 的代码
synchronized(obj_A)
{
	if(!condition){ 
		// do something ...
	    condition = true;
	    obj_A.notify();
	}
}

给它加上之后synchronized之后,在分析以上过程,A在wait之前被换下,此时B线程执行,但是由于有锁的存在,所以condition的更新与wait的操作互斥,B阻塞;
在回到A,Await并且释放锁;
B此时获得锁进入,更新玩conditon之后,notify之前被换下;A此刻要么在是已经wait()要么就是还没进入到condition检查,conditon检查和notify互斥;
B再次执行,执行notify,唤醒A,执行完成之后退出,释放锁; A唤醒再次获得锁;

Lock和Condition(java SE5 ,更高级的用法 )

Lock lock = new ReentrantLock();//显式的使用一个锁
Condition condition = lock.newCondition();//在这个锁上的Condtion 
...
condition.signalAll()//相当于notifyAll() 但是更安全
...

while(..){
    condition.wait()//释放锁lock 
}