并发与多线程

并发与多线程

1. 进程和线程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在Java中,当启动main函数时就是启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区(JDK1.8之后的元空间)资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

2. 线程的创建方式

2.1继承Thread类,重写run()方法

a.定义一个类来实现Thread类;

b.重写Thread类的run()方法;

c.将需要执行的代码写在run()方法内部

d.创建Thread子类对象交替;

e.启动线程(用对象.start())

2.2 实现Runnable接口

a.定义一个类实现Runnable接口;

b.重写run()方法;

c.将要执行的代码写在run()方法内部

d.创建Runnable子类对象

e.将Runnable子类对象当做参数传入到Thread构造器中创建Thread对象

f.开启线程

2.3 实现Callable接口

a.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

b.创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

c.使用FutureTask对象作为Thread对象的target创建并启动新线程。

d.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

2.4 通过创建线程池的方法创建线程

三种方式的优缺点

采用继承Thread类方式:

(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。

(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

采用实现Runnable接口方式:

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

Runnable和Callable的区别:

(1)Callable规定的方法是call(),Runnable规定的方法是run()。

(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得。

(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常。

(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

start()和run()的区别

(1)start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行。

(2)run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)。

线程类的相关方法(补充):
a.设置线程名称

1.通过Thread类的构造方法

public Thread(String name)

2.通过Thread的成员方法

public final void setName(String name)

b.获取线程名称

public final String getName():返回该线程的名称

c.获取当前正在执行的线程

public static Thread currentThread():返回当前正在执行的线程的引用;

3. 多线程应用场景

多线程处理后台任务

通常会使用定时器来开启后台线程处理后台的任务,比如有些数据表的状态需要定时去修改、搜索引擎里面的数据需要定时去采集、定时生成统计信息、定时清理上传的垃圾文件等。

多线程异步处理任务

当需要处理一个耗时操作并且不要立刻知道处理结果时,可以开启后台线程异步处理该耗时操作,这样可以提高用户体验。比如用户下载简历时,需要将数据表中的数据生成简历附件并且通过邮件发送到用户邮箱,该操作也可以开启多线程异步处理。

多线程分布式计算

当处理一个比较大的耗时任务时,可以将该任务切割成多个小的任务,然后开启多个线程同时处理这些小的任务,切割的数量一般根据服务器CPU的核数,合理利用多核CPU的优势。比如下载操作可以使用多线程下载提高下载速度;清理文件时,开启多个线程,按目录并行处理等等。

4. 线程状态与转换

Java语言定义了6种线程状态:

NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED.

4.1 新建(New):

初始状态,线程创建后还未调用start方法。

4.2 就绪(Runnable):

一旦调用了start方法,线程就处于可运行状态。可运行状态的线程可能正在运行,也可能还没有运行而正在等待CPU时间片。(Java规范中并没有分为可运行状态和正在运行状态这两个状态,而是把它们合为一个状态。所以,可以把一个正在运行中的线程仍然称其处于可运行状态)。

就绪状态只是有资格运行。

  • 调用线程的start()方法,此线程进入就绪状态。
  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
  • 锁池里的线程拿到对象锁后,进入就绪状态。
4.3 阻塞(Blocked):处于阻塞状态的线程,并不会占用CPU资源。

以下情况会让线程进入阻塞状态:

  • sleep( )使线程在一定的时间内进入阻塞状态,不能得到cpu时间,但不会释放锁资源*。指定的时间一过,线程重新进入可执行状态。
  • wait( )使线程进入阻塞状态,同时释放自己占有的锁资源,和notify( )搭配使用。
  • suspend( )使线程进入阻塞状态,并且不会自动恢复,必须其对应的resume( )被调用,才能使线程重新进入可执行状态。因为有死锁倾向,在JDK1.5中已经废除。
4.4 等待(Waiting):

处于这种状态的线程不会被分配CPU时间片,需要等待其它线程显式地唤醒。

以下方法会让线程进入无限期的等待状态:

  • Object.wait() 方法结束:Object.notify()/Object.notifyAll()
  • Thread.join() 方法结束:被调用的线程执行完毕
  • LockSupport.park() 方法结束:LockSupport.unpark(currentThread)
4.5 超时等待(TimedWaiting):

处于这种状态的线程也不会被分配CPU时间片,在一定时间之后会被系统自动唤醒。

以下方法会让线程进入限期等待状态:

  • Thread.sleep(time) 方法结束:sleep时间结束
  • Object.wait(time) 方法结束:wait时间结束,或者Object.notify()/notifyAll()
  • LockSupport.parkNanos(time)/parkUntil(time) 方法结束:park时间结束,或者LockSupport.unpark(当前线程)
4.6 终止(Terminated):

线程结束任务之后自己结束,或者产生了异常而结束。

注:通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

5. 什么是上下文切换?

​ 多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

​ 概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

​ 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间,事实上,可能是操作系统中时间消耗最大的操作。

6. 什么是线程死锁?如何避免死锁?

​ 线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

①互斥条件:该资源任意一个时刻只由一个线程占用。

②请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

③不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

④循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

为了避免死锁,只要破坏产生死锁的四个条件中的其中一个就可以了。

①破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

②破坏请求与保持条件:一次性申请所有的资源。

③破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

④破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

检测死锁可以使用jconsole工具;或者使用jps定位进程id,再用jstack定位死锁。

7. sleep、yield、wait、join

sleep

1.调用sleep会让当前线程从Running进入TimedWaiting状态(阻塞)

2.其它线程可以使用interrupt方法打断正在睡眠的线程,那么被打断的线程这时就会抛出InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】

3.睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)

4.sleep存在异常,可用于模拟网络延迟、倒计时等。

5.建议用TimeUnit的sleep()代替Thread的sleep()来获得更好的可读性。

6.每一个对象都有一个锁,sleep不会释放锁

yield

1.调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程

2.具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片。

Join

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

补充:

1.sleep,join,yield,interrupted是Thread类中的方法

2.wait/notify是object中的方法

​ Sleep:不释放锁、释放cpu

​ Join:释放锁、抢占cpu

​ Yiled:不释放锁、释放cpu

​ Wait:释放锁、释放cpu

sleep()与wait()的区别
  • sleep()方法必须传入参数,参数为时间,时间到了自动醒来;wait()方法可传可不传,不传即为直接等待,传入之后为参数的时间结束后等待。
  • sleep()方法在同步方法或者同步代码块中不会释放锁;wait()方法在同步方法或者同步代码块中,会释放锁。
  • sleep不需要唤醒;wait()方法需要notify()或者notifyAll()方法唤醒。
  • 两者都可以暂停线程的执行。wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(longtimeout)超时后线程会自动苏醒。

1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。

2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。

3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。

4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。

5)从wait()方法返回的前提是获得了调用对象的锁。

8. 为什么调用start()方法时会执行run()方法,为什么不能直接调用run()方法?

​ new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

​ 总结:调用start()方法方可启动线程并使线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行。

9.线程同步

①同步代码块
a.什么时候同步?

并发时,有多段代码需要同时执行,我们希望某一段代码在执行时,不希望其他线程进入到该代码块中,只让某一个线程运行该段代码,这里理解的并不是说,只有这一个线程在执行,CPU不去执行其他的线程。

b.同步代码块

使用Synchronized关键字加上一个锁对象来定义一段代码,称为同步代码块。多个同步代码块如果使用同一个锁对象,那么他们就是同步的。

**注:**锁对象不能是匿名类对象,锁的是对象,锁的不是代码块。

②同步方法
a.使用synchronized关键字修饰一个方法,该方法中的所有代码都是同步的;
b.实现方式:在方法的返回值前面加上synchronized进行修饰
c.分类:
1.非静态方法:锁对象为当前类对象的实例,实例是存放在堆内存的;

public synchronized void show(){}

2.静态方法:锁对象为当前类的Class对象

public static synchronized show(){}

10. synchronized底层原理及其锁的升级与降级

10.1 synchronized作用

-原子性:synchronized保证语句块内操作是原子的

-可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)

-有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)

