栏目分类:
子分类:
返回
终身学习网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
终身学习网 > IT > 软件开发 > 后端开发 > Java

Java并发编程

Java 更新时间:发布时间: 百科书网 趣学号

目录

线程,进程名词解释,关系

Java中创建线程

状态

多线程优缺点

并发编程

死锁,如何避免死锁.

守护线程

线程间通信,

并发编程中的问题

AtomicInteger底层实现原理是什么? 包证原子性

ThreadLocal的底层原理

synchronized 和 volatile 的区别是什么?

线程的 run() 和 start() 有什么区别?

Runnable和Callable的区别 T call()Throws Exception{}

什么是CAS

什么是AQS

线程池的好处,如何创建线程池?


线程,进程名词解释,关系

程序:是为完成特定任务,用某种语言编写的一组指令的集合,即一段静态代码.

进程:就是正在执行的程序,从windows角度讲,进程是操作系统进行资源分配的最小单位.

线程:进程可进一步细化为线程,是一个进程内部的最小执行单元,是操作系统进行任务调度的最小单元,隶属于进程.

一个进程可以包含多个线程,一个线程只能属于一个进程,线程不能脱离进程而独立运行.

每一个进程至少包含一个线程(称为主线程);在主线程中开始执行程序,Java程序的入口main()方法就是在主线程中被执行的

在主线程中可以创建并启动其他的线程;

一个进程内的所有线程共享该进程的内存资源.

Java中创建线程

1.继承Thread类

在Java中要实现线程,最简单的方式就是扩展Thread类,重写其中的run方法,

Thread类中的run方法本事并不执行任何操作,如果我们重写了run方法,当线程启动时,它将执行run方法.

定义: 
​
public class MyThread extends Thread { 
​
public void run() { 
​
} 
​
}
调用: 
MyThread thread = new MyThread(); 
thread.start();

Thread类中的常用方法

(1)构造方法:

Thread();创建一个新的线程

Thread(String name);创建一个指定名称的线程.

Thread(Runnable target);利用Runnable对象创建一个线程,启动时将执行对象的run方法.

Thread(Runnable target,String name);利用Runnable对象创建一个线程,并指定该线程的名称.

(2)Thread类中方法

void start();启动线程

final void setName(String name);设置线程的名称.

final String getName();返回线程的名称.

final void setPriority(int newPriority);设置线程优先级

final int getPriority();返回线程的优先级.

final void join() throws InterruptedException;等待线程终止.

static Thread currentThread();返回当前正在执行的线程对象的引用.

static Thread currentThread();返回对当前正在执行的线程对象的引用.

static void sleep(long millis) throws InterruptedExcption;让当前正在执行的线程休眠,休眠时间有millis(毫秒)指定.

2.实现Runnable接口

java.lang.Runnable接口中仅仅只有一个抽象方法;

public void run();

也可以通过实现Runnable接口的方式来实现线程,只需要实现其中的run方法即可;

Runnable接口的存在主要是为了解决java中不允许多继承的问题 .

定义: public class MyThread implements Runnable{ 
@Override 
public void run() {
…… 
} 
}
线程执行任务 
MyThread r = new MyThread(); 
创建一个线程作为外壳,将r包起来, 
Thread thread = new Thread(r);
thread.start();

继承方式与实现方式的联系与区别

区别:

(1)继承Thread:线程代码存放Thread子类run方法中.

(2)实现Runnable:线程代码存放在接口的子类run方法.

实现Runnable的好处;

(1)避免了单继承的局限性.

