Java并发总结

目录

 

java并发

Java内存模型

Java并发基础

1.sychronized

2. volatile

3.线程的状态

4.wait、sleep、notify、notifyall、join、yeild

5.ThreadLocal

6.interrupt()和线程终止方式

7.线程优先级和守护线程

JUC集合

1.ConcurrentHashMap

JUC原子类

JUC锁

1.CAS

2.AQS

3.锁的原理(共享锁和互斥锁)

4.Condition接口

JUC并发工具类

1.CyclicBarrier

2.CountDownLanch

3.Semaphore

JUC线程池

 

其他问题


java并发

Java内存模型

(1)可见性、有序性、原子性指的是?

  • 原子性
    • 提供了互斥访问,同一时刻只能有一个线程对它进行操作。
    • 可以通过Atomic包、CAS算法、synchronized、Lock实现。
      • syncronized:不可中断锁,适合竞争不激烈场景,可读性好。
      • Lock:可中断锁,多样化同步,竞争激烈时能维持常态,需要大量代码实现。
      • Atomic:竞争激烈时能维持常态,比Lock性能好;缺点是一次只能同步一个值,虽然提供了AtomicReference、AtomicReferenceFieldUpdater也只是一次同步一个对象。
  • 可见性
    • 一个线程对主内存的修改可以及时的被其他线程观察到。
    • 导致共享变量在线程间不可见的原因:
      • 线程交叉执行。
      • 代码重排序结合线程交叉执行。
      • 共享变量更新后的值没有在工作内存与主内存之间及时更新。
    • 可见性实现方式:synchronized、volatile。
      • syncronized,JMM关于syncronized的两条规定:
        • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
        • 线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)。
        • 由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用。
      • volatile:通过加入内存屏障和禁止重排序优化来实现可见性。
        • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
        • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
        • 注:volatile不能保证操作的原子性,也就是不能保证线程安全性。volatile修饰的变量适合作为状态标记量。
  • 有序性
    • Java内存模型中,允许编译器和处理器对指令进行重排序,但重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    • 实现:happens-before原则(volatile、syncronized、Lock都可保证有序性)。
    • 一系列操作如果无法保证happens-before原则,就说这段操作无法保证有序性。
      • happens-before原则
        • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
        • 锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作。
        • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
        • 传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C。
        • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
        • 线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生)。
        • 线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行。
        • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

(2)sychronized的底层实现原理

  • 修饰同步代码块:通过两条指令实现的。
    • monitorenter 。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
      • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
      • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
      • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
    • monitorexit
      • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
      • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 
  • 修饰方法
    • 方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
    • JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

(3)内存模型

 

Java并发基础

1.sychronized

(1)sychronized在方法体内和修饰方法锁的是什么?有什么区别?

sychronized修饰方法的时候可以修饰静态方法和非静态方法,修饰非静态方法的时候锁的是对象实例,修饰静态方法的时候锁的是类;sychronized在方法体内修饰的即为同步代码块,使用的时候需要在后面括号中传一个参数,该参数可以是this(对象实例)、.class(某个类)等等,即锁的对象。

(2)sychronized是一个重量级锁

(3)sychronized原则

第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块
第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

2. volatile

(1)什么是volatile

  • 首先,jvm将内存组织为主内存和工作内存两个部分。
    • 主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两部分,一个是属于该线程私有的栈,另一个是对主存部分变量拷贝的寄存器(包括程序计数器PC和cpu工作的高速缓存区)。
    • 所有的变量都存储在主内存中,对于所有线程都是共享的。
    • 每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
    • 线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
  • volatile是java提供的一种同步手段,只不过它是轻量级的同步,volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的。

3.线程的状态

(1)等待状态和阻塞状态的区别?

  • 阻塞:当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。
  • 等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。例如调用:Object.wait()、Thread.join()以及等待Lock或Condition。

(2)线程的状态

  • 新建(NEW):新创建了一个线程对象。
  • 就绪/可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
  • 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
  • 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种: 
    • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  • 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

4.wait、sleep、notify、notifyall、join、yeild

(1)几种方法的比较

  • Thread.sleep(long millis)
    • 一定是当前线程调用此方法,当前线程进入阻塞,仅释放CPU使用权,锁仍然占用,millis后线程自动苏醒进入可运行状态。
    • 作用:给其它线程执行机会的最佳方式。
  • Thread.yield()
    • 一定是当前线程调用此方法,当前线程仅释放CPU执行权,锁仍然占用,由运行状态变会可运行状态,让OS再次选择线程。
    • 作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
  • t.join()/t.join(long millis)
    • 当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。
  • obj.wait()
    • 当前线程调用对象的wait()方法,当前线程会释放CPU执行权和占有的锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
  • obj.notify()
    • 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

