java多线程学习笔记。

java多线程学习笔记


线程的优缺点:

  • 多线程的好处:
  1. 充分利用多处理核心,提高资源的利用率和吞吐量。
  2. 提高接口的响应效率,异步系统工作。
  • 线程的风险
  1. 安全危险(竞争条件):什么坏事都没有发生。在没有充分进行同步的情况下,多线程中的各个操作的顺序是不可预测的。如:i++操作,多线程交替占用运行,多个线程返回相同的值。
  2. 活跃度的危险(活跃度失败):好事终会发生。1线程等待2线程的锁(独立占有的资源),2线程一直不释放这个锁,1线程一直等待。如:死锁,饥饿,活锁等等
  3. 性能危险(上下文切换):好事尽快发生。当调度程序临时挂起当前运行的程序时,另外一个线程开始运行—这个切换的操作是很频繁的。
    1. 线程调度,上下文的保存。
    2. 同步机制,限制编译器的优化
    3. 阻塞

一、概念


       1.进程:程序(任务)的执行过程--正在执行的(动态) 进程是资源(内存,文件)和线程的载体。

         每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)。

       2.线程: 系统执行的最小单元(线程共享进程的资源)【一个进程可以有多个线程】
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

       3.线程的交互:互斥,同步

       4.使用多线程的目的是更好的利用CPU资源

       5.多线程:一个程序(进程)运行时产生了多个线程。 

       6.并发与并行:

 

  • 并行:多个CPU实例(多台机器)同时执行一段处理逻辑,是真正的同时。又称共行性,是指能处理多个同时性活动的能力;并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
  • 并发:通过CPU调度算法(同一个CPU),让用户看上去同时执行(宏观上来看是同时的),实际上从CPU操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用tps或者qps来反应这个系统的处理能力。在微观上他们都是序列被处理的,只不过资源不会在某一个上被阻塞(一般是通过时间片轮转)。如果是同一时刻到达的请求也会根据优先级的不同,而先后进入队列排队等候执行。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
  • 并发和并行的区别:
    1.并发是一次处理很多事情(一个人做多件事情),并行是同时做很多事情(多个人做很多事情)
    2.并发是独立执行过程的组合,而并行是同时执行(可能相关的)计算。
    3.并发是两个任务可以在重叠的时间段内启动,运行和完成。并行是任务在同一时间运行,例如,在多核处理器上。
    4.并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。 
  • 通过多线程实现并发,并行:

    1. java中的Thread类定义了多线程,通过多线程可以实现并发或并行。
    2. 在CPU比较繁忙,资源不足的时候(开启了很多进程),操作系统只为一个含有多线程的进程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
    3. 在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
    4. 至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所有,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
    5. 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。

二、java对线程的支持


    1.Runnable接口

    2.Thread类

    3.Callable接口

join():线程A中调用线程B的B.join()方法,那么线程A放弃cpu资源,等待线程B执行完毕之后线程A再继续执行。可以用来将并行的线程改为串行执行。并且只有线程A调用了start()方法之后才能调用join()方法。join是通过调用wait()方法来实现的。

yield():就是当前线程不忙的时候,暗示自己的资源可以让出来。

Thread.yield();

  • Yield是一个静态的原生(native)方法
  • Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
  • Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
  • 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
  • 放弃当前cpu资源,但放弃的时间不确定,有可能刚放弃,马上又获得cpu时间片。

 

  1. 当线程的优先级没有指定时,所有线程都携带普通优先级。
  2. 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
  3. 优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
  4. 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
  5. 由调度程序决定哪一个线程被执行。
  6. t.setPriority()用来设定线程的优先级。
  7. 记住在线程开始方法被调用之前,线程的优先级应该被设定。
  8. 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级
  9. 优先级具有继承性,A线程启动B线程,则A和B的优先级是一样的。
  10. 优先级具有规则性:cpu尽量将执行资源让给优先级高的线程,是尽量,不是绝对。
  11. 优先级具有随机性:高优先级优先执行不是绝对,只是尽量。也有可能低优先级比高优先级先执行。