(2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源.

线程优先级:

计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务;

优先级较高的线程有更多获得CPU的机会.

优先级用整数表示,1~10,一般情况下,线程的默认优先级都是5,但也可以通过setPriority和getPriority方法来设置或返回优先级.

调度策略:

(1)时间片.

(2)抢占式:高优先级的线程抢占CPU

Java的调度方法:

(1)对于同优先级线程组成先进先出队列,使用时间片策略.

(2)对于高优先级,使用优先调度的抢占策略.

Thread类有3个静态常量来表示优先级.

(1)MAX_PRIORITY:取值为10,表示最高优先级.

(2)MIN_PRIORITY:取值为1,表示最底优先级。

(3)NORM_PRIORITY:取值为5,表示默认的优先级。

状态

线程在它的声明周期中会处于不同的状态.

新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态.

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行条件,只是没分配到CPU资源.

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能.

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态.

死亡:线程完成了它的全部工作或线程被提前强制性的终止或出现异常导致结束.

多线程优缺点

多线程的概念

多线程是指程序中包含多个执行单元,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务.

何时需要多线程

程序需要同时执行两个或多个任务.

程序需要实现一些需要等待的任务时,如用户输入,文件读写等,网络操作,搜索等.

需要一些后台运行的程序时.

多线程的优点

提高程序的响应.

提高CPU的利用率.

改善程序结构,讲复杂任务分为单个线程,独立运行.

多线程的缺点

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多.

多线程需要协调和管理,所以需要CPU时间跟踪线程;

线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题.

并发编程

线程同步(并发与并行)

并行:多个CPU同时执行多个任务.例:多个人同时做不同的事.

并发:一个CPU(采用时间片)同时执行多个任务,例:秒杀,多个人做同一件事.

多线程同步

多个线程同时读写同一份共享资源时,可能会引起冲突.所以引入线程"同步"机制",即个线程间要有先来后到.

同步就是排队+锁

几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作.

为了保证数据在方法中被访问时的正确性,在访问时加入锁机制.

确保一个时间点只有一个线程访问共享资源.可以给共享资源加一把锁,那个线程获取这把锁,才有权利访问该共享资源.

在Java中实现同步:

使用synchronized(同步监视器)关键字同步方法或代码块.

synchronized(同步监视器){

//需要被同步的代码

}

synchronized还可以放在方法声明中,表示整个方法为同步方法.

例:

public synchronized void show(String name){

//需要被同步的代码;

}

同步监视器:

同步监视器可以是任何对象,必须唯一,保证多个线程获得的是同一个对象(锁).

同步线程的执行过程:

1.第一个线程访问,锁定同步监视器,执行其中代码.

2.第二个线程访问,发现同步监视器被锁定,无法访问.

3.第一个线程访问完毕,解锁同步监视器.

4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问.

一个线程持有锁会导致其他所有需要此 锁的线程挂起;在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题.

Lock(锁)

在Java中,从JDK5.0开始,Java提供了更强大的线程同步机制,通过显示定义同步锁对象来实现同步.同步锁使用Lock对象充当.

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具.锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象.

ReentrantLock类实现Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁.

线程死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁.

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续.

设计时考虑清楚锁的顺序,尽量减少嵌套的加锁交互数量.

线程通信

线程通讯指的是多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用.

涉及三个方法:

wait();一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.

notify();一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait.就会唤醒优先级高的那个.

notifyAll();一旦执行此方法,就会唤醒所有被wait的线程.

wait(),notify(),notifyAll(),三个方法必须使用在同步代码块或同步方法中.

在Java中新增创建线程方式

实现Callable接口与使用Runnable相比,Callable功能更强大一些.

相比run()方法,可以有返回值.

方法可以抛出异常.

支持泛型的返回值.

需要借助FutureTask类,获取返回结果.

接受任务

FutureTask futureTask=new FutureTask(任务);

创建线程

Thread t=new Thread(futureTask);

t.start();

Integer val=futureTask.get();获得线程call方法的返回值.

死锁,如何避免死锁.

什么是死锁:

多个进程或线程互相等待对方的资源,在得到新的资源之前不会释放自己的资源,这样就形成了循环等待,这样的现象就被称为死锁.

产生死锁的四大必要条件:

资源互斥:资源只有两种状态,只有可用和不可用两种状态,不能同时使用,同一时刻只能被一个进程或线程使用.

占有且请求:已经得到资源的进程或线程,继续请求新的资源,并持续占有旧的资源.

资源不可剥夺:资源已经分配进程或线程后,不能被其他进程或线程强制性的获取,除非资源的占有者主动释放.

环路等待:死锁发生时,系统中必定有两个或两个以上的进程或线程组成一条等待环路.

注意:死锁一旦产生基本无解,现在的操作系统无法解决死锁,只能防止死锁的发生.

防止死锁产生的方法:

破坏占有且请求条件:采用预先静态分配的方法,进程或线程在运行前一次申请所有资源,在资源没有满足前不投入运行.

缺点:系统资源会被严重浪费,因为有些资源可能开始时使用,有些资源结束时才使用.

破坏不可剥夺条件:当一个进程或线程已经占有一个不可剥夺的资源时,请求新资源无法满足时,则释放已经占有的资源,一段时间后在重新申请.

缺点:该策略实现起来比较复杂,释放已经获取资源可能导致前一阶段的工作失效,反复的申请释放资源会增加系统开销,占用CPU和寄存器,内存等资源.

破坏循环等待条件:给每个资源进行编号,进程或线程按照顺序请求资源,只有拿到前一份资源,才能继续请求下一个资源.

缺点:资源的编号必须相对稳定,资源添加或销毁时会受到影响.

死锁根本原因:
是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
一个线程T1持有锁L1,并且申请获得锁L2;而另一个线程T2持有锁L2,并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。
​
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
​
既然我们知道了产生死锁可能性的原因,那么就可以在编码时进行规避。
如何避免
1、避免嵌套锁
2、只锁需要的部分
3、避免无限期等待

守护线程

  是服务于用户线程的线程,指在后台运行提供一种通用的线程,这种线程并不属于程序不可或缺的部分,任何一个守护线程都是用户线程的"保姆".比如垃圾回收线程就是一个守护线程,如果存在用户线程存在的话,JVM就不会退出,此时所有用户线程都执行完毕,main线程也执行完毕,那JVM也会停止运行,守护线程也会停止.

线程间通信,

线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题.

线程间的通信方式: wait/notify机制

并发编程中的问题

Thread和Runnable的关系,区别

多线程的创建方式一:继承于Thread类的子类.

1.创建一个继承于Thread类的子类.

2.重写Thread类的run().[将此线执行的操作声明在run()中].

3.创建Thread类的子类的对象.

4.通过此对象调用start();

多线程创建方式二:实现Runnable接口:

1.创建一个实现了Runnable接口的类.

2.实现类去实现Runnable中的抽象方法:run();

3.创建实类的对象.

4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象.

5.通过Thread类的对象调用start();

两种方式都需要重写run()方法.

Thread类对Runnable接口进行了扩展,他们的实质上是Thread实现了Runnable接口.

如果只是简单的执行一个任务,那就实现Runnable,若有复杂的线程需求,那就选择继承Thread.

开发中优先选择Runnable接口的方式,实现的方式没有类的单继承性的局限性,实现的方式更适合来处理多个线程共有数据的情况.

synchronized锁优化,怎么优化?

为什么需要优化

synchronized监视器锁在互斥同步上对性能的影响很大.

Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,状态转换需要花费很多的处理器时间.

所以频繁的通过synchronized实现同步会严重影响到程序效率,这种锁机制也被称为重量级锁,为了减少重量级锁带来的性能开销,JDK对synchronized进行种种优化.

jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

自旋锁和适应自旋锁

大多数情况下,线程持有锁的时间都不会太长,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的.

自旋锁

当锁被占用时,当前想要获取锁的线程不会立即被挂起,而是作几个空循环,看持有锁的线程是否会很快释放锁,在经过若干次循环后,如果得到锁,就顺利进入临界区,如果还不能获得锁,那就会将线程在操作系统层面挂起.

自旋锁和阻塞最大的区别

放弃处理器的执行时间.

阻塞放弃了CPU时间,进入等待区,等待被唤醒.响应慢.自旋锁一直占用CPU时间,时刻检查共享资源是否可以被访问,所以响应速度更快.

缺点

如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好.但是如果持有锁的线程占用锁时间较长,等待锁的线程自旋一定次数后还是那不到锁而被阻塞,那么自旋就白白浪费了CPU资源.所以自旋的次数决定了自旋锁的性能.JDK自旋默认次数为10次,可以通过参数-XX:PreBlockSpin来调整

自适应自旋锁

所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自选时间及锁的拥有者的状态来决定.线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么他就会允许自旋等待持续的次数更多,如果对于某个锁,很少有自旋能够成功的,那么在以后要这个锁的时候自旋的次数会减少甚至省略掉自旋过程,一面浪费处理器资源.

锁消除

如果JVM检测到某段代码不可能存在共享数据竞争,JVM会对这段代码的同步锁消除.

在编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析的技术来判断同步块所使用的锁对象是否只能被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步.

锁粗化

通常情况下,我们提倡尽量减小锁的粒度,可以避免不必要的阻塞,让同步块的作用范围尽可能小,仅在共享数据的实际作用域才进行同步,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁.

但如果在一段代码中连续的用同一个监视器锁反复的加锁解锁,甚至加锁操作出现在循环体中的时候,就会导致不必要的性能损耗.这种情况就需要锁粗化.

锁粗化就是将多个连续的加锁,解锁操作连接在一起,扩展成一个范围更大的锁.

Java对象头

对象在内存中存储的布局可以分为三块区域:对象头,实例数据,和对齐填充.

普通对象的对象头包括两部分:Mark Word和 Class Metadata Address(类型指针).如果是数据对象还包括一个额外的Array length数组长度部分.

Mark Word:用于存储对象自身的运行时数据,如哈希码(hashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等,占用内存大小与虚拟机位长一致.

Class Metadata Address:类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例.

偏向锁,轻量级锁,重量级锁

从Java对象有的Mark Word中可以看到,synchronized锁一共是四种状态:无锁,偏向锁,轻量级锁,重量级锁.

偏向锁,轻量级锁,重量级锁,三种形式分别对应了锁只被一个线程持有,不同线程交替持有锁,多线程竞争锁三种情况.

偏向锁

大多数情况下锁不仅存不存在多线程竞争,而且总是由同一个线程多次获取,所以引用偏向锁让线程获得锁的代价更低.

偏向锁认为环境中不存在竞争情况,锁只被一个线程持有,一旦有不同的线程获取或竞争锁对象,偏向锁就会升级为轻量级锁.

偏向锁在无多线程竞争情况下可以减少不必要的轻量级锁执行路径.

轻量级锁

在同步块中并不会出现竞争情况,大部分是不同线程交替持有锁,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销.

重量级锁

监视器锁Monitor

锁膨胀

synchronized锁膨胀过程:无锁->偏向锁->轻量级锁->重量级锁的一个过程.这个过程是随着多线程对锁的竞争越来越激烈,锁逐渐升级膨胀的过程.

1)一个锁对象刚刚开始创建的时候,没有任何线程来访问它,此时线程状态为无锁状态。Mark word(锁标志位-01 是否偏向-0)
​
2)当线程A来访问这个对象锁时,它会偏向这个线程A。线程A检查Mark word(锁标志位-01 是否偏向-0)为无锁状态。此时,有线程访问锁了,无锁升级为偏向锁,Mark word(锁标志位-01,是否偏向-1,线程ID-线程A的ID)
​
3)当线程A执行完同步块时,不会主动释放偏向锁。持有偏向锁的线程执行完同步代码后不会主动释放偏向锁,而是等待其他线程来竞争才会释放锁。Mark word不变(锁标志位-01,是否偏向-1,线程ID-线程A的ID)
​
4)当线程A再次获取这个对象锁时,检查Mark word(锁标志位-01,是否偏向-1,线程ID-线程A的ID),偏向锁且偏向线程A,可以直接执行同步代码。这样偏向锁保证了总是同一个线程多次获取锁的情况下,每次只需要检查标志位就行,效率很高。
​
5)当线程A执行完同步块之后,线程B获取这个对象锁 检查Mark word(锁标志位-01,是否偏向-1,线程ID-线程A的ID),偏向锁且偏向线程A。有不同的线程获取锁对象,偏向锁升级为轻量级锁,并由线程B获取该锁。
​
6)当线程A正在执行同步块时,也就是正持有偏向锁时,线程B获取来这个对象锁。
​
检查Mark word(锁标志位-01,是否偏向-1,线程ID-线程A的ID),偏向锁且偏向线程A。
​
线程A撤销偏向锁:
​
等到全局安全点执行撤销偏向锁,暂停持有偏向锁的线程A并检查程A的状态;
如果线程A不处于活动状态或者已经退出同步代码块,则将对象锁设置为无锁状态,然后再升级为轻量级锁。由线程B获取轻量级锁。
如果线程A还在执行同步代码块,也就是线程A还需要这个对象锁,则偏向锁膨胀为轻量级锁。
线程A膨胀为轻量级锁过程:
​
1.在升级为轻量级锁之前,持有偏向锁的线程(线程A)是暂停的
2.线程A栈帧中创建一个名为锁记录的空间(Lock Record)
3.锁对象头中的Mark Word拷贝到线程A的锁记录中
4.Mark Word的锁标志位变为00,指向锁记录的指针指向线程A的锁记录地址,Mark word(锁标志位-00,其他位-线程A锁记录的指针)
5.当原持有偏向锁的线程(线程A)获取轻量级锁后,JVM唤醒线程A,线程A执行同步代码块
7)线程A持有轻量级锁,线程A执行完同步块代码之后,一直没有线程来竞争对象锁,正常释放轻量级锁。释放轻量级锁操作:CAS操作将线程A的锁记录(Lock Record)中的Mark Word替换回锁对象头中。
​
8)线程A持有轻量级锁,执行同步块代码过程中,线程B来竞争对象锁。Mark word(锁标志位-00,其他位-线程A锁记录的指针)
​
1.线程B会先在栈帧中建立锁记录,存储锁对象目前的Mark Word的拷贝
2.线程B通过CAS操作尝试将锁对象的Mark Word的指针指向线程B的Lock Record,如果成功,说明线程A刚刚释放锁,线程B竞争到锁,则执行同步代码块。
3.因为线程A一直持有锁,大部分情况下CAS是会失败的。CAS失败之后,线程B尝试使用自旋的方式来等待持有轻量级锁的线程释放锁。
4.线程B不会一直自旋下去,如果自旋了一定次数后还是失败,线程B会被阻塞,等待释放锁后唤醒。此时轻量级锁就会膨胀为重量级锁。Mark word(锁标志位-10,其他位-重量级锁monitor的指针)
5.线程A执行完同步块代码之后,执行释放锁操作,CAS 操作将线程A的锁记录(Lock Record)中的Mark Word 替换回锁对象对象头中,因为对象头中已经不是原来的轻量级锁的指针了,而是重量级锁的指针,所以CAS操作会失败。
6.释放轻量级锁CAS操作替换失败之后,需要在释放锁的同时需要唤醒被挂起的线程B。线程B被唤醒,获取重量级锁monitor

