offer来了(原理篇)学习笔记-第3章Java并发编程

Java并发编程

相对于传统的单线程,多线程能够在操作系统多核配置的基础上,能够更好地利用服务器的多个CPU资源,使程序运行起来更加高效。Java通过提供对多线程的支持来在一个进程内并发执行多个线程,每个线程都并行执行不同的任务,以满足编写高效率程序的要求。

Java线程的创建方法

常见的Java线程的4种创建方式分别为:1.继承Thread类2.实现Runnable接口3.Callable实现有返回值的线程和基于4.线程池ExecutorService

  1. 继承Thread类: Thread类实现了Runnable接口并定义了操作线程的一些方法,我们可以通过继承Thread类的方式创建一个线程。具体实现为创建一个类并继承Thread接口,然后实例化线程对象并调用start方法启动线程。start方法是一个native方法,通过在操作系统上启动一个新线程,并最终执行run方法来启动一个线程。run方法内的代码是线程类的具体实现逻辑。具体的实现代码如下,定义了一个名为NewThread的线程类,该类继承了Thread, run方法内的代码为线程的具体执行逻辑,在使用该线程时只需新建一个该线程的对象并调用其start方法即可。:
//step 1:通过继承Thread类创建NewThread线程
public  class  NewThread  extends  Thread  {public  void  run()  {
    	System.out.println("create  a  thread  by  extends  Thread");}}//step 2:实例化一个NewThread线程对象
NewThread   newThread  =  new  NewThread();//step 3:调用start方法启动NewThread线程
newThread.start();
  1. 实现Runable接口:基于Java编程语言的规范,如果子类已经继承(extends)了一个类,就无法再直接继承Thread类,此时可以通过实现Runnable接口创建线程。
//step 1:通过实现Runnable接口方式创建ChildrenClassThread线程
public  class  ChildrenClassThread  extends  SuperClass  implements  Runnable  {public  void  run()  {
    	System.out.println("create  a  thread  by  implements  Runnable  ");}}//step 2:实例化一个ChildrenClassThread对象
ChildrenClassThread  childrenThread  =  new  ChildrenClassThread();//step 3:创建一个线程对象并将其传入已经实例化好的childrenThread实例
Thread  thread  =  new  Thread(childrenThread);//step 4:调用start方法启动一个线程
thread.start();
  1. Callable:有时,我们需要在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并将最终结果汇总起来,这时就要用到Callable接口。具体的实现方法为:创建一个类并实现Callable接口,在call方法中实现具体的运算逻辑并返回计算结果。具体的调用过程为:创建一个线程池、一个用于接收返回结果的Future List及Callable线程实例,使用线程池提交任务并将线程执行之后的结果保存在Future中,在线程执行结束后遍历Future List中的Future对象,在该对象上调用get方法就可以获取Callable线程任务返回的数据并汇总结果,实现代码如下:
//step 1:通过实现Callable接口创建MyCallable线程
public  class  MyCallable  implements  Callable<String>  {private  String  name;public MyCallable(String name){
	//通过构造函数为线程传递参数,以定义线程的名称
	this.name  =  name;}@Overridepublic String call() throws Exception {
    	//call方法内为线程实现逻辑
		return  name;}}//step 2:创建一个固定大小为5的线程池
  ExecutorService  pool  =  Executors.newFixedThreadPool(5);//step 3:创建多个有返回值的任务列表list
  List<Future>  list  =  new  ArrayList<Future>();for  (int  i  =  0;  i  <  5;  i++)  {//step 4:创建一个有返回值的线程实例
      Callable  c  =  new  MyCallable(i  +  "  ");//step 5:提交线程,获取Future对象并将其保存到Future List中
      Future  future  =  pool.submit(c);
      System.out.println("submit  a  callable  thread:"  +i);
      list.add(future);}//step 6:关闭线程池,等待线程执行结束
  pool.shutdown();//step 7:遍历所有线程的运行结果
  for  (Future  future  :list)  {//从Future对象上获取任务的返回值,并将结果输出到控制台
    System.out.println("get  the  result  from  callable  thread:"+ f.get().toString())}
  1. 线程池:线程是非常宝贵的计算资源,在每次需要时创建并在运行结束后销毁是非常浪费资源的。我们可以使用缓存策略并使用线程池来创建线程,具体过程为创建一个线程池并用该线程池提交线程任务,实现代码如下:
//step 1:创建大小为10的线程池
ExecutorService  threadPool  =  Executors.newFixedThreadPool(10);
//step 2:提交多个线程任务并执行
for(int  i  =0  ; i<10; i++){
	  threadPool.execute(new  Runnable()  {@Overridepublic  void  run()  {
        System.out.println(Thread.currentThread().getName()  +  "is  running");}});}

线程池的工作原理

Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。

线程复用
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。前面说过,在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,**在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。**可以将在循环方法中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。这样就简单实现了一个线程池,达到了线程复用的效果。

线程池的核心组件和核心类
Java线程池主要由以下4个核心组件组成。
线程池管理器,ThreadPool:用于创建并管理线程池。
工作线程,PoolWorker:线程池中执行具体任务的线程。
任务接口,Task:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
任务队列,taskQueue:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。

Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类。ThreadPoolExecutor是构建线程的核心方法,具体的7个参数如下:

Java线程池的工作流程
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
◎ 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
◎ 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
◎ 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
◎ 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
◎ 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
◎ 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。

线程池的拒绝策略
内置的拒绝策略有AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。

  1. AbortPolicy:直接抛出异常,阻止线程正常运行。
  2. CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务,run()。
  3. DiscardOldestPolicy:移除线程队列中最早的一个线程任务,并尝试提交当前任务。
  4. DiscardPolicy:丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。
  5. 自定义策略:以上4种拒绝策略均实现了RejectedExecutionHandler接口,若无法满足实际需要,则用户可以自己扩展RejectedExecutionHandler接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略。

5种常用的线程池

Java定义了Executor接口并在该接口中定义了execute()用于执行一个线程任务,然后通过ExecutorService实现Executor接口并执行具体的线程操作。ExecutorService接口有多个实现类可用于创建不同的线程池,包括newCachedThreadPool(可缓存的线程池)、newFixedThreadPool(固定大小线程池)、newScheduledThreadPool(可做任务调度的线程池)、newSingleThreadExecutor(单一线程的线程池)、newWorkStealingPool(足够大小的线程池,1.8新增)

1.newCachedThreadPool(可缓存的线程池)

之所以叫缓存线程池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则重新创建一个新的线程并将其添加到线程池中。对于执行时间很短的任务而言,newCachedThreadPool线程池能很大程度地重用线程进而提高系统的性能。

在线程池的keepAliveTime时间超过默认的60秒后,该线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool将不会占用系统的线程资源。在创建线程时需要执行申请CPU和内存、记录线程状态、控制阻塞等多项工作,复杂且耗时。因此,在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool能够很好地复用运行中的线程(任务已经完成但未关闭的线程)资源来提高系统的运行效率。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

2. newFixedThreadPool(固定大小线程池)

创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。在newFixedThreadPool线程池中,若处于活动状态的线程数量大于等于核心线程池的数量,则新提交的任务将在阻塞队列中排队,直到有可用的线程资源。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

3. newScheduledThreadPool(可做任务调度的线程池)

创建了一个可定时调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个线程任务。

ScheduledExecutorService  scheduledThreadPool=
                    Executors.newScheduledThreadPool(3);
 //1:创建一个延迟3秒执行的线程
 scheduledThreadPool.schedule(newRunnable(){@Overridepublic  void  run()  {
              System.out.println("delay  3  seconds  execu.");}},  3,  TimeUnit.SECONDS);//2:创建一个延迟1秒执行且每3秒执行一次的线程
 scheduledThreadPool.scheduleAtFixedRate(newRunnable(){@Overridepublic  void  run()  {
      System.out.println("delay  1  seconds, repeat  execute  every  3  seconds");}},1,3, TimeUnit.SECONDS);

4. newSingleThreadExecutor(单一线程的线程池)

线程池会保证永远有且只有一个可用的线程,在该线程停止或发生异常时,newSingleThreadExecutor线程池会启动一个新的线程来代替该线程继续执行任务。

ExecutorService singleThread = Executors.newSingleThreadExecutor();

5. newWorkStealingPool(足够大小的线程池,1.8新增)

创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。这里所说的有足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。

线程的生命周期

线程的生命周期分为**新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)**这5种状态。在系统运行过程中不断有新的线程被创建,旧的线程在执行完毕后被清理,线程在排队获取共享资源或者锁时将被阻塞,因此运行中的线程会在就绪、阻塞、运行状态之间来回切换。