wait():让当前线程进入等待状态,并释放锁,一直到其他线程调用当前线程的notify或者notifyall方法,或者超时,等待的线程才会被唤醒,进入可运行状态。

notify:随机唤醒等待同一资源的线程。

notifyall:唤醒所有等待同一资源的线程。

 

注:wait和notify必须被包含在synchronized里面,否则会抛异常,

      wait执行之后立马释放锁,  notify执行之后,会继续吧加锁的代码执行完了之后才释放锁。

 

ThreadLocal:线程可以用该类set()值,并且多线程相互之间set的值互不影响get的获取。

InheritableThreadLocal:可以继承父线程才值,但是如果子线程获取值的同时,父线程修改了值,那么子线程获取到的依然是旧的值。

 

 

Thread和Runnable的区别

    如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

 

Callable和Runnale的区别

Callable可以有返回值和抛出异常。

 


  三、线程的状态


    线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

 

 

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

  • 取消和中断:

interrupt();

  • 不安全的取消

单独使用一个取消标志位.

Stop(),suspend(),resume()是过期的api,很大的副作用,容易导致死锁或者数据不一致。

  • 安全的终止线程

      使用线程的中断 :

interrupt() 中断线程标记,本质是将线程的中断标志位设为true,其他线程向需要中断的线程打个招呼。是否真正进行中断由线程自己决定。

isInterrupted() :线程检查自己的中断标志位,不会将状态重置。

interrupted():静态方法Thread.interrupted() 返回当前中断状态并且中断标志位复位为false:配合Thread.curentThread().interrupt(),来中断当前线程。

由上面的中断机制可知Java里是没有抢占式任务,只有协作式任务。

为何要用中断,线程处于阻塞(如调用了java的sleep,wait等等方法时)的时候,是不会理会我们自己设置的取消标志位的,但是这些阻塞方法都会检查线程的中断标志位

当使用

Thread t1 = new Thread(new MyThread());

t1.interrupt();

t1设置的中断标记不会作用到MyThread类中。

 

中断标志是Thread的而非Runnable的。

在沉睡中停止:当wait或sleep遇到interrupt,会抛出InterruptedException

 

  • 不可中断的阻塞

 java.io中的同步 socket I/O:inputSream 和outputStream中的read和write方法都不响应中断。通过关闭底层的Socket来使其抛出一个SocketException。

java.nio中的同步I/O:

Selector的异步I/O:

注:在Thread中通过覆写interrupt来封装非标准取消。

public class OverrideInterrupt extends Thread {
    private final Socket socket;
    private final InputStream in;

    public OverrideInterrupt(Socket socket, InputStream in) {
        this.socket = socket;
        this.in = in;
    }



    @Override
    public void interrupt() {
        try {
            //关闭底层的套接字
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
            //.....
        }finally {
            //同时中断线程
            super.interrupt();
        }

    }
    @Override
    public void run(){
        try{
            byte[] buf = new byte[10];
            while(true){
                //
            }
        }catch (Exception e){
            //允许线程退出
        }
    }
}

 

线程间的通信:

通过管道来实现,pipedStream(pipedInputStream,pipedOutputStream)和pipedWriter,pipedReader


四、 线程调度


线程的调度

1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。

 

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY

          线程可以具有的最高优先级,取值为10。

static int MIN_PRIORITY

          线程可以具有的最低优先级,取值为1。

static int NORM_PRIORITY

          分配给线程的默认优先级,取值为5。

 

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

 

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。

线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

 

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

 

3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

 

4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

 

5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

 

6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

 注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。


五、常用函数 



 六、volitale


(1)java内存与多线程中的三个概念

1. 原子性:指一些操作要么全部执行,要么全部不执行。