10.2 synchronized的使用

-修饰实例方法,对当前实例对象加锁

-修饰静态方法,多当前类的Class对象加锁

-修饰代码块,对synchronized括号内的对象加锁

10.3 实现原理

​ jvm基于进入和退出监视器锁(Monitor对象)来实现方法同步和代码块同步。

​ 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_infoStructure)中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

​ 代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

10.4 这里要注意:

​ synchronized是可重入的,所以不会自己把自己锁死。

​ synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。

10.5 三种锁的状态是通过对象监视器在对象头中的字段来表明的

​ 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

​ 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

​ 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

synchronized的锁升级

​ 所谓锁的升级、降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

​ 当没有竞争出现时,默认会使用偏向锁。JVM会利用CAS操作(compareandswap),在对象头上的MarkWord部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。

​ 如果有另外的线程试图锁定某个已经被偏向过的对象,JVM就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作MarkWord来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

锁的升级过程(简述一下锁的升级过程)
  • 无锁状态:刚刚new出来对象的时候,并没有任何锁相关的事情。
  • 偏向锁状态:当一个线程访问同步块并获取锁时,上偏向锁指的是将markword中的线程ID改为自己的线程ID的过程。
  • 轻量级锁(自旋锁):当另外一个线程来进行竞争时,就会由升级为轻量级锁,同时撤销偏向锁。 线程在自己的虚拟机栈的栈帧生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LockRecord的指针,设置成功者得到锁。
  • 重量级锁:当有较多的线程同时运行时,会造成部分线程自旋时间较长,严重消耗CPU资源,此时JVM会升级为重量级锁。其他线程进入到该锁的等待队列中,按照顺序依次获取锁。