具体流程如下
(1)调用new方法新建一个线程,这时线程处于新建状态。
(2)调用start方法启动一个线程,这时线程处于就绪状态。
(3)处于就绪状态的线程等待线程获取CPU资源,在等待其获取CPU资源后线程会执行run方法进入运行状态。
(4)正在运行的线程在调用了yield方法或失去处理器资源时,会再次进入就绪状态。
(5)正在执行的线程在执行了sleep方法、I/O阻塞、等待同步锁、等待通知、调用suspend方法等操作后,会挂起并进入阻塞状态,进入Blocked池。
(6)阻塞状态的线程由于出现sleep时间已到、I/O方法返回、获得同步锁、收到通知、调用resume方法等情况,会再次进入就绪状态,等待CPU时间片的轮询。该线程在获取CPU资源后,会再次进入运行状态。
(7)处于运行状态的线程,在调用run方法或call方法正常执行完成、调用stop方法停止线程或者程序执行错误导致异常退出时,会进入死亡状态。

  • 新建状态New:在Java中使用new关键字创建一个线程,新创建的线程将处于新建状态。在创建线程时主要是为线程分配内存并初始化其成员变量的值。
  • 就绪状态Runnable:新建的线程对象在调用start方法之后将转为就绪状态。此时JVM完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。
  • 运行状态Running:就绪状态的线程在竞争到CPU的使用权并开始执行run方法的线程执行体时,会转为运行状态,处于运行状态的线程的主要任务就是执行run方法中的逻辑代码。
  • 阻塞状态Blocked:运行中的线程会主动或被动地放弃CPU的使用权并暂停运行,此时该线程将转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到CPU使用权并转为运行状态。阻塞的状态分为以下三种。(1)等待阻塞:在运行状态的线程调用o.wait方法时,JVM会把该线程放入等待队列(Waitting Queue)中,线程转为阻塞状态。(2)同步阻塞:在运行状态的线程尝试获取正在被其他线程占用的对象同步锁时,JVM会把该线程放入锁池(Lock Pool)中,此时线程转为阻塞状态。(3)其他阻塞:运行状态的线程在执行Thread.sleep(long ms)、Thread.join()或者发出I/O请求时,JVM会把该线程转为阻塞状态。直到sleep()状态超时、Thread.join()等待线程终止或超时,或者I/O处理完毕,线程才重新转为可运行状态。
  • 线程死亡Dead:线程在以下面三种方式结束后转为死亡状态。
    ◎ 线程正常结束:run方法或call方法执行完成。
    ◎ 线程异常退出:运行中的线程抛出一个Error或未捕获的Exception,线程异常退出。
    ◎ 手动结束:调用线程对象的stop方法手动结束运行中的线程(该方式会瞬间释放线程占用的同步对象锁,导致锁混乱和死锁,不推荐使用)。

线程的基本方法

线程相关的基本方法有wait、notify、notifyAll、sleep、join、yield、interrupt等,这些方法控制线程的运行,并影响线程的状态变化。

  1. 线程等待wait方法:调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。需要注意的是,在调用wait方法后会释放对象的锁,因此wait方法一般被用于同步方法或同步代码块中。
  2. 线程睡眠sleep方法:调用sleep方法会导致当前线程休眠。与wait方法不同的是,sleep方法不会释放当前占有的锁,会导致线程进入TIMED-WATING状态,而wait方法会导致当前线程进入WATING状态。
  3. 线程让步yield方法:调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。在一般情况下,优先级高的线程更有可能竞争到CPU时间片,但这不是绝对的,有的操作系统对线程的优先级并不敏感。
  4. 线程中断interrupt方法:interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态(阻塞、终止等)。状态的具体变化需要等待接收到中断标识的程序的最终处理结果来判定。对interrupt方法的理解需要注意以下4个核心点。
    ◎ 调用interrupt方法并不会中断一个正在运行的线程,也就是说处于Running状态的线程并不会因为被中断而终止,仅仅改变了内部维护的中断标识位而已。
    ◎ 若因为调用sleep方法而使线程处于TIMED-WATING状态,则这时调用interrupt方法会抛出InterruptedException,使线程提前结束TIMED-WATING状态。
    ◎ 许多声明抛出InterruptedException的方法如Thread.sleep(long mills),在抛出异常前都会清除中断标识位,所以在抛出异常后调用isInterrupted方法将会返回false。
    中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程。比如,在想终止一个线程时,可以先调用该线程的interrupt方法,然后在线程的run方法中根据该线程isInterrupted方法的返回状态值安全终止线程。
  5. 线程加入join方法:join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU的使用权。在很多情况下,主线程生成并启动了子线程,需要等到子线程返回结果并收集和处理再退出,这时就要用到join方法。
  6. 线程唤醒notify方法:Object类有个notify方法,用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。我们通常调用其中一个对象的wait方法在对象的监视器上等待,直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他线程竞争。类似的方法还有notifyAll,用于唤醒在监视器上等待的所有线程。
  7. 后台守护线程setDaemon方法:setDaemon方法用于定义一个守护线程,也叫作“服务线程”,该线程是后台线程,有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。守护线程的优先级较低,用于为系统中的其他对象和线程提供服务。将一个用户线程设置为守护线程的方法是在线程对象创建之前用线程对象的setDaemon(true)来设置。在后台守护线程中定义的线程也是后台守护线程。后台守护线程是JVM级别的,比如垃圾回收线程就是一个经典的守护线程,在我们的程序中不再有任何线程运行时,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以在回收JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态下运行,用于实时监控和管理系统中的可回收资源。守护线程是运行在后台的一种特殊线程,独立于控制终端并且周期性地执行某种任务或等待处理某些已发生的事件。也就是说,守护线程不依赖于终端,但是依赖于JVM,与JVM“同生共死”。在JVM中的所有线程都是守护线程时,JVM就可以退出了,如果还有一个或一个以上的非守护线程,则JVM不会退出。

sleep方法与wait方法的区别
◎ sleep方法属于Thread类,wait方法则属于Object类。
◎ sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持,在指定的时间过后又会自动恢复运行状态。
◎ 在调用sleep方法的过程中,线程不会释放对象锁。
◎ 在调用wait方法时,线程会放弃对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。

start方法与run方法的区别
◎ start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕,就可以继续执行下面的代码。
◎ 在通过调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。
◎ run方法也叫作线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态,开始运行run方法中的代码。在run方法运行结束后,该线程终止,CPU再调度其他线程。

线程终止的4种方式

  1. 正常运行结束:指线程体执行完成,线程自动结束。
  2. 使用退出标志退出线程:在一般情况下,在run方法执行完毕时,线程会正常结束。然而,有些线程是后台线程,需要长时间运行,只有在系统满足某些特殊条件后,才能触发关闭这些线程。这时可以使用一个变量来控制循环,比如设置一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。如代码,定义了一个退出标志exit, exit的默认值为false。在定义exit时使用了一个Java关键字volatile,这个关键字用于使exit线程同步安全,也就是说在同一时刻只能有一个线程修改exit的值,在exit为true时,while循环退出。
public  class  ThreadSafe  extends  Thread  {public  volatile  boolean  exit  =  false;public  void  run()  {while  (! exit){//执行业务逻辑代码
      }}}
  1. 使用Interrupt方法终止线程:使用interrupt方法终止线程有以下两种情况。
    (1)线程处于阻塞状态。例如,在使用了sleep、调用锁的wait或者调用socket的receiver、accept等方法时,会使线程处于阻塞状态。在调用线程的interrupt方法时,会抛出InterruptException异常。我们通过代码捕获该异常,然后通过break跳出状态检测循环,可以有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法就会结束线程,这实际上理解有误,一定要先捕获InterruptedException异常再通过break跳出循环,才能正常结束run方法。
public  class  ThreadSafe  extends  Thread  {public  void  run()  {while (! isInterrupted()){ //在非阻塞过程中通过判断中断标志来退出
          try{
              Thread.sleep(51000); //在阻塞过程中捕获中断异常来退出
          }catch(InterruptedException  e){
              e.printStackTrace();break; //在捕获到异常后执行break跳出循环
          }}}}

(2)线程未处于阻塞状态。此时,使用isInterrupted方法判断线程的中断标志来退出循环。在调用interrupt方法时,中断标志会被设置为true,并不能立刻退出线程,而是执行线程终止前的资源释放操作,等待资源释放完毕后退出该线程。

  1. 使用stop方法终止线程不安全:在程序中可以直接调用Thread.stop方法强行终止线程,但这是很危险的,就像突然关闭计算机的电源,而不是正常关机一样,可能会产生不可预料的后果。在程序使用Thread.stop方法终止线程时,该线程的子线程会抛出ThreadDeatherror错误,并且释放子线程持有的所有锁。加锁的代码块一般被用于保护数据的一致性,如果在调用Thread.stop方法后导致该线程所持有的所有锁突然释放而使锁资源不可控制,被保护的数据就可能出现不一致的情况,其他线程在使用这些被破坏的数据时,有可能使程序运行错误。因此,并不推荐采用这种方法终止线程。

Java中的锁

Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁,该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保障数据的安全。

锁从乐观和悲观的角度可分为乐观锁和悲观锁,从获取资源的公平性角度可分为公平锁和非公平锁,从是否共享资源的角度可分为共享锁和独占锁,从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。下面将详细介绍这些锁。

  1. 乐观锁:乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作。Java中的乐观锁大部分是通过CAS(Compare And Swap,比较和交换)操作实现的,CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态。

  2. 悲观锁:悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。

  3. 自旋锁:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。
    自旋锁的优缺点:
    ◎ 优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。
    ◎ 缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。
    自旋锁的时间阈值
    JDK的不同版本所采用的自旋周期不同,JDK 1.5为固定DE时间,JDK 1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间是就一个最佳时间

  4. synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。synchronized属于独占式的悲观锁,同时属于可重入锁。在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源后才能访问该对象或执行同步代码块。Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
    synchronized的作用范围
    ◎ synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象
    ◎ synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象。
    ◎ synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象

