《Java并发编程之美-总结》

本文深入探讨Java并发编程,包括线程概念、线程安全、线程通知与等待、线程中断、ThreadLocal以及并发包中的类如CopyOnWriteArrayList、ConcurrentLinkedQueue和锁原理。文章还介绍了AQS(AbstractQueuedSynchronizer)及其在读写锁中的应用,并提到了JDK8新增的StampedLock。最后,讨论了并发队列如ConcurrentLinkedQueue和ArrayBlockingQueue的特点和实现。
摘要由CSDN通过智能技术生成

《一》

线程的概念:线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。真正要占用CPU运行的是线程线程是CPU分配的基本单位。

一个进程中有多个线程多个线程共享进程的堆和方法区资源但是每个线程有自己的程序计数器和栈区域

JVM 内存分配
程序计数器 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。
栈: 将线程的局部变量放在栈里面,这些局部变量是该线程私有的。每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量都是该线程私有的,其他线程是访问不了的,栈还用来存放线程的调用栈帧。
堆: 是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,主要存放使用new 操作创建的对象实例。
方法区: 方法区则用来存放JVM加载的类,常量及静态变量等信息,也是线程共享的。

程序计数器设计为线程私有的原因:线程是占用CPU执行的基本单位,而CPU一个是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,,要让出CPU,等下次轮到自己的时候中再执行。其实程序计数器就是为了记录下该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。如果执行的是本地native方法,那么PC计数器记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指定的地址。

3种创建线程的优缺点
创建方式 优点 缺点
继承Thread 类,重写run方法; 继承Thread的好处就是方便传参,通过set方法设置参数,或者通过构造函数进行传递。在run()方法内获取当前线程直接使用this 就可以了,不必使用Thread.currentThread()方法


1.java 是单继承,如果继承了thread类,就不能再继承其他类;

2.任务与代码没有分离,当多个线程执行一样的任务时,需要多份任务代码,而Runable则没有这个限制。
3.没有返回值;

实现Runnable 接口的run方法; 多个线程执行一样的任务时,只写一份代码就可以了。实现接口的方式比继承Thread类的方式更灵活,也能减少程序之间的耦合度,面向接口也是设计模式的6大原则的核心。

1.没有返回值;

2.参数只能使用主线程里面被声明为final的变量

使用FutureTask方式。 返回结果
多线程总结
继承Thread类 1.继承Thread方式的好处是方便传参,你可以在子类里面添加成员变量,通过set 方法设置参数或通过构造函数进行传递。java 单继承,多实现。
实现runnable接口 如果使用Runnable接口,则只能使用主线程里面被声明为final 的变量。
实现callable接口 callable 可以得到返回值

多线程的作用:

1.发挥多核CPU 的优势
2.防止阻塞
3.便于建模。

线程通知和等待:
Java中的object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了object类里面。

多线程中的方法
start()方法 只有调用了start()方法,才会表现出多线程的特性不同线程的run()方法里面的代码交替执行。
run()方法 如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
wait()函数:

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事才返回:
 1.其他线程调用了该共享对象的notify()或notifyAll()方法;
 2.其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回

注意:如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时,调用线程会抛出IIIegalMonitorStateException异常。

wait(long timeout)函数:

该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms 时间内被其他线程调用该共享变量的notify()或notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout 设置为0 则和wait 方法效果一样,因为wait方法内部就是调用了wait(0)。

注意:如果在调用该函数时,传递了一个负值,则会抛出iiiegalArgumentException异常。

notify()函数: 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait 方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程时随机的。被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
nofifyAll()函数: nofifyAll()方法会唤醒所有在该共享变量上由于调用wait 方法而被挂起的线程。
yield()函数:

让出CPU执行权的yield 方法

Thread 类中有一个静态的yield 方法,当一个线程调用yield方法时,实际上就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。

操作系统是为每个线程分配一个时间片来占有CPU,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度。而当一个线程调用了Thread类的静态方法yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用yield 方法时,当前线程会让出CPU 使用权,然后处于就绪状态线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU 执行权。

注意: 线程调用start方法之后线程并没有马上执行,而是处于就绪状态,这个就绪状态是指该线程已经过去了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态,一旦run方法执行完毕,该线程就处于终止状态。
sleep 与yileld 方法的区别
wait() 当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起。
sleep() 当线程调用sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。
yield() yield 方法,线程只是让出自己剩余的时间片,并没有阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调到当前线程执行。
sleep 和wait总结: sleep 和wait 都可以用来放弃CPU执行的事件,不同点在于如果线程持有某个对象的监视器sleep 方法不会放弃这个对象的监视器wait 会放弃这个对象的监视器

一个线程如何获取一个共享变量的监视器锁:

1.执行synchronized 同步代码块时,使用该共享变量作为参数。
  Synchronized(共享变量){
  //to do
  }

2.调用该共享变量的方法,并且该方法使用了synchronized修改。
synchronized void add(int a,int b){
  //to do 
  }

虚假唤醒: 一个线程可以从挂起状态变成可以运行状态(唤醒),即使该线程没有被其他线程调用notify(),notifyAll()方法进行通知,或被中断,或等待超时。

synchronized(obj){
  while(条件不满足){
    obj.wait();
  }
}

线程中断:java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

多线程中使用的方法
方法名称 作用
void interrupt()方法: 中断线程。例如,当线程A运行时,线程B可以调用线程A的interrupt()方法类设置线程A的中断标志为true 并立即返回。设置标志仅仅是设置标志,线程A 实际上并没有中断,它会继续往下执行。如果线程A 因为调用了wait,join, sleep()方法而被阻塞挂起,这时候若线程B 调用线程A 的interrupt()方法,线程A 会在调用这些方法 的 地方抛出interrupedException异常返回。
boolean isInterrupted()方法: boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
Boolean interrupted()方法: Boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。和isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。并且该方法是static方法,可以通过Thread类直接调用。
理解线程上下文切换: 在多线程编程中,线程个数一般都大于CPU个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配使用了时间片轮转的策略也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换。在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
现场上下文切换时机 当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。
线程死锁的定义 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
造成死锁的四个必备条件
互斥条件: 指线程对已经获取到的资源进行排他性使用,即该资源同时只能由一个线程占用。如果此时还是有其他线程请求获取该资源,则请求者只能等待,直到占有资源的线程释放该资源。
请求并持有条件: 指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件: 指线程获取到的资源在自己使用完毕之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件: 发生死锁时,比如存在一个线程一资源的环形链。指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
线程死锁解决方案: 要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可。目前只有请求并持有和环路等待条件时可以破坏的。造成死锁的原因就是和申请资源的顺序由很大关系,使用资源申请的有序性原则就可以避免死锁。

线程分为两类:用户线程和守护线程

daemon daemon 守护线程,JVM中的垃圾收集就是守护线程。只要还有一个用户线程,正常情况下JVM就不会退出。
user user用户线程。main 函数所在的线程就是一个用户线程
总结:如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后,子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

ThreadLocal:多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。

同步的措施一般是加锁当创建一个变量后,每个线程对其进行访问的时候使用ThreadLocal 就可以做这件事情。ThreadLocal 是JDK包提供的,它提供了线程本地变量,即如果你创建了一个Threadlocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。

Thread 类中有一个threadLocals 和 一个inheritableThreadLocals ,它们都是ThreadLocalMap 类型的变量,而ThreadLocalMap 是一个定制化的HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLoal 的set 或者get时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal 实例里面&#

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值