synchronized和ReentrantLock/Lock有什么区别呢?

synchronized和Lock的区别

1.synchronized是Java关键字属于原生语法层次,需要jvm实现,而Lock是个java类.

2.synchronized需要获取的锁的线程执行完同步代码块后释放锁,但Lock需要在finally中手工释放锁,否则会造成线程死锁.

3.synchronized无法查看锁状态,而Lock可以查看是否获取到锁.

4.若有线程A和B,使用synchronized关键字时,A获得了锁,但没有执行完同步代码块,或是堵塞了,B就会一直等待.而Lock锁可以有尝试获取锁,如果尝试获取不到线程就可以不用一直等待就结束了.

5.synchronized的锁可重入,不可中断,非公平.而Lock,可重入,可中断,可公平

6.synchronized锁适合少量同步,而Lock锁适合大量同步.

synchronized和ReentrantLock的区别

synchronized是java中的关键字而ReentrantLock是类.提供了比synchronized更到灵活的特性,可以被继承,可以有方法,可以有各种变量.

ReentrantLock可以对回去锁的等待时间进行设计.

ReentrantLock可以获取各种锁的信息.

ReentrantLock可以灵活的实现多路通知.

ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中的mark word.

synchronized和lock的用法区别:

synchronized是在需要的同步对象中加锁,可以加在方法上,也可加在特定的代码块中,括号中表示需要锁的对象.