经典的死锁代码
blockMethod1方法中,synchronized(lockA)在第一次循环执行后使用synchronized(lockB)锁住了lockB,下次执行等待lockA锁释放后才能继续;而在blockMethod2方法中,synchronized (lockB)在第一次循环执行后使用ynchronized(lockA)锁住了lockA,等待lockB释放后才能进行下一次执行。这样就出现blockMethod1等待blockMethod2释放lockA,而blockMethod2等待blockMethod1释放lockB的情况,就出现了死锁。执行结果是两个线程都挂起,等待对方释放资源。

String  lockA  =  "lockA";
String  lockB  =  "lockB";public  static  void  main(String[]  args)  {final  SynchronizedDemo  synchronizedDemo  =  new  SynchronizedDemo();new  Thread(new  Runnable()  {@Overridepublic  void  run()  {
            synchronizedDemo.blockMethod1();}}).start();new  Thread(new  Runnable()  {@Overridepublic  void  run()  {
          synchronizedDemo.blockMethod2();}}).start();


}//synchronizedyoghurt用于同步方法块,锁住的是括号里面的对象
public    void  blockMethod1()  {try  {synchronized  (lockA)  {for(int  i  =  1  ;  i<3; i++)  {
              System.out.println("Method  1  execute");
              Thread.sleep(3000);synchronized  (lockB){}}}}  catch  (InterruptedException  e)  {
      e.printStackTrace();}}//synchronizedyoghurt用于同步方法块,锁住的是括号里面的对象
public    void  blockMethod2()  {try  {synchronized  (lockB)  {for(int  i  =  1  ;  i<3; i++)  {
                System.out.println("Method  2  execute");
                Thread.sleep(3000);synchronized  (lockA){}}}}  catch  (InterruptedException  e)  {
          e.printStackTrace();}}

synchronized的实现原理(后再看)
在synchronized内部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、! Owner这6个区域,每个区域的数据都代表锁的不同状态。
◎ ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。
◎ EntryList:竞争候选列表,在Contention List中有资格成为候选者来竞争锁资源的线程被移动到了Entry List中。
◎ WaitSet:等待集合,调用wait方法后被阻塞的线程将被放在WaitSet中。
◎ OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。
◎ Owner:竞争到锁资源的线程被称为Owner状态线程。
◎ !Owner:在Owner线程释放锁后,会从Owner的状态变成!Owner。
synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中。
为了防止锁竞争时ContentionList尾部的元素被大量的并发线程进行CAS访问而影响性能,Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中,并指定EntryList中的某个线程(一般是最先进入的线程)为OnDeck线程。Owner线程并没有直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,让OnDeck线程重新竞争锁。在Java中把该行为称为“竞争切换”,该行为牺牲了公平性,但提高了性能。
获取到锁资源的OnDeck线程会变为Owner线程,而未获取到锁资源的线程仍然停留在EntryList中。
Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,直到某个时刻被notify方法或者notifyAll方法唤醒,会再次进入EntryList中。ContentionList、EntryList、WaitSet中的线程均为阻塞状态,该阻塞是由操作系统来完成的(在Linux内核下是采用pthread_mutex_lock内核函数实现的)。
Owner线程在执行完毕后会释放锁的资源并变为!Owner状态,如图所示。
在synchronized中,在线程进入ContentionList之前,等待的线程会先尝试以自旋的方式获取锁,如果获取不到就进入ContentionList,该做法对于已经进入队列的线程是不公平的,因此synchronized是非公平锁。另外,自旋获取锁的线程也可以直接抢占OnDeck线程的锁资源。
synchronized是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间。
JDK 1.6对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫作锁膨胀。在JDK 1.6中默认开启了偏向锁和轻量级锁,可以通过-XX:UseBiasedLocking禁用偏向锁。

  1. ReentrantLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器(Abstract Queued Sychronized, AQS)来实现锁的获取与释放。独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。ReentrantLock支持公平锁和非公平锁的实现。公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

ReentrantLock的用法
具体的使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁。

public  class  ReenterLockDemo  implements  Runnable{//step 1:定义一个ReentrantLock
    public  static  ReentrantLock  lock  =  new  ReentrantLock();public  static  int  i  =  0;public  void  run()  {for  (int  j  =  0; j<10; j++)  {
          lock.lock(); //step 2:加锁
          //lock.lock();可重入锁
          try  {
              i++;}finally  {
              lock.unlock(); //step 3:释放锁
              //lock.unlock();可重入锁
          }}}public  static  void  main(String[]  args)  throws  InterruptedException  {
      ReenterLockDemo  reenterLock  =  new  ReenterLockDemo();
      Thread  t1  =  new  Thread(reenterLock);
      t1.start();
      t1.join();
      System.out.println(i);}}

ReentrantLock之所以被称为可重入锁,是因为**ReentrantLock锁可以反复进入。即允许连续两次获得同一把锁,两次释放同一把锁。**将上述代码中的注释部分去掉后,程序仍然可以正常执行。注意,获取锁和释放锁的次数要相同,如果释放锁的次数多于获取锁的次数,Java就会抛出java.lang.IllegalMonitorStateException异常;如果释放锁的次数少于获取锁的次数,该线程就会一直持有该锁,其他线程将无法获取锁资源。

ReentrantLock如何避免死锁:响应中断、可轮询锁、定时锁(内容过多,后续再看)
(1)响应中断:在synchronized中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。具体的实现代码如下:

public  class  InterruptiblyLock   {public   ReentrantLock lock1 = new ReentrantLock(); //step 1:第1把锁lock1
 public   ReentrantLock lock2 = new ReentrantLock(); //step 2:第2把锁lock2
 public  Thread  lock1(){
  Thread  t  =  new  Thread(new  Runnable(){public  void  run(){try  {
            lock1.lockInterruptibly(); //step 3.1:如果当前线程未被中断,则获取锁
                try  {
                Thread.sleep(500); //step 4.1:sleep 500ms,这里执行具体的业务逻辑
                }  catch  (InterruptedException  e)  {
                    e.printStackTrace();}
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");}  catch  (InterruptedException  e)  {
            e.printStackTrace();}  finally  {//step 5.1:在业务逻辑执行结束后,检查当前线程是否持有该锁,如果持有则释放该锁
            if  (lock1.isHeldByCurrentThread())  {
                lock1.unlock();}if  (lock2.isHeldByCurrentThread())  {
                lock2.unlock();}
            System.out.println(Thread.currentThread().getName()  +",退出。");}}});
  t.start();return  t;}


public  Thread  lock2(){
  Thread  t  =  new  Thread(new  Runnable(){public  void  run(){try  {
              lock2.lockInterruptibly(); //step 3.2:如果当前线程未被中断,则获取锁
                try  {
                 Thread.sleep(500); //step 4.2:sleep 500ms,这里执行具体业务逻辑
                }  catch  (InterruptedException  e)  {
                    e.printStackTrace();}
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");}  catch  (InterruptedException  e)  {
            e.printStackTrace();}  finally  {//step 5.2:在业务逻辑执行结束后,检查当前线程是否保持该锁,如果持有则释放该锁
             if  (lock1.isHeldByCurrentThread())  {
                lock1.unlock();}if  (lock2.isHeldByCurrentThread())  {
                lock2.unlock();}
             System.out.println(Thread.currentThread().getName()  +",退出。");}}});
  t.start();return  t;}public  static  void  main(String[]  args)  throws  InterruptedException  {long  time  =  System.currentTimeMillis();
  InterruptiblyLock  interruptiblyLock  =  new  InterruptiblyLock();
  Thread  thread1   =  interruptiblyLock.lock1();
  Thread  thread2   =   interruptiblyLock.lock2();//自旋一段时间,如果等待时间过长,则可能发生了死锁等问题,主动中断并释放锁
  while  (true){if(System.currentTimeMillis()  -  time  >=3000){
          thread2.interrupt(); //中断线程1
      }}}}

在以上代码中,在线程thread1和thread2启动后,thread1先占用lock1,再占用lock2; thread2则先占用lock2,后占用lock1,这便形成了thread1和thread2之间的相互等待,在两个线程都启动时便处于死锁状态。在while循环中,如果等待时间过长,则这里可设定为3s,如果可能发生了死锁等问题,thread2就会主动中断(interrupt),释放对lock1的申请,同时释放已获得的lock2,让thread1顺利获得lock2,继续执行下去。输出结果如下:

java.lang.InterruptedException
……
Thread-1,退出。
Thread-0,执行完毕!
Thread-0,退出。