在java中,基本数据类型的读取赋值都是原子性操作。即这些操作是不可被中断的,要么执行,要么不执行。

        i=3; //原子性;
        i = i+2;
        ++i;
        j = i;

2.可见性:多个线程操作一个变量,变量值被线程修改时,其他线程能够立即看见修改后的值。

每个线程都有自己的线程栈,当线程执行需要的数据时,会再主存(内存)中数据拷贝到自己的线程栈中,然后对自己线程栈中的副本进行操作,操作完了之后将数据save回到内存中。

java提供的volitale可以保证数据的可见性

3.有序性:程序按照代码的先后顺序执行,编译器编译时会对代码进行指令重排,编译器通过限制语句的执行顺序来保证程序的正确性,重排之后的代码可能和我们写的时候的顺序不一样。 

在单线程中,改变指令的顺序可能不会产生不良后果,但是在多线程中就不一定了。例如:

 

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

由于语句1和语句2没有数据依赖性,所以编译器可能会将两条指令重新排序,如果先执行语句2,这时线程1被阻塞,然后线程2的while循环条件不满足,接着往下执行,但是由于context没有赋值,于是会产生错误。

(2)volitale关键字的作用

volitale保障了可见性和一定程度的有序性,但是不保证原子性。

 

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

  先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

 

  这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效;

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

2、volitale关键字不能保证原子性 

  下面看一个例子: 
  

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

 

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

3、volitale关键字在一定程度上保证有序性

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

 

即使不使用volatile关键字,jvm也会尽量保证变量的可见性,比如sleep一段时间后,其它线程再去取就能取到最新,但是如果cpu一直很繁忙,那么就不能保证可见性,volatile是强制保证变量的可见性。

volatile修饰引用类型的时候,它只关心引用的变化,不关心引用内部属性的变化,所以volatile最好只修饰基本类型,不要去修饰引用类型。

结论:当没有把握的时候,最好不要使用。

volatile基于内存屏障实现,保证一定的有序性

volatile基于实现保证可见性

总线风暴:

由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。

解决办法:部分volatile和cas使用synchronize



概念:

锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。

1、锁开销 lock overhead 锁占用内存空间、 cpu初始化和销毁锁、获取和释放锁的时间。程序使用的锁越多,相应的锁开销越大

2、锁竞争 lock contention 一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁粒度越小,发生锁竞争的可能性就越小

3、死锁 deadlock 至少两个任务中的每一个都等待另一个任务持有的锁的情况锁粒度是衡量锁保护的数据量大小,通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在当单进程访问受保护的数据时锁开销小,但是当多个进程同时访问时性能很差。因为增大了锁的竞争。相反,使用细粒度的锁(锁数量多,每个锁保护少量的数据)增加了锁的开销但是减少了锁竞争。例如数据库中,锁的粒度有表锁、页锁、行锁、字段锁、字段的一部分锁 

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,线程状态切换会消耗cpu的处理时间。

1.锁的分类(按特性):

  • 公平锁/非公平锁
  • 可重入锁(递归锁)
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

1.1公平锁/非公平锁:

公平锁:指多个线程按照申请锁的顺序来获取锁。CLH队列排队

非公平锁:不按照申请锁的顺序来获取锁。可能会造成优先级反转或者饥饿现象,如:synchronized。 上来不排队,直接插队。

区别:非公平锁的吞吐量比公平锁大;

1.2可重入锁(递归锁):

同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。好处是可一定程度避免死锁。如:ReentrantLock,synchronized

可重入锁加锁和解锁的次数要相等。

1.3独享锁/共享锁:

都是通过AQS来实现的

独享锁:一次只能被一个线程所持有,如:synchronized,ReentrantLock,ReadWriteLock的写锁。
共享锁:可被多个线程所持有,如:ReadWriteLock的读锁。

1.4互斥锁/读写锁:

互斥锁 :ReentrantLock
读写锁:ReadWriteLock

1.5分段锁:

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作;