lock一般使用ReentrantLock类做为锁.一般会在finally块中释放锁(unlock())防止死锁.

synchronized和lock性能区别:

synchronized是托管给JVM执行的,而lock是Java写的是控制锁的代码.

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁.独占锁意味着其他线程只能依靠阻塞来等待线程释放锁.而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低.

而Lock用的是乐观锁方式.所谓乐观锁就是,每次不加锁而是假设没有冲突区完成某项操作,如果因为冲突失败就重试,直到成功为止.乐观锁实现的机制就是CAS操作.其中比较重要获得锁的一个方法时compareAndSetState,这里其实就是调用CPU提供的特殊指令.

AtomicInteger底层实现原理是什么? 包证原子性

Atomiclnteger是对int类型的一个封装类,提供原子性的访问和更新操作,其原子性操作实现基于CAS(compare-and-swap)技术.并用volatile修饰value,保证可见性.

CAS技术指比较并返回,是一种轻量级锁.

底层通过操作系统原语实现,保证了原子性.在CAS中,在线程读取数据不用加锁,在准备写回时,比较原值是否修改,若未被修改则写回,若已被修改,则重新执行读取流程,这是一种乐观策略,认为并发冲突并不总会发生.

适用于少量线程竞争的场景,但会存在几个问题:

1.ABA:A线程读取一个值后,发生了两次写回,先由B线程改变了值,在有C线程在将值写会原值,虽然值还是原值,但实际上已经被改变了,虽然不会影响结果,但在业务中需要记录修改过程的,解决办法是加标志位,比如版本或者时间戳.