(2)可轮询锁:通过boolean tryLock()获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false。
(3)定时锁:通过boolean tryLock(long time, TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。
◎ 当前线程获取到了可用锁并返回true。
◎ 当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态。
◎ 当前线程获取锁的时间超过了指定的等待时间,则将返回false。如果设定的时间小于等于0,则该方法将完全不等待。

Lock接口的主要方法
◎ void lock():给对象加锁,如果锁未被其他线程使用,则当前线程将获取该锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当前线程获取锁。
◎ boolean tryLock():试图给对象加锁,如果锁未被其他线程使用,则将获取该锁并返回true,否则返回false。tryLock()和lock()的区别在于tryLock()只是“试图”获取锁,如果没有可用锁,就会立即返回。lock()在锁不可用时会一直等待,直到获取到可用锁。
◎ tryLock(long timeout TimeUnit unit):创建定时锁,如果在给定的等待时间内有可用锁,则获取该锁。
◎ void unlock():释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁却执行该方法,则抛出异常。
◎ Condition newCondition():创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的await(),在调用后当前线程将释放锁。
◎ getHoldCount():查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。
◎ getQueueLength():返回等待获取此锁的线程估计数,比如启动5个线程,1个线程获得锁,此时返回4。
◎ getWaitQueueLength(Condition condition):返回在Condition条件下等待该锁的线程数量。比如有5个线程用同一个condition对象,并且这5个线程都执行了condition对象的await方法,那么执行此方法将返回5。
◎ hasWaiters(Condition condition):查询是否有线程正在等待与给定条件有关的锁,即对于指定的contidion对象,有多少线程执行了condition.await方法。
◎ hasQueuedThread(Thread thread):查询给定的线程是否等待获取该锁。
◎ hasQueuedThreads():查询是否有线程等待该锁。
◎ isFair():查询该锁是否为公平锁。
◎ isHeldByCurrentThread():查询当前线程是否持有该锁,线程执行lock方法的前后状态分别是false和true。
◎ isLock():判断此锁是否被线程占用。
◎ lockInterruptibly():如果当前线程未被中断,则获取该锁。

公平锁与非公平锁
ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制。
ReentrantLock通过在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但是执行效率明显高于公平锁。如果系统没有特殊的要求,一般情况下建议使用非公平锁。

tryLock、lock和lockInterruptibly的区别
tryLock、lock和lockInterruptibly的区别如下。
◎ tryLock若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout, TimeUnit unit)可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
◎ lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。
◎ 在锁中断时lockInterruptibly会抛出异常,lock不会。

synchronized和ReentrantLock的比较
synchronized和ReentrantLock的共同点如下。
◎ 都用于控制多线程对共享对象的访问。
◎ 都是可重入锁。
◎ 都保证了可见性和互斥性。

synchronized和ReentrantLock的不同点如下。
◎ ReentrantLock显式获取和释放锁;synchronized隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时必须在finally控制块中进行解锁操作。
◎ ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性。
◎ ReentrantLock是API级别的,synchronized是JVM级别的。
◎ ReentrantLock可以定义公平锁。
◎ ReentrantLock通过Condition可以绑定多个条件。
◎ 二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
◎ Lock是一个接口,而synchronized是Java中的关键字,synchronized是由内置的语言实现的。
◎ 我们通过Lock可以知道有没有成功获取锁,通过synchronized却无法做到。
◎ Lock可以通过分别定义读写锁提高多个线程读操作的效率。

  1. Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
//step 1:创建一个计数阈值为5的信号量对象,即只能有5个线程同时访问
Semaphore  semp  =  new  Semaphore(5);try  {//step 2:申请许可
    semp.acquire();try  {//step 3:执行业务逻辑
    }  catch  (Exception  e)  {}  finally  {//step 4:释放许可
        semp.release();}}  catch  (InterruptedException  e)  {}

Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphone.acquire方法默认和ReentrantLock. lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。
此外,Semaphore也实现了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的机制。对公平与非公平锁的定义在构造函数中设定。
Semaphore的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
Semaphore也可以用于实现一些对象池、资源池的构建,比如静态全局对象池、数据库连接池等。此外,我们也可以创建计数为1的Semaphore,将其作为一种互斥锁的机制(也叫二元信号量,表示两种互斥状态),同一时刻只能有一个线程获取该锁。

  1. AtomicInteger:在多线程程序中,诸如++i或i++等运算不具有原子性,因此不是安全的线程操作。我们可以通过synchronized或ReentrantLock将该操作变成一个原子操作,但是synchronized和ReentrantLock均属于重量级锁。因此JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger。AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,它们的实现原理相同,区别在于运算对象的类型不同。还可以通过AtomicReference将一个对象的所有操作都转化成原子操作。AtomicInteger的性能通常是synchronized和ReentrantLock的好几倍。
class  AtomicIntegerDemo  implements  Runnable  {//step 1:定义一个原子操作数
    static  AtomicInteger  safeCounter  =  new  AtomicInteger(0);public  void  run()  {for  (int  m  =  0;  m  <  1000000;  m++)  {
          safeCounter.getAndIncrement(); //step 2:对原子操作数执行自增操作
        }}};public  class  AtomicIntegerDemoTest  {public  static  void  main(String[]  args)  throws  InterruptedException  {
        AtomicIntegerDemo  mt  =  new  AtomicIntegerDemo();
        Thread  t1  =  new  Thread(mt);
        Thread  t2  =  new  Thread(mt);
        t1.start();
        t2.start();
        Thread.sleep(500);
        System.out.println(mt.safeCounter.get());}}

  1. 可重入锁:也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁

  2. 公平锁与非公平锁
    ◎ 公平锁(Fair Lock)指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
    ◎ 非公平锁(Nonfair Lock)指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待。
    因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁

  3. 读写锁:ReadWriteLock。在Java中通过Lock接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫作普通锁。为了提高性能,Java提供了读写锁。读写锁分为读锁和写锁两种,多个读锁不互斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的。如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取该共享数据,则需要使用写锁。
    一般做法是分别定义一个读锁和一个写锁,在读取共享数据时使用读锁,在使用完成后释放读锁,在写共享数据时使用写锁,在使用完成后释放写锁。在Java中,通过读写锁的接口java.util.concurrent.locks.ReadWriteLoc的实现类ReentrantReadWriteLock来完成对读写锁的定义和使用。