如:ConcurrentHashMap 其并发的实现就是通过分段锁的形式来实现高效的并发操作。

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

1.6偏向锁/轻量级锁/重量级锁:

是针对Synchronized的状态的锁,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

从jdk1.6开始为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。

详解转自:https://blog.csdn.net/u010648018/article/details/79750608

1.7自旋锁:

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    public class SpinLock {

        private AtomicReference<Thread> sign =new AtomicReference<>();

        public void lock(){
            Thread current = Thread.currentThread();
            while(!sign.compareAndSet(null, current)){
            }
        }

        public void unlock (){
            Thread current = Thread.currentThread();
            sign.compareAndSet(current, null);
        }
    }

使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。

当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。

 

1.8乐观锁/悲观锁:

悲观锁:(Pessimistic Lock)它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。有极强的占有和排他性。整个过程,数据呈现锁死状态,悲观锁一般依靠数据库的锁机制。悲观的认为,不加锁的并发操作一定会出问题

乐观锁:( Optimistic Locking )因为悲观锁往往依靠数据库的锁机制来实现,这样对数据库的性能有影响,乐观锁解决了这个问题,通过数据版本机制来实现乐观锁,给数据制定一个版本,读取数据的时候把版本一起读出来,更新数据时,版本号加1,在提交的时候,把读取的版本号和数据库当前的版本号进行比较,如果提交的版本号大于数据库版本号,则执行更新,否则失败。乐观的认为,不加锁的并发操作是没有事情的。

悲观锁就是java的锁机制,乐观锁就是不加锁,而通过CAS算法来实现。

java中的乐观锁:通过CAS来实现乐观锁的思想,比如AtomicInteger。当多个线程尝试使用CAS同时更新一个变量时,只有一个线程操作能执行成功,其他的都执行失败,并且失败的线程不会被挂起,并且可以再次尝试(循环)。

CAS操作中包含三个操作数:

 V:需要读写的内存位置

 A:用于比较的预期原值,类似数据的版本号。

 B:将被写入的新值

如果V的值与A的值相匹配,那么处理器就会讲该位置更新为B。反之,不做操作。 

CAS缺点:

1.ABA问题:线程1读取值为B,其他线程将A改为B再改为A,那么线程1最后去比较的时候,是能匹配的,但是其实值在中途已经被其他线程改过了。通过每次修改加标识(如版本号)来解决,如AtomicStampedReference就是这样解决的。

2.开销大:因为可能会自旋(循环),所以会有很大的开销。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

3.只能保证一个共享变量的原子操作:多个共享变量就会出现问题。通过将多个变量封装到一个对象中来解决,AtomicReference提供了保证引用对象之间的原子性。

当线程竞争少的时候使用乐光锁,当线程竞争大的时候就直接使用悲观锁。

2.synchronized

多线程访问同步方法的7种情况:

  1. 两个线程同时访问一个对象的同步方法:
    串行执行
  2. 两个线程访问的是两个对象的同步方法:
    并行方法,不是一个对象。
  3. 两个线程访问的是synchronized的静态方法:
    串行执行,只有一个Class对象
  4. 同时访问同步方法和非同步方法:
    串行执行
  5. 访问同一个对象的不同的同步方法:
    串行,this是同一个
  6. 同时访问静态synchronized和非静态的synchronized方法:
     并行执行,锁对象不是同一个
  7. 方法抛出异常后,释放锁:
    synchronized会自动释放,Lock等不会

核心思想

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
  2. 每个实例都对应有自己的一把锁,不同实例之前互不影响;
    例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应2、3、4、6种情况)
  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)

ps:不能使用string常量,Integer,Long来做锁,因为重写了equals,虽然连个字符串相等,但是实际上不是一个对象。要使用的话要使用,string.intern();

synchronized("A");synchronized("A"); 两个A虽然相等,但是实际上是两个对象。

性质:

1、可重入:

​ 同一个线程的外层函数获取锁后,内层函数可以直接再次获取该锁。