2.循环时间开销大:CAS如果长时间操作不成功,就会一直自旋,占用资源,解决办法是设定阈值.

3.只能保证一个共享变量的原子性.

从Atomiclnteger的内部属性可以看出,它依赖于Unsafe提供的一些底层能力,进行底层操作;以volatile的value字段,记录数值,以保证可见性.

ThreadLocal的底层原理

ThreadLocal是一个线程级别的局部变量,内部使用Map结构保存数据.

它的作用更多的是包装数据到线程中,相当于线程任何位置都可以用this来获取这个数据.

在线程执行方法的时候,不用每次都显示的指定一个参数位置用来传递对象,而是用线程中的ThreadLocal来获取对象.

在线程池中使用ThreadLocal会造成内存泄漏

 ThreadLocal作为key,被弱引用管理(存活到下次的垃圾回收), 而值,是强引用的,与ThreadLocalMap和Thread强关联. 如果一个ThreadLocal不存在外部强引用,key(ThreadLocal)势必会被GC回收,这样就导致ThreadLocalMap中的key为null,而value还存在强引用,只有thead线程退出以后,value的强引用链条才会断掉.

  如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:造成内存泄漏.

Thread Ref->Thread->ThreadLocalMap->Entry->value

解决办法是,在使用ThreadLocal对象之后,手动调用ThreadLocal的remove()方法,手动清除Entry对象.

经典的使用场景

经典的使用场景是为每个线程分配一个JDBC连接Connection.

当然每个连接都是新new的一个连接,并不是同一个连接.

这样就可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关闭了B线程正在使用的Connection.

包证每个线程中会存储一份变量

ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,是一个key-value数据形式结构,也就是ThreadLocal的核心.