public  class  SeafCache  {private  final  Map<String,  Object>  cache  =  new  HashMap<String,  Object>();private  final  ReentrantReadWriteLock  rwlock  =  new  ReentrantReadWriteLock();private final Lock readLock = rwlock.readLock();    //step 1:定义读锁
      private final Lock writeLock = rwlock.writeLock();  //step 2:定义写锁
      //step 3:在读数据时加读锁
      public  Object  get(String  key)  {
          readLock.lock();try  {  return  cache.get(key);  }finally  {  readLock.unlock();  }}//step 4:在写数据时加写锁
      public  Object  put(String  key,  Object  value)  {
          writeLock.lock();try  {  return  cache.put(key,  value);  }finally  {  writeLock.unlock();  }}}
  1. 共享锁和独占锁
    Java并发包提供的加锁模式分为独占锁和共享锁。
    ◎ 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现
    ◎ 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock中的读锁为共享锁的实现
    ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对象通过继承AQS(Abstract Queued Synchronizer)进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。
    独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性;因为并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。

  2. 重量级锁和轻量级锁
    重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高。JDK在1.6版本以后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁。轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。

  3. 偏向锁
    除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。
    偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS(Compare and Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
    在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。
    综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。

锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级。

  1. 分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。

  2. 同步锁与死锁:在有多个线程同时被阻塞时,它们之间若相互等待对方释放锁资源,就会出现死锁。为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。

如何进行锁优化
1.减少锁持有的时间
减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。
2.减小锁粒度
减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁。
3.锁分离
锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。
操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。
4.锁粗化
锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。
5.锁消除
在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。

线程上下文切换

CPU利用时间片轮询来为每个任务都服务一定的时间,然后把当前任务的状态保存下来,继续服务下一个任务。任务的状态保存及再加载就叫作线程的上下文切换。
进程:指一个运行中的程序的实例。在一个进程内部可以有多个线程在同时运行,并与创建它的进程共享同一地址空间(一段内存区域)和其他资源。
上下文:指线程切换时CPU寄存器和程序计数器所保存的当前线程的信息。
寄存器:指CPU内部容量较小但速度很快的内存区域(与之对应的是CPU外部相对较慢的RAM主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来加快计算机程序运行的速度。
程序计数器:是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存储的值为正在执行的指令的位置或者下一个将被执行的指令的位置,这依赖于特定的系统。

上下文切换
上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。上下文切换过程中的信息被保存在进程控制块(PCB-Process Control Block)中。PCB又被称作切换桢(SwitchFrame)。上下文切换的信息会一直被保存在CPU的内存中,直到被再次使用。上下文的切换流程如下。
(1)挂起一个进程,将这个进程在CPU中的状态(上下文信息)存储于内存的PCB中。
(2)在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行)并恢复该进程。
时间片轮转方式使多个任务在同一CPU上的执行有了可能。

引起线程上下文切换的原因
◎ 当前正在执行的任务完成,系统的CPU正常调度下一个任务。
◎ 当前正在执行的任务遇到I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务。
◎ 多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
◎ 用户的代码挂起当前任务,比如线程执行sleep方法,让出CPU。
◎ 硬件中断。

Java阻塞队列

队列是一种只允许在表的前端进行删除操作,而在表的后端进行插入操作的线性表。阻塞队列和一般队列的不同之处在于阻塞队列是“阻塞”的,这里的阻塞指的是操作队列的线程的一种状态。在阻塞队列中,线程阻塞有如下两种情况。
消费者阻塞:在队列为空时,消费者端的线程都会被自动阻塞(挂起),直到有数据放入队列,消费者线程会被自动唤醒并消费数据。
生产者阻塞:在队列已满且没有可用空间时,生产者端的线程都会被自动阻塞(挂起),直到队列中有空的位置腾出,线程会被自动唤醒并生产数据。

阻塞队列的主要操作
阻塞队列的主要操作有插入操作和移除操作。插入操作有add(e)、offer(e)、put(e)、offer(e, time, unit),移除操作有remove()、poll()、take()、poll(time, unit)。

(后再看)
插入操作
(1)public abstract boolean add(E paramE):将指定的元素插入队列中,在成功时返回true,如果当前没有可用的空间,则抛出IllegalStateException。如果该元素是null,则抛出NullPointerException异常。
(2)public abstract boolean offer(E paramE):将指定的元素插入队列中,在成功时返回true,如果当前没有可用的空间,则返回false。
(3)offer(E o, long timeout, TimeUnit unit):将指定的元素插入队列中,可以设定等待的时间,如果在设定的等待时间内仍不能向队列中加入元素,则返回false。
(4)public abstract void put(E paramE) throws InterruptedException:将指定的元素插入队列中,如果队列已经满了,则阻塞、等待可用的队列空间的释放,直到有可用的队列空间释放且插入成功为止。

移除/获取数据
(1)poll():取走队列队首的对象,如果取不到数据,则返回null。
(2)poll(long timeout, TimeUnit unit):取走队列队首的对象,如果在指定的时间内队列有数据可取,则返回队列中的数据,否则等待一定时间,在等待超时并且没有数据可取时,返回null。
(3)take():取走队列队首的对象,如果队列为空,则进入阻塞状态等待,直到队列有新的数据被加入,再及时取出新加入的数据。
(4)drainTo(Collection collection):一次性从队列中批量获取所有可用的数据对象,同时可以指定获取数据的个数,通过该方法可以提升获取数据的效率,避免多次频繁操作引起的队列锁定。

Java中的阻塞队列实现
Java中的阻塞队列有:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue、LinkedBlockingDeque

  1. ArrayBlockingQueue
    ArrayBlockingQueue是基于数组实现的有界阻塞队列。ArrayBlockingQueue队列按照先进先出原则对元素进行排序,在默认情况下不保证元素操作的公平性。队列操作的公平性指在生产者线程或消费者线程发生阻塞后再次被唤醒时,按照阻塞的先后顺序操作队列,即先阻塞的生产者线程优先向队列中插入元素,先阻塞的消费者线程优先从队列中获取元素。因为保证公平性会降低吞吐量,所以如果要处理的数据没有先后顺序,则对其可以使用非公平处理的方式。我们可以通过以下代码创建一个公平或者非公平的阻塞队列。
//大小为1000的公平队列
final   ArrayBlockingQueue  fairQueue  =  new   ArrayBlockingQueue(1000, true);//大小为1000的非公平队列
final   ArrayBlockingQueue  fairQueue  =  new   ArrayBlockingQueue(1000, false);
  1. LinkedBlockingQueue
    LinkedBlockingQueue是基于链表实现的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出原则对元素进行排序;LinkedBlockingQueue对生产者端和消费者端分别采用了两个独立的锁来控制数据同步,我们可以将队列头部的锁理解为写锁,将队列尾部的锁理解为读锁,因此生产者和消费者可以基于各自独立的锁并行地操作队列中的数据,队列的并发性能较高。具体用法如下
final LinkedBlockingQueue linkqueue = new LinkedBlockingQueue(100);
  1. PriorityBlockingQueue
    PriorityBlockingQueue是一个支持优先级的无界队列。元素在默认情况下采用自然顺序升序排列。可以自定义实现compareTo方法来指定元素进行排序规则,或者在初始化PriorityBlockingQueue时指定构造参数Comparator来实现对元素的排序。注意:如果两个元素的优先级相同,则不能保证该元素的存储和访问顺序。具体用法如下:
public  class  Data  implements  Comparable<Data>{private  String  id;private Integer number; //排序字段number
    public  Integer  getNumber()  {return  number;}public  void  setNumber(Integer  number)  {this.number  =  number;}@Overridepublic int compareTo(Data o) {
      //自定义排序规则:将number自动作为排序字段
      return  this.number.compareTo(o.getNumber());}}//定义可排序的阻塞队列,根据data的number属性大小由小到大排序
 final  PriorityBlockingQueue<Data>  priorityQueue  =  new
PriorityBlockingQueue<Data>();
  1. DelayQueue
    DelayQueue是一个支持延时获取元素的无界阻塞队列,在队列底层使用PriorityQueue实现。DelayQueue队列中的元素必须实现Delayed接口,该接口定义了在创建元素时该元素的延迟时间,在内部通过为每个元素的操作加锁来保障数据的一致性。只有在延迟时间到后才能从队列中提取元素。我们可以将DelayQueue运用于以下场景中。
    缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素,则表示缓存的有效期到了。
    定时任务调度:使用DelayQueue保存即将执行的任务和执行时间,一旦从DelayQueue中获取元素,就表示任务开始执行,Java中的TimerQueue就是使用DelayQueue实现的。
    在具体使用时,延迟对象必须先实现Delayed类并实现其getDelay方法和compareTo方法,才可以在延迟队列中使用:
public  class  DelayData  implements  Delayed  {private Integer number; //延迟对象的排序字段
    private long delayTime = 50000; //设置队列5s延迟获取
    public  Integer  getNumber()  {return  number;}public  void  setNumber(Integer  number)  {this.number  =  number;}@Overridepublic  long  getDelay(TimeUnit  unit)  {return  this.delayTime;}@Overridepublic  int  compareTo(Delayed  o)  {
      DelayData  compare  =  (DelayData)  o;return  this.number.compareTo(compare.getNumber());}public  static  void  main(String[]  args)  {//创建延时队列
      DelayQueue<DelayData>  queue  =  new  DelayQueue<DelayData>();//实时添加数据
      queue.add(new  DelayData());while  (true){try  {//延迟5s后才能获取数据
              DelayData  data  =   queue.take();}catch  (Exception  e){}}}
  1. SynchronousQueue
    SynchronousQueue是一个不存储元素的阻塞队列。SynchronousQueue中的每个put操作都必须等待一个take操作完成,否则不能继续添加元素。我们可以将SynchronousQueue看作一个“快递员”,它负责把生产者线程的数据直接传递给消费者线程,非常适用于传递型场景,比如将在一个线程中使用的数据传递给另一个线程使用。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。具体的使用方法如下:
public  class  SynchronousQueueDemo  {public  static  void  main(String[]  args)  throws  InterruptedException  {
      SynchronousQueue<Integer>  queue  =  new  SynchronousQueue<Integer>();new  Producter(queue).start();new  Customer(queue).start();}static class Producter extends Thread{//生产者线程
      SynchronousQueue<Integer>  queue;public  Producter(SynchronousQueue<Integer>  queue){this.queue  =  queue;}@Overridepublic  void  run(){while(true){try  {int  product  =  new  Random().nextInt(1000);//生产一个随机数作为数据放入队列
                  queue.put(product);
                  System.out.println("product  a  data:"+product);}  catch  (InterruptedException  e)  {
                  e.printStackTrace();}
              System.out.println(queue.isEmpty());}}}static class Customer extends Thread{//消费者线程
      SynchronousQueue<Integer>  queue;public  Customer(SynchronousQueue<Integer>  queue){this.queue  =  queue;}@Overridepublic  void  run(){while(true){try  {int  data  =  queue.take();
                System.out.println("customer  a  data:"+data);}  catch  (InterruptedException  e)  {
                e.printStackTrace();}}}}}
  1. LinkedTransferQueue
    LinkedTransferQueue是基于链表结构实现的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了transfer、tryTransfer和tryTransfer(E e, long timeout, TimeUnit unit)方法。
    ◎ transfer方法:如果当前有消费者正在等待接收元素,transfer方法就会直接把生产者传入的元素投递给消费者并返回true。如果没有消费者在等待接收元素,transfer方法就会将元素存放在队列的尾部(tail)节点,直到该元素被消费后才返回。
    ◎ tryTransfer方法:首先尝试能否将生产者传入的元素直接传给消费者,如果没有消费者等待接收元素,则返回false。和transfer方法的区别是,无论消费者是否接收元素,tryTransfer方法都立即返回,而transfer方法必须等到元素被消费后才返回。
    ◎ tryTransfer(E e, long timeout, TimeUnit unit)方法:首先尝试把生产者传入的元素直接传给消费者,如果没有消费者,则等待指定的时间,在超时后如果元素还没有被消费,则返回false,否则返回true。

  2. LinkedBlockingDeque
    LinkedBlockingDeque是基于链表结构实现的双向阻塞队列,可以在队列的两端分别执行插入和移出元素操作。这样,在多线程同时操作队列时,可以减少一半的锁资源竞争,提高队列的操作效率。
    LinkedBlockingDeque相比其他阻塞队列,多了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法。以First结尾的方法表示在队列头部执行插入(add)、获取(peek)、移除(offer)操作;以Last结尾的方法表示在队列的尾部执行插入、获取、移除操作。
    在初始化LinkedBlockingDeque时,可以设置队列的大小以防止内存溢出,双向阻塞队列也常被用于工作窃取模式。

Java并发关键字(后再看)

  • CountDownLatch
    CountDownLatch类位于java.util.concurrent包下,是一个同步工具类,允许一个或多个线程一直等待其他线程的操作执行完后再执行相关操作。
    CountDownLatch基于线程计数器来实现并发访问控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。其使用过程为:在主线程中定义CountDownLatch,并将线程计数器的初始值设置为子线程的个数,多个子线程并发执行,每个子线程在执行完毕后都会调用countDown函数将计数器的值减1,直到线程计数器为0,表示所有的子线程任务都已执行完毕,此时在CountDownLatch上等待的主线程将被唤醒并继续执行。
    我们利用CountDownLatch可以实现类似计数器的功能。比如有一个主任务,它要等待其他两个任务都执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能。具体实现如下:
//step 1:定义大小为2的CountDownLatch
final  CountDownLatch  latch  =  new  CountDownLatch(2);new  Thread(){public  void  run()  {try  {
          System.out.println("子线程1"+"正在执行");
          Thread.sleep(3000);
          System.out.println("子线程1"+"执行完毕");
            latch.countDown(); //step 2.1:在子线程1执行完毕后调用countDown方法
        }catch  (Exception  e){}  }}.start();new  Thread(){  public  void  run()  {try  {
                System.out.println("子线程2" +"正在执行");
                Thread.sleep(3000);
                System.out.println("子线程2"+"执行完毕");
                latch.countDown(); //step 2.2:在子线程2执行完毕后调用countDown方法
            }catch  (Exception  e){}  }}.start();try  {
            System.out.println("等待两个子线程执行完毕...");
            latch.await(); //step 3:在CountDownLatch上等待子线程执行完毕
           //step 4:子线程执行完毕,开始执行主线程
            System.out.println("两个子线程已经执行完毕,继续执行主线程");}catch  (Exception  e){
            e.printStackTrace();}

以上代码片段先定义了一个大小为2的CountDownLatch,然后定义了两个子线程并启动该子线程,子线程执行完业务代码后在执行latch.countDown()时减少一个信号量,表示自己已经执行完成。主线程调用latch.await()阻塞等待,在所有线程都执行完成并调用了countDown函数时,表示所有线程均执行完成,这时程序会主动唤醒主线程并开始执行主线程的业务逻辑。

  • CyclicBarrier
    CyclicBarrier(循环屏障)是一个同步工具,可以实现让一组线程等待至某个状态之后再全部同时执行。在所有等待线程都被释放之后,CyclicBarrier可以被重用。CyclicBarrier的运行状态叫作Barrier状态,在调用await方法后,线程就处于Barrier状态。
    CyclicBarrier中最重要的方法是await方法,它有两种实现。
    ◎ public int await():挂起当前线程直到所有线程都为Barrier状态再同时执行后续的任务。
    ◎ public int await(long timeout, TimeUnit unit):设置一个超时时间,在超时时间过后,如果还有线程未达到Barrier状态,则不再等待,让达到Barrier状态的线程继续执行后续的任务。
    CyclicBarrier的具体使用方法如下:
public  static  void  main(String[]  args)  {int  N  =  4;//step 1:定义CyclicBarrier
      CyclicBarrier  barrier   =  new  CyclicBarrier(N);for(int  i=0; i<N; i++)new  BusinessThread  (barrier).start();}//step 2:定义业务线程
      static  class   BusinessThread  extends  Thread{private  CyclicBarrier  cyclicBarrier;//通过构造函数向线程传入cyclicBarrier
      public  BusinessThread  (CyclicBarrier  cyclicBarrier)  {this.cyclicBarrier  =  cyclicBarrier;}@Overridepublic  void  run()  {try  {//step 3:执行业务线程逻辑,这里sleep 5s
              Thread.sleep(5000);
    System.out.println("线程执行前准备工作完成,等待其他线程准备工作完成");//step 3:业务线程执行完成,等待其他线程也成为Barrier状态
            cyclicBarrier.await();}  catch  (InterruptedException  e)  {
              e.printStackTrace();}catch(BrokenBarrierException  e){
              e.printStackTrace();}//step 4:所有线程已经成为Barrier状态,开始执行下一项任务
System.out.println("所有线程准备工作均完成,执行下一项任务");//这里写需要并发执行的下一阶段的工作代码
      }}

以上代码先定义了一个CyclicBarrier,然后循环启动了多个线程,每个线程都通过构造函数将CyclicBarrier传入线程中,在线程内部开始执行第1阶段的工作,比如查询数据等;等第1阶段的工作处理完成后,再调用cyclicBarrier.await方法等待其他线程也完成第1阶段的工作(CyclicBarrier让一组线程等待到达某个状态再一起执行);等其他线程也执行完第1阶段的工作,便可执行并发操作的下一项任务,比如数据分发等。

  • Semaphore
    Semaphore指信号量,用于控制同时访问某些资源的线程个数,具体做法为通过调用acquire()获取一个许可,如果没有许可,则等待,在许可使用完毕后通过release()释放该许可,以便其他线程使用。
    Semaphore常被用于多个线程需要共享有限资源的情况,比如办公室有两台打印机,但是有5个员工需要使用,一台打印机同时只能被一个员工使用,其他员工排队等候,且只有该打印机被使用完毕并释放后其他员工方可使用,这时就可以通过Semaphore来实现:

int printNnmber = 5; //step 1:设置线程数,即员工数量
Semaphore semaphore = new Semaphore(2); //step 2:设置并发数,即打印机数量
for(int  i=0; i<  printNnmber; i++)new  Worker(i, semaphore).start();}static  class  Worker  extends  Thread{private  int  num;private  Semaphore  semaphore;public  Worker(int  num, Semaphore  semaphore){this.num  =  num;this.semaphore  =  semaphore;}@Overridepublic  void  run()  {try  {
	      semaphore.acquire(); //step 3:线程申请资源,即员工申请打印机
	      System.out.println("员工"+this.num+"占用一个打印机...");
	      Thread.sleep(2000);
	      System.out.println("员工"+this.num+"打印完成,释放出打印机");
	      semaphore.release(); //step 4:线程释放资源,即员工在使用完成后释放打印机
	    }  catch  (InterruptedException  e)  {
	      e.printStackTrace();}}

在以上代码中首先定义了一个数量为2的Semaphore,然后定义了一个工作线程Worker并通过构造函数将Semaphore传入线程内部。在线程调用semaphore.acquire()时开始申请许可并执行业务逻辑,在线程业务逻辑执行完成后调用semaphore.release()释放许可以便其他线程使用。
在Semaphore类中有以下几个比较重要的方法。
◎ public void acquire():以阻塞的方式获取一个许可,在有可用许可时返回该许可,在没有可用许可时阻塞等待,直到获得许可。
◎ public void acquire(int permits):同时获取permits个许可。
◎ public void release():释放某个许可。
◎ public void release(int permits):释放permits个许可。
◎ public boolean tryAcquire():以非阻塞方式获取一个许可,在有可用许可时获取该许可并返回true,否则返回false,不会等待。
◎ public boolean tryAcquire(long timeout, TimeUnit unit):如果在指定的时间内获取到可用许可,则返回true,否则返回false。
◎ public boolean tryAcquire(int permits):如果成功获取permits个许可,则返回true,否则立即返回false。
◎ public boolean tryAcquire(int permits, long timeout, TimeUnit unit):如果在指定的时间内成功获取permits个许可,则返回true,否则返回false。
◎ availablePermits():查询可用的许可数量。
CountDownLatch、CyclicBarrier、Semaphore的区别如下。
◎ CountDownLatch和CyclicBarrier都用于实现多线程之间的相互等待,但二者的关注点不同。CountDownLatch主要用于主线程等待其他子线程任务均执行完毕后再执行接下来的业务逻辑单元,而CyclicBarrier主要用于一组线程互相等待大家都达到某个状态后,再同时执行接下来的业务逻辑单元。此外,CountDownLatch是不可以重用的,而CyclicBarrier是可以重用的。
◎ Semaphore和Java中的锁功能类似,主要用于控制资源的并发访问。

  • volatile关键字的作用
    Java除了使用了synchronized保证变量的同步,还使用了稍弱的同步机制,即volatile变量。volatile也用于确保将变量的更新操作通知到其他线程。
    volatile变量具备两种特性:一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
    因为在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。volatile主要适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取的操作。
    在有多个线程对普通变量进行读写时,每个线程都首先需要将数据从内存中复制变量到CPU缓存中,如果计算机有多个CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都需要将同一个数据复制到不同的CPU Cache中,这样在每个线程都针对同一个变量的数据做了不同的处理后就可能存在数据不一致的情况。如果将变量声明为volatile, JVM就能保证每次读取变量时都直接从内存中读取,跳过CPU Cache这一步,有效解决了多线程数据同步的问题。具体的流程如图3-11所示。

需要说明的是,volatile关键字可以严格保障变量的单次读、写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次操作。volatile在某些场景下可以代替synchronized,但是volatile不能完全取代synchronized的位置,只有在一些特殊场景下才适合使用volatile。比如,必须同时满足下面两个条件才能保证并发环境的线程安全。
◎ 对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag = true)。
◎ 该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile。
volatile关键字的使用方法比较简单,直接在定义变量时加上volatile关键字即可:volatile boolean flag = false;

多线程如何共享数据

在Java中进行多线程通信主要是通过共享内存实现的,共享内存主要有三个关注点:可见性、有序性、原子性。**Java内存模型(JVM)解决了可见性和有序性的问题,而锁解决了原子性的问题。**在理想情况下,我们希望做到同步和互斥来实现数据在多线程环境下的一致性和安全性。常用的实现多线程数据共享的方式有将数据抽象成一个类,并将对这个数据的操作封装在类的方法中;将Runnable对象作为一个类的内部类,将共享数据作为这个类的成员变量。

1. 将数据抽象成一个类,并将对这个数据的操作封装在类的方法中
这种方式只需要在方法上加synchronized关键字即可做到数据的同步,具体的代码实现如下:

public  class  MyData  {//step 1:将数据抽象成MyData类,并将数据的操作(add、dec方法)作为类的方法
    private  int  j=0;public   synchronized  void  add(){
        j++;
        System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);}public   synchronized  void  dec(){
        j--;
        System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);}public  int  getData(){return  j;}}public  class  AddRunnable  implements  Runnable{
    MyData  data;//step 2:线程使用该类的对象并调用类的方法对数据进行操作
    public  AddRunnable(MyData  data){this.data=  data;}public  void  run()  {
          data.add();}}public  class  DecRunnable  implements  Runnable  {
    MyData  data;public  DecRunnable(MyData  data){this.data  =  data;}public  void  run()  {
          data.dec();}}public  static  void  main(String[]  args)  {
        MyData  data  =  new  MyData();
        Runnable  add  =  new  AddRunnable(data);
        Runnable  dec  =  new  DecRunnable(data);for(int  i=0; i<2; i++){new  Thread(add).start();new  Thread(dec).start();}

在以上代码中首先定义了一个MyData类,并在其中定义了变量j和对该变量的操作方法。注意,在这里对数据j操作的方法需要使用synchronized修饰,以保障在多个并发线程访问对象j时执行加锁操作,以便同时只有一个线程有权利访问,可保障数据的一致性;然后定义了一个名为AddRunnable的线程,该线程通过构造函数将MyData作为参数传入线程内部,而线程内部的run函数在执行数据操作时直接调用MyData的add方法对数据进行加1操作,这样便实现了线程内数据操作的安全性。还定义了一个名为DecRunnable的线程并通过构造函数将MyData作为参数传入线程的内存中,在run函数中直接调用MyData的dec方法实现了对数据进行减1的操作。
在应用时需要注意的是,如果两个线程AddRunnable和DecRunnable需要保证数据操作的原子性和一致性,就必须在传参时使用同一个data对象入参。这样无论启动多少个线程执行对data数据的操作,都能保证数据的一致性。

2. 将Runnable对象作为一个类的内部类,将共享数据作为这个类的成员变量
前面讲了如何将数据抽象成一个类,并将对这个数据的操作封装在这个类的方法中来实现在多个线程之间共享数据。还有一种方式是将Runnable对象作为类的内部类,将共享数据作为这个类的成员变量,每个线程对共享数据的操作方法都被封装在该类的外部类中,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。具体的代码实现如下:

public  class  MyData  {private  int  j=0;public   synchronized  void  add(){
        j++;
    	System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);}public   synchronized  void  dec(){
        j--;
      	System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);}public  int  getData(){return  j;}}public  class  TestThread  {public  static  void  main(String[]  args)  {final  MyData  data  =  new  MyData();for(int  i=0; i<2; i++){new  Thread(new  Runnable(){public  void  run()  {
                  data.add();}}).start();new  Thread(new  Runnable(){public  void  run()  {
                  data.dec();}}).start();}}}