2、不可中断

原理

1、加锁和释放锁的原理:Monitor

2、可重入原理:加锁次数计数器。

3、可见性原理:内存模型图

https://www.cnblogs.com/paddix/p/5367116.html

https://www.jianshu.com/p/d53bf830fa09

http://www.importnew.com/21866.html

实现原理:https://blog.csdn.net/thousa_ho/article/details/77992743

https://ifeve.com/java-synchronized/?tdsourcetag=s_pcqq_aiomsg

缺点

1、效率低

  • 锁的释放情况少
  • 视图获取锁时不能设置超时时间
  • 不能中断一个正在视图获取锁的线程

2、灵活度较差

  • 加锁和释放锁的时机单一
  • 每个锁仅有单一的对象

3、无法知道是否成功获取到锁

用法:

对象锁:只要多线程访问的是同一个对象(new object()),就会有锁。

类锁:多线程访问的是对象属于同一个类型,才会有锁。

注:无论synchronized怎么使用(修饰方法或者修饰代码块,相同或者不同的方法或者代码块)

       只要是对象锁,同一个对象就会有锁;

       只要是类锁,只要是同一个类就会有锁。

1、synchronized 修饰方法时​

 ​​a.修饰非静态方法时,锁住的是对象

 b.修饰静态方法时,锁住的是类

2.synchronized修饰代码块

a.静态方法中的代码块,只能synchronized(obj.class),锁住类

b.非静态方法中的代码块,synchronized既可以锁对象,也可以是类,如:

synchronized(this)//当前对象

synchronized(obj)//某个对象

synchronized(obj.class)//某个对象的类型

 注:synchronized关键字不会被子类继承,override的时候,如果没有重写则可以继承,可以通过子类的锁调用父类的加锁方法。

 

 

注意点

1、锁对象不能为空、作用域不宜过大、避免死锁

2、尽量使用JUC包下的类,再考虑Synchronized,再考虑Lock

3.Lock

https://www.cnblogs.com/dolphin0520/p/3923167.html

https://www.cnblogs.com/aishangJava/p/6555291.html

ReentrantLock:https://blog.csdn.net/u014730165/article/details/82144848

ReentrantReadWriteLock:https://www.cnblogs.com/zaizhoumo/p/7782941.html

 

AQS:https://www.cnblogs.com/daydaynobug/p/6752837.html

https://blog.csdn.net/zhangdong2012/article/details/79983404

设计模式:模板方法

CLH队列:双向链表

 

 

 

4.lock和synchronized的区别

https://www.cnblogs.com/iyyy/p/7993788.html

https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1


线程池


1. 线程池的优点

  • 降低资源的消耗
  • 提高响应速度,任务:T1创建线程时间,T2任务执行时间,T3线程销毁时间,线程池没有或者减少T1和T3
  • 提高线程的可管理性。

2. 线程池的主要处理流程

1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。

2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

3. ThreadPoolExecutor

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

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

corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;

如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize

keepAliveTime

线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用

TimeUnit

keepAliveTime的时间单位

workQueue

workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能

threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名

Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”

RejectedExecutionHandler(饱和策略)

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

(1)AbortPolicy:直接抛出异常,默认策略;

(2)CallerRunsPolicy:用调用者所在的线程来执行任务;