ThreadLocalMap中数据时存储在Entry类型的数据table中的,Entry继承了WeakReference(弱引用),注意key是弱引用,但value不是.提高了垃圾回收的效率.

ThreadLocalMap可能存在内存泄漏,因为key被回收后,但是vlaue依然和Entry存在强引用关系,所以使用完进行remove是一个很好的习惯,可以避免内存泄漏.

ThreadLocal只是一个用来操作ThreadLocalMap的一个工具类和充当ThreadLocalMap得到key.

ThreadLocalMap是真正存储数据的,但是ThreadLocalMap是存储在每个线程中的.

synchronized 和 volatile 的区别是什么?

volatile

volatile具有两种特性,第一就是保证共享变量对所有线程的可见性,将一个共享变量声明为volatile后:

1.当写一个volatile变量时,JVM会把该线程对应的本地内存中变量强制刷新到主内存中去;

2.这个写操作会导致其他线程中的缓存无效.

使用场景:

只能在有限的一些情形下使用volatile变量替代锁.要使用volatile变量提供理想的线程安全,必须同时满足下面两个条件:

1.对变量的写操作不依赖与当前值.

2.该变量没有包含在具有其他变量的不变式中.

volatile最适用于一个线程写,多个线程读的场合.

如果有多个线程并发写操作,任然需要使用锁或者线程安全的容器或者原子变量来代替.

synchronized

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.

1.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完成这个代码块后才能执行该代码块.

2.然而,当一个线程访问object的一个synchronized同步代码块时,另一个线程任然可以访问该object中的非synchronized同步代码块.

3.尤其关键的是,当一个线程访问object的一个synchronized同步代码块时,其他线程对object中所有其他synchronized同步代码块的访问将被阻塞.

4.当一个线程访问object的一个synchronized同步代码块时,它就获得了这个object的对象锁,结果其他线程对该object对象所有同步代码部分的访问都会被暂时阻塞.

两者的区别

1.volatile是变量修饰符,而synchronized则作用于一段代码或方法.

2.volatile只是在线程内存和"主"内存间同步某个变量的值.而synchronized通过锁定和解锁某个监视器同步所有变量的值,显然synchronized要比voatile消耗更多的资源.

3.volatile不会造成线程的阻塞;synchronized可能造成线程的阻塞.

4.volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步.

5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化.

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的.

关键字volatile主要使用的场合是多个线程中可以感知实例变量被修改,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用.

关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存中读取的,这样就保证了同步数据的可见性.volatile本身并不处理数据的原子性,而是强制对数据的读写及时的影响到主内存中.

线程的 run() 和 start() 有什么区别?

1.线程中的start()方法,当程序调用 start()方法,将会创建一个新线程去执行run()方法中的代码.但如果直接调用run()方法的话,会直接在当前线程中执行run()方法中的代码,并不会创建新线程,这样就像一个普通的方法一样.

2.另外当一个线程启动之后,不能重复调用start(),会报IllegalStateException异常。但是可以重复调用run()方法。

Runnable和Callable的区别 T call()Throws Exception{}

Java多线程有两个重要的接口,Runnable和Callable,分别提供一个run()方法和call()方法.

1.Runnable提供run方法,无法通过throws抛出异常,所有CheckedException必须在run方法啊内部处理.Callable提供call方法,直接抛出Exception异常.

2.Runnable的run方法无返回值,Callable的call方法提供返回值用来表示任务运行的结果.

3.Runnable可以作为Thread构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行.而Callable只能通过线程池执行

4.运行Callable任务可拿到一个Future对象,Future表示异步计算的结果.

5.它提供了检查计算是否完成的方法,以等待计算的完成,还可以获取任务执行的结果.

6.通过Future对象可了解任务执行情况,可取消任务的执行,还可以获取任务执行的结果.

7.Callable时候类似于Runnable的接口,实现Callable接口的和实现Runnable的类都可以被其他线程任务执行.

Callable转换成Runnable的流程

1.开发者实现Callable接口.

2.实例化Callable,然后提交到线程池.

3.以Callable为构造函数创建Future Task.

4.最终将Future Task提交给线程池的线程进行执行.

线程池如何执行Callable

将Callable包装成Runnable后,线程池的执行和执行Runnable一样,Callable的特点就是可以获得返回值.如果执行的逻辑不关心返回值就可以直接用Runnable来.但是如果需要涉及到获取到业务逻辑中的返回值那么就是用Callable来提交到线程池中.

Runnable和Callable两者没有继承关系,Callable通过Future Task包装成Runnable.

线程池执行任务的时候,如果关系返回值就用Callable,不关心返回值用Runnable.

Runnable如果也需要返回值,线程池内部通过RunnableAdapter适配器来适配成Callable.