在以上代码中定义了一个MyData类,并在其中定义了变量j和对该变量的操作方法。在需要多线程操作数据时直接定义一个内部类的线程,并定义一个MyData类的成员变量,在内部类线程的run方法中直接调用成员变量封装好的数据操作方法,以实现多线程数据的共享。

ConcurrentHashMap并发

ConcurrentHashMap和HashMap的实现方式类似,不同的是它**采用分段锁的思想支持并发操作,所以是线程安全的。**下面介绍ConcurrentHashMap是如何采用分段锁的思想来实现多线程并发下的数据安全的。

减小锁粒度
减小锁粒度指通过缩小锁定对象的范围来减少锁冲突的可能性,最终提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效方法,ConcurrentHashMap并发下的安全机制就是基于该方法实现的。
ConcurrentHashMap是线程安全的Map,对于HashMap而言,最重要的方法是get和set方法,如果为了线程安全对整个HashMap加锁,则可以得到线程安全的对象,但是加锁粒度太大,意味着同时只能有一个线程操作HashMap,在效率上就会大打折扣;而ConcurrentHashMap在内部使用多个Segment,在操作数据时会给每个Segment都加锁,这样就通过减小锁粒度提高了并发度。

ConcurrentHashMap的实现
ConcurrentHashMap在内部细分为若干个小的HashMap,叫作数据段(Segment)。在默认情况下,一个ConcurrentHashMap被细分为16个数据段,对每个数据段的数据都单独进行加锁操作。Segment的个数为锁的并发度。
ConcurrentHashMap是由Segment数组和HashEntry数组组成的。Segment继承了可重入锁(ReentrantLock),它在ConcurrentHashMap里扮演锁的角色。HashEntry则用于存储键值对数据。
在每一个ConcurrentHashMap里都包含一个Segment数组,Segment的结构和HashMap类似,是数组和链表结构。在每个Segment里都包含一个HashEntry数组,每个HashEntry都是一个链表结构的数据,每个Segment都守护一个HashEntry数组里的元素,在对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
在操作ConcurrentHashMap时,如果需要在其中添加一个新的数据,则并不是将整个HashMap加锁,而是先根据HashCode查询该数据应该被存放在哪个段,然后对该段加锁并完成put操作。在多线程环境下,如果多个线程同时进行put操作,则只要加入的数据被存放在不同的段中,在线程间就可以做到并行的线程安全。