(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

(4)DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

 

关闭线程池

ShutDown():interrupt方法来终止线程

shutDownNow() 尝试停止所有正在执行的线程

       

合理地配置线程池

        线程数配置:

  • 任务:计算密集型,IO密集型,混合型

  • 计算密集型=计算机的cpu数或计算机的cpu数+1(应付页缺失)

  • IO密集型=计算机的cpu数*2

  • 混合型,拆分成计算密集型,IO密集型

  • Runtime.getRuntime().availableProcessors();当前机器中的cpu核心个数

  • 尽量有界队列,不要使用无界队列

 

Executor框架调度模型:

在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。

在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。

 

 

三大组成部分:任务,任务的执行,异步计算的结果

        

任务

包括被执行任务需要实现的接口:Runnable接口或Callable接口。

任务的执行

包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。

异步计算的结果

包括接口Future和实现Future接口的FutureTask类。

成员结构

Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。

ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;

AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;

ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。

ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;

ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。

Future接口和实现Future接口的FutureTask类,代表异步计算的结果。

Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。

Executor框架基本使用流程

主线程首先要创建实现Runnable或者Callable接口的任务对象。

工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnablecommand));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(Executor-Service.submit(Runnable task)或ExecutorService.submit(Callable<T>task))。

如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。

最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

 

ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。

FixedThreadPool

创建使用固定线程数的FixedThreadPool的API。适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较重的服务器。FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。

当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。

2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。

3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。

4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或

shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

SingleThreadExecutor

创建使用单个线程的SingleThread-Executor的API,适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。

corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

CachedThreadPool

创建一个会根据需要创建新线程的CachedThreadPool的API。大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。

FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

WorkStealingPool

利用所有运行的处理器数目来创建一个工作窃取的线程池,使用forkjoin实现。

ScheduledThreadPoolExecutor

使用工厂类Executors来创建。Executors可以创建2种类

型的ScheduledThreadPoolExecutor,如下。

·ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。

·SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。

ScheduledThreadPoolExecutor适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。

SingleThreadScheduledExecutor适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。

 

对这4个步骤的说明。

1)线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。

2)线程1执行这个ScheduledFutureTask。

3)线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。

4)线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(Delay-

Queue.add())。

有关提交定时任务的四个方法:

//向定时任务线程池提交一个延时Runnable任务(仅执行一次)

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

 

//向定时任务线程池提交一个延时的Callable任务(仅执行一次)

public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

 

//向定时任务线程池提交一个固定时间间隔执行的任务

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,

                                                  long period, TimeUnit unit)

 

//向定时任务线程池提交一个固定延时间隔执行的任务

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,

                                                      long delay, TimeUnit unit);

固定时间间隔的任务不论每次任务花费多少时间,下次任务开始执行时间是确定的,当然执行任务的时间不能超过执行周期。

固定延时间隔的任务是指每次执行完任务以后都延时一个固定的时间。由于操作系统调度以及每次任务执行的语句可能不同,所以每次任务执行所花费的时间是不确定的,也就导致了每次任务的执行周期存在一定的波动。

注意:定时或延时任务中所涉及到时间、周期不能保证实时性及准确性,实际运行中会有一定的误差。

ScheduleThreadPoolExecutor与Timer相比的优势:

(1)Timer是基于绝对时间的延时执行或周期执行,当系统时间改变,则任务的执行会受到的影响。而ScheduleThreadPoolExecutore中,任务时基于相对时间进行周期或延时操作。

(2)Timer也可以提交多个TimeTask任务,但只有一个线程来执行所有的TimeTask,这样并发性受到影响。而ScheduleThreadPoolExecutore可以设定池中线程的数量。

(3)Timer不会捕获TimerTask的异常,只是简单地停止,这样势必会影响其他TimeTask的执行。而ScheduleThreadPoolExecutore中,如果一个线程因某些原因停止,线程池可以自动创建新的线程来维护池中线程的数量。

scheduleAtFixedRate定时任务超时问题

若任务处理时长超出设置的定时频率时长,本次任务执行完才开始下次任务,下次任务已经处于超时状态,会马上开始执行.

若任务处理时长小于定时频率时长,任务执行完后,定时器等待,下次任务会在定时器等待频率时长后执行

如下例子:

设置定时任务每60s执行一次

若第一次任务时长80s,第二次任务时长20ms,第三次任务时长50ms

第一次任务第0s开始,第80s结束;

第二次任务第80s开始,第110s结束;(上次任务已超时,本次不会再等待60s,会马上开始),