synchronized的优化
a.锁的细化(细化锁)

当一个方法中,存在前后两段业务逻辑代码,中间是需要加锁的代码,不应该将整个方法加锁,而是应该在该段代码加锁。

b.锁的粗化(粗化锁

当某一个方法中存在很多的细化锁,可以考虑将其转为粗化锁,即将锁加在整个方法上。

11. ReentrantLock

相对于synchronized它具备如下特点

  • 可中断

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

  • 可以设置超时时间

  • 可以设置为公平锁

    • 可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(booleanfair)构造方法来制定是否是公平的。
  • 支持多个条件变量,即对与不满足条件的线程可以放到不同的集合中等待

    • 等待可中断:

      可实现选择性通知(锁可以绑定多个条件):synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

      Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

  • 与synchronized一样,都支持可重入

    • “可重入锁”表示该锁能够支持一个线程对 资源的重复加锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
  • synchronized依赖于JVM而ReentrantLock依赖于API

    • synchronized是依赖于JVM实现的,前面我们也讲到了虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
  • 释放锁

    Synchronized:不需要手动释放锁,当执行完代码时,会自动释放锁;

    ReentrantLock:需要手动释放锁,采用lock()和unlock()并配合try catch使用(try用睡眠方法调试出)。

12.并发编程的三个重要特性

  • 原子性:一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized可以保证代码片段的原子性。
  • 可见性:当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile关键字可以保证共享变量的可见性。
  • 有序性:代码在执行的过程中的先后顺序,Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile关键字可以禁止指令进行重排序优化。

13.线程之间通信

13.1 为什么需要通信

多个线程并发时,默认情况下cpu是随机切换线程的,如果我们希望他们有规律的执行,则需要使用通信。

13.2 如何实现线程通信

a.如果希望线程等待,就调用wait();

b.如果希望唤醒等待的线程,采用notify();

**注:**这另个方法必须在同步代码块中执行,并且使用同步锁对象来调用

13.3 方法补充(☆):
a.public final void wait():

在其他线程调用此对象的notify()方法或者notifyAll()方法前,导致当前线程等待。

b.public final void notify():

随机唤醒在此对象的监视器上等待的单个线程。

c.public final void notifyAll():

唤醒在此对象的监视器上等待的所有线程。

13.4 三个及以上线程通信

notifyAll()是唤醒所有线程;

如果多个线程之间通信,需要使用notifyAll()通知所有线程,用while来反复判断条件

两个线程轮流打印例子
public class Demo_notify {
	/**
	 * 两线程之间通讯
	 */
	public static void main(String[] args) {
		final Demo d=new Demo();
		//线程1
		new Thread(){
			public void run(){
                //一定要在循环里执行
				while(true){
					try {
						d.show1();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
		//线程二:
		new Thread(){
			public void run(){
				while(true){
					try {
						d.show2();
					} catch (InterruptedException e) {
						
						e.printStackTrace();
					}
				}
			}
		}.start();
	}

}
//两线程交替进行
class Demo{
	private  int flag=1;
	public void show1() throws InterruptedException{
		synchronized (this) {
//过程解释:两个线程都开启,然后假设线程1,抢到了执行权,则拿到了锁对象,此时线程2,得不到锁,无法打印
//然后打印“赵钱孙李”,然后flag为2
//执行唤醒,此时两个线程都是醒着的,然后,循环进行,然后线程1变为等待状态,然后释放了锁对象。
//线程2就拿到了锁对象,然后打印“周吴郑王”,然后flag变为1,然后执行了对象的notify(),此时线程1被唤醒
//这个时候也就是都醒着,然后循环继续,flag不为2,线程2变为等待状态,然后释放锁对象,线程1再继续。
			if (flag!=1) {
				//线程等待
				this.wait();
			}
			System.out.print("赵");
			System.out.print("钱");
			System.out.print("孙");
			System.out.print("李");
			System.out.print("\r\n");
			flag=2;
			this.notify();
		}
	}
	public void show2() throws InterruptedException{
		synchronized (this) {
			if (flag!=2) {
				this.wait();
			}
			System.out.print("周");
			System.out.print("吴");
			System.out.print("郑");
			System.out.print("王");
			System.out.print("\r\n");
			flag=1;
			this.notify();
		}
	}
}

14.volitile是什么?

是java虚拟机提供的轻量级的同步机制。

a.内存可见性

一个线程修改了共享变量的值,其他线程是立即可见的。

b.禁止指令重排序

cpu和编译器在程序运行期间,会对其进行优化,为了保证效率,可能会对指令进行重新排序,但是在多线程环境下,容易产生线程安全问题,因此采用volatile修饰能够保证进行指令重排序

c.不保证原子性
什么是内存可见性?

内存可见性指的是:当一个线程将共享变量拷贝回工作内存并修改后,将修改后的变量写回主物理内存,其他线程能够第一时间内被通知到该变量已经被修改。这种性质被称为内存可见性。

volitile是如何保证内存可见性的?

volatile保证可见性的原因在于其与内存屏障有关;

内存屏障的作用:确保特定操作执行的顺序,影响数据的可见性,强制更新一次不同的cpu缓存。

当线程A修改完主物理内存***享变量后,会强制令其他线程中的共享变量的值无效,然后迫使其他线程读取最新的修改后的值。

写一个volatile时,JMM会首先会修改线程工作内存的值,然后刷新主物理内存的值;

image

读一个volatile变量时,JMM会强制令其他线程中的本地变量无效,然后从主内存中读取新的变量值。

volatile的具体应用场景有哪些?

a:volatile修饰主物理内存共享变量,为防止多线程并发安全问题,元素递增,采用AutomicInteger等原子类;

b:单例模式中,实例变量需要用volitile修饰;

volatile为什么不能保证原子性?结合场景描述

原子性指的是某个线程在操作时,不能被中断或者是分割;

多个线程操作同一个变量时,正常情况下,应该为每个线程轮流执行,但是中间有可能出现线程被挂起,而当线程被唤醒时,没有来得及被通知到内存中的变量的值改变,然后就写回主物理内存,造成了写覆盖的现象。

如当前有两个线程A,B;两个线程操作共享变量+1操作,共享变量采用volatile进行修饰,初始为0,此时线程A执行加1操作,此时线程B可能被挂起,当线程B被唤醒时,还没来得及获取到共享变量的值时,仍然认为当前的值为0,于是进行了加1操作,此时,造成了写覆盖,本来是为2,但是现在为1,这种现象是因为volatile不保证原子性。

volatile是如何保证禁止指令重排序的?
a.内存屏障定义及作用:

内存屏障又称为内存栅栏,是一条CPU指令,主要作用有两个:

①保证特定操作执行的顺序;

②保证某系某些变量的内存可见性;

b.如何保证内存可见性

由于编译器和处理器都能执行指令重排序优化,如果在指令间插入一条内存屏障,则不管什么指令都不能和这条内存屏障指令重排,也就是通过插入内存屏障,就能禁止在内存屏障前后的指令进行重排优化。

具体实现:

  • 在每个volatile的写操作前后加内存屏障;

前面加内存屏障,作用是防止上面的普通写和下面的的volatile写重排序

下面的内存屏障,作用是防止volatile写与下面可能有的volatile写/读重排序

  • 在每个volatile的读操作之后加两道内存屏障

第一个内存屏障的作用,禁止下面所有的普通读操作与volatile读操作重排序

第二个内存屏障的作用,禁止下面的所有的普通写操作与volatile读操作重排序

15. 说说synchronized关键字和volatile关键字的区别

synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
  • volatile关键字能保证数据的可见性和有序性,但不能保证数据的原子性。synchronized关键字都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
volatile 的底层实现原理是内存屏障
  • 对volatile变量的写指令后会加入写屏障
  • 对 volatile变量的读指令前会加入读屏障
如何保证可见性:
  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
  • 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。
如何保证有序性:
  • 在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序。
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
volatile不能解决指令交错,不能保证原子性:
  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其它线程的读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序。
  • 多个线程操作同一个变量时,正常情况下,应该为每个线程轮流执行,但是中间有可能出现线程被挂起,而唤醒时,没有来得及被通知到主内存中的值改变,造成写覆盖的现象。
  • 解决方法
    • 采用synchronized对方法进行加锁(“大材小用”)
    • 采用juc下的atomic类相关子类(如atomicInteger类),其采用的是CAS原理,保证原子性
volatile主要用在一个线程改多个线程读时的来保证可见性,和double-checked locking模式中保证synchronized代码块外的共享变量的重排序问题。

16. 使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,13.4还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

17. 如何创建线程池

通过Executor框架的工具类Executors来实现我们可以创建三种类型的ThreadPoolExecutor:

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors返回线程池对象的弊端如下:
  • FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

  • CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。

通过ThreadPoolExecutor的方式,ThreadPoolExecutor构造函数7个重要参数分析:
  • corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
  • keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
  • unit:keepAliveTime参数的时间单位。
  • threadFactory:executor用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字。
  • handler:拒绝策略。
ThreadPoolExecutor拒绝策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。

  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。

  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被唤醒。

当提交一个新任务到线程池时,线程池的处理流程如下。
  • 当创建了线程池后,会等待提交的任务申请

  • 当调用execute()方法添加一个任务时,线程池进行如下判断:

    ①:如果正在运行的线程数小于核心线程数,将创建新的线程运行这个任务;

    ②:如果正在运行的线程数大于或者等于核心线程数,则将任务放入阻塞队列中;

    ③:如果阻塞队列已满且正在运行的线程数小于最大线程数量,则创建非核心线程执行任务;

    ④:如果阻塞队列已满并且线程运行数等于最大线程数,则执行饱和拒绝测策略。

    c.当一个线程完成任务时,会从阻塞队列中取出下一个任务来执行。

    d.当一个空闲线程超过一定的时间,线程池进行判断,如果当前线程数大于核心线程数,则会进行线程回收。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPollByhands {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ExecutorService threadpoll=new ThreadPoolExecutor(
				2, 
				5, 
				1L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
		
		//模拟十个办理业务人员
		for (int i = 0; i < 10; i++) {
			
			threadpoll.execute(new Runnable(){
				public void run(){
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				}
			});
		}
		threadpoll.shutdown();
	}
}
ThreadPoolExecutor执行execute方法分下面4种情况。
  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

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

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

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

向线程池提交任务:

execute()和submit()方法

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线 程一段时间后立即返回,这时候有可能任务没有执行完。
关闭线程池

shutdown或shutdownNow方法,它们的原理是遍历线 程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务 可能永远无法终止。

  • shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
  • shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

​ 只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。

​ 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪 一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭 线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

线程池的线程数量

性质不同的任务可以用不同规模的线程池分开处理。

b.IO密集型:IO密集型任务线程并不是一直在执行,因此可以设置为CPU核心数*2

  • CPU密集型:该任务需要大量的运算,没有阻塞,CPU一直全速运行。任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。
  • 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
  • 混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量 将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。

可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

线程池五种状态
  • RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务
  • SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。
  • STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态
  • TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。
  • TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态

18. 原子类

Atomic是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所谓原子类就是具有原子/原子操作特征的类。

使用原子的方式更新基本类型:
  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类
数组类型:使用原子的方式更新数组里的某个元素
  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型:

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
  • AtomicMarkableReference:原子更新带有标记位的引用类型对象的属性修改类型。
  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器。
AtomicInteger的方法:
  • publicfinalintget()//获取当前的值
  • publicfinalintgetAndSet(intnewValue)//获取当前的值,并设置新的值
  • publicfinalintgetAndIncrement()//获取当前的值,并自增
  • publicfinalintgetAndDecrement()//获取当前的值,并自减
  • publicfinalintgetAndAdd(intdelta)//获取当前的值,并加上预期的值
  • booleancompareAndSet(intexpect,intupdate)//如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
  • publicfinalvoidlazySet(intnewValue)//最终设置为newValue,使用lazySet设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger原理:

​ AtomicInteger类主要利用CAS(compareandswap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

​ CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是valueOffset。另外value是一个volatile变量,在内存中可见,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。

19. ConcurrentHashMap为什么可以线程安全?1.7

​ 线程不安全的HashMap:在多线程环境下,使用HashMap进行put操作会引起死循环。HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表 形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获 取Entry。

​ HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

​ ConcurrentHashMap的锁分段技术可有效提升并发访问率。锁分段技术:首先将数据分成一段一段地存 、储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

​ ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

20. 线程间通信

volatile和synchronized关键字

​ Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

​ 关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要 从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

20.ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。
主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
场景1:

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

场景2:

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

21. 生产者消费者模型 为什么加锁 什么情况不用加锁

阻塞队列
①定义(什么是阻塞队列)

首先阻塞队列是一个队列,在队列一段线程1相对列中插入元素,线程2在队列另一端获取元素,当队列中元素已满时,线程1阻塞,当队列中元素为空时,线程2阻塞。

85907e24d7ec23ca6efa31195843b0e.png

②作用

在某些情况下,我们需要挂起线程,然后再需要的时候将其唤醒,而阻塞队列的作用为不需要人为的挂起和唤醒线程,能够实现自我管理线程。

③阻塞队列架构(BlockingQueue接口

继承自queue接口,间接继承自collection

④常见阻塞队列接口实现类

a.ArrayBlcokingQueue

底层以数组实现的阻塞队列

b.linkedBlockingQueue

底层以链表实现的阻塞队列

c.同步synchronousBlockingQueue

特点:SynchronousQueue是一个 不存储元素的BlcokingQueue,每一次put都需要take()的完成,否则不能添加元素;每一个take()都需要一个put()才行,否则取不到元素( 进一个,我立马取一个)。

注:

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是 从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

​ 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦。如果队列里的产品已经满了,生产者就不能继续生产;

  • 如果队列里的产品从无到有,生产者就得通知一下消费者,告诉它可以来消费了;
  • 如果队列里已经没有产品了,消费者也无法继续消费;
  • 如果队列里的产品从满到不满,消费者也得去通知下生产者,说你可以来生产了。

所以它们之间还需要有协作,最经典的就是使用 Object 类里自带的 wait() 和 notify() 或者 notifyAll() 的消息通知机制。

22. Java中的并发工具类

①countDownLatch(倒计时发射)

a.定义:同步辅助类,在完成一组正在其他线程中的操作之前,允许一个或多个线程一致等待

b.使用步骤:

1.首先用给定值初始化countDownLatch;

2.调用**countDown()**方法,然后每当一个线程执行完后,就-1;

3.采用**await()**一直阻塞,直到countDownlatch的值减到0;

public class CountDLDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		CountDownLatch cd=new CountDownLatch(6);
		for (int i = 0; i < 6; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(Thread.currentThread().getName()+"\t 同学上完自习,走了");
					cd.countDown();
				}
			}.start();
		}
		try {
            //后面的主线程需要等待前面的六个线程执行完才能执行。
			cd.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"\t 班长锁门了");
	}

}
②cyclicBarrier(人满了把栅栏推到,开始跑)

a.定义:可循环使用的屏障,主要是让一组线程达到一个屏障前被阻塞,直到最后一个线程达到屏障时,屏障才会被打开,被拦截的线程才会继续进行,如果想让达到一定数量后执行某种操作,可以在构造器的第二个参数传入执行的Runnable子类对象。

b.使用:构造函数CyclicBarrier(int nums,Runnable barrier-Action)

当阻塞线程达到nums时,会优先执行barrierAction

阻塞采用的是 cb.await();

c.代码(集齐七颗龙珠,召唤神龙)

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBDemo1 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		CyclicBarrier cb=new CyclicBarrier(7,new Runnable(){
			public void run(){
				System.out.println("召唤神龙");
			}
		});
		for (int i = 0; i < 7; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(Thread.currentThread().getName());
					try {
						cb.await();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (BrokenBarrierException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}

//例子2:人满发车

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
//人满发车
public class CyclicBarrierDemo2 {
public static void main(String[] args) {
	// TODO Auto-generated method stub
	CyclicBarrier cb=new CyclicBarrier(20, new Runnable(){
		public void run(){
			System.out.println("20人满,发车");
		}
	});
	
	for (int i = 0; i < 100; i++) {
		new Thread(){
			public void run(){
				try {
					cb.await();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}.start();
	}
}

d.应用场景

可以用于多线程计算数据,最后合并计算结果的场景。

③控制线程数的Semaphore(6辆车只有3个车位)

a.定义:用来控制同时访问特定资源的线程数量,通过协调各个线程,保证合理利用资源(限流)。

b.使用方法:

1.public void auquire():从此信号量获取一个许可,在提供一个许可之前将一直阻塞。

2.public void relese():释放一个许可,将许可返回给信号量。

3.构造方法:public Semaphore(int nThreads,boolean fair):创建具有给定许可数和给定公平设置的Semaphore。公平的意思是:允许线程按照先来后到的顺序获取许可证。

c.代码:

public class SemaphoreDemo {
	//模拟总共有6辆车,然后只有三个停车位置
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//这里模拟只有三个车位
		Semaphore s=new Semaphore(3, true);
		//模拟存在六辆车
		for (int i = 0; i < 6; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					//申请车位
					try {
						//申请到车位
						s.acquire();
						System.out.println(Thread.currentThread().getName()+"\t 号车已经进来");
						TimeUnit.SECONDS.sleep(3);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}finally{
                        //释放许可
						s.release();
						System.out.println(Thread.currentThread().getName()+"\t 号车已经离开");
					}
				}
			}.start();
		}
		
	}
}
④线程间交换数据的Exchanger()

用于两个线程之间交换数据。

​ 等待多线程完成的CountDownLatch CountDownLatch,允许一个或多个线程等待其他线程完成操作。

​ CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个 CountDownLatch的引用传递到线程里即可。可以使用带指定时间的await方法——await(long time,TimeUnit unit),这个方法等待特定时间后,就会不再阻塞当前线程。

​ 同步屏障CyclicBarrier CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

​ CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数 器,并让线程重新执行一次。CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier 阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。

​ 控制并发线程数的Semaphore Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以 保证合理的使用公共资源。Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。Semaphore的用法也很简单,首先线程使用 Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证

23. 守护进程Daemon线程

​ 是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调 用Thread.setDaemon(true)将线程设置为Daemon线程。

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保哦用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕

- 例如:后台记录操作日志,监控内存,垃圾回收等

- 通过setDaemon(boolen b)设置是否为守护线程,默认为false

注:Vector是由synchronized实现安全的

CopyOnWriteArrayList是ReentrantLock实现的

24. java中的各种锁详细介绍

1. 乐观锁 VS 悲观锁

​ 乐观锁与悲观锁是一种广义上的概念。在Java和数据库中都有此概念对应的实际应用。

​ 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

​ 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

​ 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。

​ CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

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

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:

根据定义我们可以看出各属性的作用:

unsafe:获取并操作内存的数据。

valueOffset:存储value在AtomicInteger中的偏移量。

value:存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

CAS虽然很高效,但是它也存在三大问题:
  • ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  • 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  • 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
2. 自旋锁 VS 适应性自旋锁

​ 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

​ 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

​ 自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

​ 自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

​ 自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

​ 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

首先为什么Synchronized能实现线程同步?

在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

整体的锁状态升级流程如下:

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

4. 公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。

根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

5. 可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

6. 独享锁 VS 共享锁

独享锁和共享锁同样是一种概念。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

下图为ReentrantReadWriteLock的部分源码:

ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。

  • 线程安全的单例模式?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值