Java中的线程调度

  1. 抢占式调度
    抢占式调度指每个线程都以抢占的方式获取CPU资源并快速执行,在执行完毕后立刻释放CPU资源,具体哪些线程能抢占到CPU资源由操作系统控制,在抢占式调度模式下,每个线程对CPU资源的申请地位是相等,从概率上讲每个线程都有机会获得同样的CPU执行时间片并发执行。抢占式调度适用于多线程并发执行的情况,在这种机制下一个线程的堵塞不会导致整个进程性能下降。
  1. 协同式调度
    协同式调度指某一个线程在执行完后主动通知操作系统将CPU资源切换到另一个线程上执行。线程对CPU的持有时间由线程自身控制,线程切换更加透明,更适合多个线程交替执行某些任务的情况。
    协同式调度有一个缺点:如果其中一个线程因为外部原因(可能是磁盘I/O阻塞、网络I/O阻塞、请求数据库等待)运行阻塞,那么可能导致整个系统阻塞甚至崩溃。

Java线程调度的实现:抢占式
Java采用抢占式调度的方式实现内部的线程调度,Java会为每个线程都按照优先级高低分配不同的CPU时间片,且优先级高的线程优先执行。优先级低的线程只是获取CPU时间片的优先级被降低,但不会永久分配不到CPU时间片。Java的线程调度在保障效率的前提下尽可能保障线程调度的公平性。

线程让出CPU的情况
◎ 当前运行的线程主动放弃CPU,例如运行中的线程调用yield()放弃CPU的使用权。
◎ 当前运行的线程进入阻塞状态,例如调用文件读取I/O操作、锁等待、Socket等待。
◎ 当前线程运行结束,即运行完run()里面的任务。

进程调度算法

进程调度算法包括优先调度算法、高优先权优先调度算法和基于时间片的轮转调度算法。其中,优先调度算法分为先来先服务调度算法和短作业优先调度算法;高优先权优先调度算法分为非抢占式优先权算法、抢占式优先权调度算法和高响应比优先调度算法。基于时间片的轮转调度算法分为时间片轮转算法和多级反馈队列调度算法。

1. 优先调度算法
优先调度算法包含先来先服务调度算法和短作业(进程)优先调度算法。
1.1 先来先服务调度算法
先来先服务调度算法指每次调度时都从队列中选择一个或多个最早进入该队列的作业,为其分配资源、创建进程和放入就绪队列。调度算法在获取到可用的CPU资源时会从就绪队列中选择一个最早进入队列的进程,为其分配CPU资源并运行。该算法优先运行最早进入的任务,实现简单且相对公平。
2.2 短作业优先调度算法
短作业优先调度算法指每次调度时都从队列中选择一个或若干个预估运行时间最短的作业,为其分配资源、创建进程和放入就绪队列。调度算法在获取到可用的CPU资源时,会从就绪队列中选出一个预估运行时间最短的进程,为其分配CPU资源并运行。该算法优先运行短时间作业,以提高CPU整体的利用率和系统运行效率,某些大任务可能会出现长时间得不到调度的情况。