第三次任务第150s开始,第200s结束.

第四次任务第210s开始.....

 

Callable、Future和FutureTask详解

Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。

当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。

Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。

除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。

Executors提供的,把一个Runnable包装成一个Callable的API。

public static Callable<Object> callable(Runnable task)  // 假设返回对象Callable1

Executors提供的,把一个Runnable和一个待返回的结果包装成一个Callable的API。

public static <T> Callable<T> callable(Runnable task, T result)  // 假设返回对象Callable2

当任务成功完成后FutureTask.get()将返回该任务的结果。例如,如果提交的是对象Callable1,FutureTask.get()方法将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。

FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。

当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常。

当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会被执行;当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;当FutureTask处于已启动状态时,执行FutureTask.cancel(false)方法将不会对正在执行此任务的线程产生影响(让正在执行的任务运行完成);当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。

CompletionService详解

CompletionService实际上可以看做是Executor和BlockingQueue的结合体。CompletionService在接收到要执行的任务时,通过类似BlockingQueue的put和take获得任务执行的结果。CompletionService的一个实现是ExecutorCompletionService,ExecutorCompletionService把具体的计算任务交给Executor完成。在实现上,ExecutorCompletionService在构造函数中会创建一个BlockingQueue(使用的基于链表的无界队列LinkedBlockingQueue),该BlockingQueue的作用是保存Executor执行的结果。当计算完成时,调用FutureTask的done方法。当提交一个任务到ExecutorCompletionService时,首先将任务包装成QueueingFuture,它是FutureTask的一个子类,然后改写FutureTask的done方法,之后把Executor执行的计算结果放入BlockingQueue中。

与ExecutorService最主要的区别在于submit的task不一定是按照加入时的顺序完成的。CompletionService对ExecutorService进行了包装,内部维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。所以,先完成的必定先被取出。这样就减少了不必要的等待时间。

总结:

使用方法一,自己创建一个集合来保存Future存根并循环调用其返回结果的时候,主线程并不能保证首先获得的是最先完成任务的线程返回值。它只是按加入线程池的顺序返回。因为take方法是阻塞方法,后面的任务完成了,前面的任务却没有完成,主程序就那样等待在那儿,只到前面的完成了,它才知道原来后面的也完成了。

使用方法二,使用CompletionService来维护处理线程不的返回结果时,主线程总是能够拿到最先完成的任务的返回值,而不管它们加入线程池的顺序。

 

 

减少锁的竞争:

快进快出,缩小锁的范围,将与锁无关的,有大量计算或者阻塞操作的代码移出同步范围。

减小锁的粒度,多个相互独立的状态变量可以使用多个锁来保护,每个锁只保护一个变量。

锁的分段,例如ConcurrentHashMap中的实现。

减少独占锁的使用,例如读多写少的情况下,用读写锁替换排他锁。

 

 


底层实现原理和JMM


 

 

 

CountDownLatch

是一个同步工具类,用来协调多个线程之间的同步。

CountDownLatch countDownLatch = new CountDownLatch(N);

当线程1.执行countDownLatch.await();方法后,进入等待(等同wait()),当其他线程执行N次countDownLatch..countDown();之后,才会被唤醒。 

内部实现: AQS。

ps:只能在初始化的时候,定义N。并且只能使用一次。

 

CyclicBarrier:循环栅栏

让所有线程都等待完成后才会继续下一步行动。等到一批数量集合完成之后,再进行下一步。

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。 

await();进入等待,当等待的线程数量达到指定的数量,就会被唤醒,继续执行。

 

Semaphore :信号量

基于AQS实现的。内部也定义了一个Sync ,AQS的实现类

 

LockSupport:

LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。

主要的方法:park(阻塞线程)和unpark(唤醒线程)。

(1)wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以锁住线程。

(2)notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。unpark可以先执行,则park不阻塞。

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
  1. unpark调用时,如果当前线程还未进入park,则许可为true
  2. park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值