(2)wait、notify、notifyall?

  • wait和notify必须配套使用,即必须使用同一把锁调用;
  • wait和notify必须放在一个同步块中;
  • 调用wait和notify的对象必须是他们所处同步块的锁对象。
  • notify随机唤醒一个等待中的线程,notifyall唤醒所有等待中的线程。

(3)为什么notify(), wait()等函数定义在Object中,而不是Thread中?

Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。线程调用wait()之后,会释放它锁持有的“同步锁”;等待线程可以被notify()或notifyAll()唤醒。wait()等待线程和notify()之间是通过对象的同步锁关联起来的。总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

5.ThreadLocal

(1)了解吗?

ThreadLocal这个类提供线程本地的变量。这些变量与一般正常的变量不同,它们在每个线程中都是独立的。

                                            Java并发总结_第1张图片

 

  • ThreadLocal并不维护ThreadLocalMap,并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。
  • 每个Thread对象内部都维护了一个ThreadLocalMap这样一个ThreadLocal的Map,可以存放若干个ThreadLocal。
  • 在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到ThreadLocal中。
  • 当调用set()方法的时候,很常规,就是将值设置进ThreadLocal中。

(2)应用?

当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。

web服务器 维护用户的session维护用户的登陆信息

6.interrupt()和线程终止方式

7.线程优先级和守护线程

  • java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。
  • java 中有两种线程:用户线程守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
  • 用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。

 

JUC集合

1.ConcurrentHashMap

JUC原子类

juc包中的原子类有:AtomicLong(对长整形进行原子操作)、AtomicInteger(对整形进行原子操作)等。

原理:

  • value是volatile类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的value值,即修改之后的volatile的值。
  • 通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,它的操作是原子的,即线程在操作value时不会被中断。

JUC锁

1.CAS

  • 定义:CAS函数,是比较并交换函数,它是原子操作函数;即,通过CAS操作的数据都是以原子方式进行的。
  • 原理
    • CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。
    • 如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
  • CAS的缺点
    • 循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
    • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
    • 什么是ABA问题?ABA问题怎么解决?
      • 如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。
      • 这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

2.AQS

(1)AQS是什么?怎么实现的?是接口还是类?

  • AQS -- 指AbstractQueuedSynchronizer类。
    • AQS是java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现。AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semaphore)的公共父类。
  • AQS锁的类别 -- 分为“独占锁”和“共享锁”两种。
    • 独占锁 -- 锁在一个时间点只能被一个线程锁占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁;而非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁。独占锁的典型实例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。
    • 共享锁 -- 能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock,CyclicBarrier, CountDownLatch和Semaphore都是共享锁。
  • AQS是如何实现的?使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
    • CLH队列 -- Craig, Landin, and Hagersten lock queue
      • CLH队列是AQS中“等待锁”的线程队列。在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。
      • CLH是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。

3.锁的原理(共享锁和互斥锁)

(1)互斥锁

(2)共享锁

(3)可重入锁、读写锁

4.Condition接口

(1)await、signal、signalall

(2)等待通知实现原理

JUC并发工具类

1.CyclicBarrier

2.CountDownLanch

(1)底层实现

3.Semaphore

JUC线程池

 

其他问题

(1)线程和进程的关系?一个java虚拟机是只支持单线程还是支持多线程的?

(2)并发下如何将异步变为同步?

(3)介绍ccyclicbarrier、wait、await区别,await中做了什么?

(4)生产者消费者模式

(5)如何保证线程安全?

(6)线程实现方式

  • callable和runnable之间的区别
    • Runnable只有一个run()函数,用于将耗时操作写在其中,该函数没有返回值
    • Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值。
  • 使用实例:
//runnable

public class RunnableClass implements Runnable{
    public void run(){
        //执行逻辑
    }
}

public class Main{
    public static  void main(String args[]){
        RunnableClass runnable=new RunnableClass();
        Thread thread=new Thread(runnable);
        thread.start();
    } 
}



//callable
public class CallableClass impleants Callable{
    public String call() throws Exception{
        //执行逻辑,具有返回值。
        return 
    }
}

public class Main{
    public static  void main(String args[]){
        CallableClass callable=new CallableClass();
        FutureTask futureTask=new FutureTask<>(callable);
        Thread thread=new Thread(futureTask);
        thread.start();

        //获取任务执行结果
        try {
			String resultString=futureTask.get();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    } 
}
  • 补充:
    • Future是一个接口,定义了Future对于具体的Runnable或者Callable任务的执行结果进行取消、查询任务是否被取消,查询是否完成、获取结果。
    • FutureTask的父类是RunnableFuture,而RunnableFuture继承了Runnbale和Futrue这两个接口。所以FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很费时逻辑需要计算并且返回这个值,同时这个值不是马上需要,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到!

你可能感兴趣的:(Java并发)