2. 高优先权优先调度算法
高优先权优先调度算法在定义任务的时候为每个任务都设置不同的优先权,在进行任务调度时优先权最高的任务首先被调度,这样资源的分配将更加灵活,具体包含非抢占式优先调度算法、抢占式优先调度算法和高响应比优先调度算法。
2.1 非抢占式优先调度算法
非抢占式优先调度算法在每次调度时都从队列中选择一个或多个优先权最高的作业,为其分配资源、创建进程和放入就绪队列。调度算法在获取到可用的CPU资源时会从就绪队列中选出一个优先权最高的进程,为其分配CPU资源并运行。进程在运行过程中一直持有该CPU,直到进程执行完毕或发生异常而放弃该CPU。该算法优先运行优先权高的作业,且一旦将CPU分配给某个进程,就不会主动回收CPU资源,直到任务主动放弃。
2.2 抢占式优先调度算法
抢占式优先调度算法首先把CPU资源分配给优先权最高的任务并运行,但如果在运行过程中出现比当前运行任务优先权更高的任务,调度算法就会暂停运行该任务并回收CPU资源,为其分配新的优先权更高的任务。该算法真正保障了CPU在整个运行过程中完全按照任务的优先权分配资源,这样如果临时有紧急作业,则也可以保障其第一时间被执行。
2.3 高响应比优先调度算法
高响应比优先调度算法使用了动态优先权的概念,即任务的执行时间越短,其优先权越高,任务的等待时间越长,优先权越高,这样既保障了快速、并发地执行短作业,也保障了优先权低但长时间等待的任务也有被调度的可能性。
该优先权的变化规律如下。
◎ 在作业的等待时间相同时,运行时间越短,优先权越高,在这种情况下遵循的是短作业优先原则。
◎ 在作业的运行时间相同时,等待时间越长,优先权越高,在这种情况下遵循的是先来先服务原则。
◎ 作业的优先权随作业等待时间的增加而不断提高,加大了长作业获取CPU资源的可能性。
高响应比优先调度算法在保障效率(短作业优先能在很大程度上提高CPU的使用率和系统性能)的基础上尽可能提高了调度的公平性(随着任务等待时间的增加,优先权提高,遵循了先来先到原则)。

3. 时间片的轮转调度算法
时间片的轮转调度算法将CPU资源分成不同的时间片,不同的时间片为不同的任务服务,具体包括时间片轮转法和多级反馈队列调度算法。
3.1 时间片轮转法
时间片轮转法指按照先来先服务原则从就绪队列中取出一个任务,并为该任务分配一定的CPU时间片去运行,在进程使用完CPU时间片后由一个时间计时器发出时钟中断请求,调度器在收到时钟中断请求信号后停止该进程的运行并将该进程放入就绪队列的队尾,然后从就绪队列的队首取出一个任务并为其分配CPU时间片去执行。这样,就绪队列中的任务就将轮流获取一定的CPU时间片去运行。
3.2 多级反馈队列调度算法
多级反馈队列调度算法在时间片轮询算法的基础上设置多个就绪队列,并为每个就绪队列都设置不同的优先权。队列的优先权越高,队列中的任务被分配的时间片就越大。默认第一个队列优先权最高,其他次之。
多级反馈队列调度算法的调度流程为:在系统收到新的任务后,首先将其放入第一个就绪队列的队尾,按先来先服务调度算法排队等待调度。若该进程在规定的CPU时间片内运行完成或者运行过程中出现错误,则退出进程并从系统中移除该任务;如果该进程在规定的CPU时间片内未运行完成,则将该进程转入第2队列的队尾调度执行;如果该进程在第2队列中运行一个CPU时间片后仍未完成,则将其放入第3队列,以此类推,在一个长作业从第1队列依次降到第n队列后,在第n队列中便以时间片轮转的方式运行。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。

多级反馈队列调度算法遵循以下原则。
◎ 仅在第一个队列为空时,调度器才调度第2队列中的任务。
◎ 仅在第1~(n-1)队列均为空时,调度器才会调度第n队列中的进程。
◎ 如果处理器正在为第n队列中的某个进程服务,此时有新进程进入优先权较高的队列(第1~(n-1)中的任何一个队列),则此时新进程将抢占正在运行的进程的处理器,即调度器停止正在运行的进程并将其放回第 n队列的末尾,把处理器分配给新来的高优先权进程。
多级反馈调度算法相对来说比较复杂,它充分考虑了先来先服务调度算法和时间片轮询算法的优势,使得对进程的调度更加合理。

CAS

CAS(Compare And Swap)指比较并交换。CAS算法CAS(V, E, N)包含3个参数,V表示要更新的变量,E表示预期的值,N表示新值。在且仅在V值等于 E值时,才会将V值设为 N,如果 V值和 E值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS返回当前V的真实值。

CAS的特性:乐观锁
CAS操作采用了乐观锁的思想,总是认为自己可以成功完成操作。在有多个线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然,也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

CAS自旋等待
在JDK的原子包java.util.concurrent.atomic里面提供了一组原子类,这些原子类的基本特性就是在多线程环境下,在有多个线程同时执行这些类的实例包含的方法时,会有排他性。其内部便是基于CAS算法实现的,即在某个线程进入方法中执行其中的指令时,不会被其他线程打断;而别的线程就像自旋锁一样,一直等到该方法执行完成才由JVM从等待的队列中选择另一个线程进入。
相对于synchronized阻塞算法,CAS是非阻塞算法的一种常见实现。由于CPU的切换比CPU指令集的操作更加耗时,所以CAS的自旋操作在性能上有了很大的提升。JDK具体的实现源码如下:

public  class  AtomicInteger  extends  Number  implements  java.io.Serializable  {private  volatile  int  value;public  final  int  get()  {return  value;}public  final  int  getAndIncrement()  {for (; ; ) {  //CAS自旋,一直尝试,直到成功
          int  current  =  get();int  next  =  current  +  1;if  (compareAndSet(current,  next))return  current;}}public  final  boolean  compareAndSet(int  expect,  int  update)  {return  unsafe.compareAndSwapInt(this,  valueOffset,  expect,  update);}}

在以上代码中,getAndIncrement采用了CAS操作,每次都从内存中读取数据然后将此数据和加1后的结果进行CAS操作,如果成功,则返回结果,否则重试直到成功为止。

CAS的ABA问题
对CAS算法的实现有一个重要的前提:需要取出内存中某时刻的数据,然后在下一时刻进行比较、替换,在这个时间差内可能数据已经发生了变化,导致产生ABA问题。

ABA问题指第1个线程从内存的V位置取出A,这时第2个线程也从内存中取出A,并将V位置的数据首先修改为B,接着又将V位置的数据修改为A,这时第1个线程在进行CAS操作时会发现在内存中仍然是A,然后第1个线程操作成功。尽管从第1个线程的角度来说,CAS操作是成功的,但在该过程中其实V位置的数据发生了变化,只是第1个线程没有感知到罢了,这在某些应用场景下可能出现过程数据不一致的问题。

部分乐观锁是通过版本号(version)来解决ABA问题的,具体的操作是乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。AtomicStampedReference

AQS

AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。

AQS的原理
AQS为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程放入线程等待队列,等待下一次资源调度,具体的流程如图3-14所示。许多同步类的实现都依赖于AQS,例如常用的ReentrantLock、Semaphore和CountDownLatch。

state:状态
Abstract Queued Synchronizer维护了一个volatile int类型的变量,用于表示当前的同步状态。Volatile虽然不能保证操作的原子性,但是能保证当前变量state的可见性。
state的访问方式有三种:getState()、setState()和compareAndSetState(),均是原子操作,其中,compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()。具体的JDK代码实现如下:

//返回共享资源状态,此操作的内存语义为volatile修饰的原子读操作
protected  final  int  getState()  {return  state;}//设置共享资源状态,此操作的内存语义为volatile修饰的原子写操作
protected  final  void  setState(int  newState)  {
    state  =  newState;}//自动将同步状态设置为给定的更新状态值(如果当前状态值等于预期值),//此操作的内存语义为volatile修饰的原子读写操作
protected  final  boolean  compareAndSetState(int  expect,  int  update)  {return  unsafe.compareAndSwapInt(this,  stateOffset,  expect,  update);}

AQS共享资源的方式:独占式和共享式
AQS定义了两种资源共享方式:独占式(Exclusive)和共享式(Share)。
◎ 独占式:只有一个线程能执行,具体的Java实现有ReentrantLock。
◎ 共享式:多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch。
AQS只是一个框架,只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器再做处理。自定义同步器的主要方法如表所示。

同步器的实现是AQS的核心内存。ReentrantLock对AQS的独占方式实现为:ReentrantLock中的state初始值为0时表示无锁状态。在线程执行tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire()获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1,因此ReentrantLock也属于可重入锁。但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。

CountDownLatch对AQS的共享方式实现为:CountDownLatch将任务分为N个子线程去执行,将state也初始化为N, N与线程的个数一致,N个子线程是并行执行的,每个子线程都在执行完成后countDown()一次,state会执行CAS操作并减1。在所有子线程都执行完成(state=0)时会unpark()主线程,然后主线程会从await()返回,继续执行后续的动作。

一般来说,自定义同步器要么采用独占方式,要么采用共享方式,实现类只需实现tryAcquire、tryRelease或tryAcquireShared、tryReleaseShared中的一组即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,例如ReentrantReadWriteLock在读取时采用了共享方式,在写入时采用了独占方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值