Future

Executo就是Runnable和Callable的调度容器,Future就是对于具体的Runnable或者Callable任务的执行结果进行取消,查询是否完成,获取结果,设置结果操作.get方法会阻塞,直到任务返回结果.

什么是CAS

Java在Jdk1.5中新增了java.util.concurrent(j.u.c)就是建立在CAS之上的,相对于synchronized这种阻塞算法,CAS是非阻塞算法的异常常见实现.

CAS(Compare And Swap)"比较并交换",CAS需要三个操作数:内存地址V,旧的预期值A,即将要更新的目标值B.

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V修改为B,否则就什么都不做.整个比较并交换的操作是一个原子操作.

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是告知这次竞争中失败,可以再次尝试.

CAS中经典ABA问题

1.线程A读取了数据A,线程2也读取了数据A.

2.线程通过CAS比较,发现原数据A没错,于是就将数据A改成了B.

3.线程3此时通过CAS比较,发现原数据就是B,于是将数据B改成数据A.

4.此时,线程1通过CAS比较,发现原数据是A,就改成了自己要改的值.

虽然线程1最后可以操作成功,但这样违背了CAS的初衷,数据已经被修改过了,按CAS规则来讲,线程1是不应该修改成功的.

规避ABA问题

可以设置一个自增的标志位,数据的每一次修改标志位都会自增,比较标志位的值,还可以加上时间戳,显示上一次修改的时间,比较时间戳的值.

CAS的循环时间开销大问题

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升pause指令的两个作用:

1.他可以延迟流水线指令,时候CPU不会消耗过多的执行资源.

2.他可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空.

CAS的只能保证一个共享变量的原子操作问题

当一个共享变量进行原子操作,循环CAS可以解决,但是如果多个共享变量,循环CAS无法解决这个问题.

Java5之后,JDK提供了AtomicRefence类来保证引用对象之间的原子性,可以包多个变量放在一个对象那个进行CAS操作.

什么是AQS

ReentranLock,ReentrantReadWriteLock底层都是基于AQS来实现的.

AQS(AbstractQueuedSynchronized),是抽象队列同步器,就是一个用来构建锁和同步器的框架,内部实现的关键是:先进先出的队列,state状态,在Lock包中的相关锁(常用的有ReentrantLock,ReadWriteLock)都是基于AQS来构建.

AQS核心思想

如果请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态.如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列实现的,即将暂时获取不到锁的线程加入到队列中.

AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作.AQS使用CAS对该同步状态进行原子操作实现对其值的修改.

AQS定义了两种资源获取方式:独占(只有一个线程能访问执行)和共享(多个线程可同时访问执行)

独占锁有tryAcquire和tryRelease.共享有tryAcquireShared和tryReleaseShared.AQS的核心由一个阻塞队列和一个volatile修饰的state变量组成,AQS可以通过CAS对state变量进行修改,一般来说,state为0时表示无锁状态,state大于0时表示有线程获得锁,从代码上看,如果我们调用lock方法是,触发Acquire方法,该方法又会去调用tryAcquire方法以CAS的方式尝试获取锁,如果获取失败,就调用addWaiter方法把当前线程包装为Node对象添加到阻塞队列中.然后调用acquireQueued方法通过自旋去获取锁.它的所有子类中,要么实现并使用它的独占功能的api,要么使用了共享锁的功能,而不会同时使用两套api,即便是最有名的子类ReentrantReadWirteLock也是通过两个内部类读锁和写锁分别实现了两套api来实现的.

AQS的实现思路

AQS内部维护了一个CLH队列来管理锁,线程首先会尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node结点加入到同步队列sync queue里.接着会不断的循环尝试获取锁,条件是当前结点为head的直接后继才会尝试.如果失败就会阻塞自己知道自己被唤醒.而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程.

CLH(Craig Landin And Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系).

AQS是将每条请求共享资源的线程封装成一个CLH锁队列得一个节点(Node)来实现锁的分配.

线程池的好处,如何创建线程池?

什么是线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.

在开发过程中,合理地使用线程池能够带来3个好处.

1.降低资源消耗.通过重复利用机制已降低线程创建和销毁造成的消耗.

2.提高响应速度.当任务到达时,任务可以不需要等到线程创建就能立即执行.

3.提高线程的可管理性.线程时稀缺资源,如果无限地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一分配,调优和监控.

线程池的作用

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务.减少了创建和销毁线程所需要的时间,从而提高了效率.

如果一个线程的时间非常长,就没必要用线程池(不是不能作长时间操作,而是不宜),况且我们还不能控制线程中线程的开始,挂起和中止.

Executors 目前提供了 5 种不同的 线程池创建配置:

1)newCachedThreadPool():用来处理大量短时间工作任务的线程池。当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;其内部使用 SynchronousQueue 作为工作队列。

2)newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的4。

3)newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态

4)newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

5)newWorkStealingPool(int parallelism),Java 8 才加入这个创建方法,并行地处理任务,不保证处理顺序。

1.newCachedThreadPool:可缓存的线程池.

2.newFixedThreadPool:固定大小的线程池.

3.newSingleThreadExecutor:固定单个线程的线程池.

4.newScheduledThreadPool:用作任务调度的线程池

5.newWorkStedlingPoll:足够大小的线程池,JDK1.8新增

阿里编程规约

建议使用ThreadPoolExecutor类,是最原始的线程池创建.

一个各个类型的线程池,除了newWorkStealingPool用的是ForkJoinPool类,其他线程的构建最后其实都调用了ThreadPoolExecutor类的构造方法区创建线程池,而这些类型的线程池只是根据自己的需要来传入一些默认的值,也正是因为这些参数,才会导致可能出现OOM的问题,那我们也可使用ThreadPoolExecutor来创建线程池,里面所有的参数我们都可以根据需求自己家指定,这样使用起来放心许多,所以推荐使用ThreadPoolExecutor来创建线程池.

7个参数

public ThreadPoolExecutor(int corePoolSize,                          
                          int maximumPoolSize,                          
                          long keepAliveTime,                          
                          TimeUnit unit,                          
                          BlockingQueue workQueue) {    
                          this(
                          corePoolSize, 
                          maximumPoolSize, 
                          keepAliveTime, 
                          unit, 
                          workQueue,         
                          Executors.defaultThreadFactory(), 
                          defaultHandler
                 
);}

1.corePoolSize

线程池核心线程大小.

线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,他们也不会销毁,除非设置allowCoreThreadTimeOut.这里的最小线程数量即是corePoolSize;

2.maximumPoolSize

线程最大线程数量

一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列中,如果工作队列满了,才会创建一个新的线程,然后从工作队列的头部取出一个任务交由新线程处理,而将刚提交的任务放入工作队列的尾部,线程池不会无限制的去创建线程,它会有一个最大线程数量的限制,这个数量即有maximumPoolSize指定.

3.keepAliveTime

空闲线程存活时间

一个线程如果处于空闲状态,并且当前线程数量大于corePoolSize,那么在指定的时间后,这个空线程会被销毁,这里的指定时间由keepAliveTime设定.

4.unit

keepAliveTime的计量单位

TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒 5.workQueue

工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务.JDK提供了四种工作队列.

1.ArrayBlockingQueue有界队列

基于数组的有界阻塞队列,按FIFO排序.新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽的问题,当线程中线程数量达到corePollSize后,在有新任务进来,则会将任务放入该队列的队伍,等待被调度.如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略.

2.LinkedBlockingQueue无界队列

基于链表的无界阻塞队列(其实最大容量为Integer.Max).按照FIFO排序.由于该队列的近似无界性,当线程池中线程数量达到corePollSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize是不起作用的.

3.SynchronousQueue直接提交

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务,也就说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建线程,如果线程数量达到maxPoolSize,则执行拒绝策略.

4.PriorityBlockingQueue优先级队列

具有优先级的无界阻塞队列,优先级通过参数Comparator实现.

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue.线程池的排队策略与BlockingQueue有关.

6.ThreadFactory

用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设定线程名,设置daemon和优先级等等.

7.handler

拒绝策略

当工作队列中的任务已经到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提价进来,该如何处理呢,这里的拒绝策略,就是解决这个问题的,JDK提供了4种拒绝策略.

CallerRunsPolicy只用调用者所在线程来运行任务

该策略下,在调用者线程中直接执行被拒绝的run方法,除非线程已经shutdown,则直接抛弃任务.

AbortPolicy直接抛出异常

直接丢弃任务,并抛出RejectedExecutionException异常

DiscardPolicy不处理,丢弃掉

直接丢弃任务,什么都不做.

DiscardOldestPolicy丢弃队列里2最近的一个任务,并执行当前任务

抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列.

也可以根据应用场景需要来实现RejectedExecutionHandle接口自定义策略.如日志或持久化不能处理的任务.

 等待队列满了,线程最大数量
 AbortPolicy  报错
 DiscardPolicy 直接丢弃
 DiscardOldestPolicy  抛弃等待时间最长的任务 
 CallerRunsPolicy  调用者线程(运行提交任务的线程)

线程池原理

提交一个任务到线程池中:

1.判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务.如果核心线程都在执行任务,则进入下个流程.

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

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

转载请注明:文章转载自 www.051e.com
本文地址:http://www.051e.com/it/959717.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 ©2023-2025 051e.com

ICP备案号:京ICP备12030808号