并发
写在前面:
千万不要被目录吓到,其实仔细研究是很简单的。本文中引用了很多比笔者优秀百倍的人的文章。希望大家得以善用。每一篇文章笔者都进行过仔细的研究,觉得很耐人寻味。笔者最想感谢的就是这篇文章中所引用的其他优秀的作者,笔者把他们的出处写在了文章结尾处
Java并发—基础
说到并发,我们就不得不提进程和线程的关系。进程和线程的关系和区别下面是我在知乎上看到的一个很生动的比喻:
- 开个QQ,开了一个进程;开了迅雷,开了一个进程。在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ的运行,那么这“多个工作”分别有一个线程。所以一个进程管着多个线程。通俗的讲:“进程是爹妈,管着众多的线程儿子”
由于进程是由操作系统统一分配资源,所以本文主要以介绍线程为主。
创建线程的两种方法
- Thread类和Runnable接口(Thread类最终也是实现Runnable接口),每个线程都通过执行Runnable对象中的run()方法来开始他的生命周期。其中run方法必须是公有的不带任何参数,没有返回值,且不抛出异常。
- 创建线程后新的线程都将保持在空闲状态,知道调用他的start的方法来唤醒并开始执行目标对象的run方法。在一个线程的生命周期中只能调用一次start方法一旦线程启动之后,将一直保持运行状态,直至目标对象的run方法返回才技术。
使用继承Thread类的方法进行创建线程:
Thread类k看似代码简介其实最终也是实现Runnable接口。而且Thread是一个,调用者只能继承一次。
public class A{
public static void main(String[] args) {
A0 a0=new A0();
A0 a1=new A0();
a0.start();
a1.start();
}
}
class A0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i==50){
try {
Thread.sleep(5000);//线程暂停5000ms后被重新唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
使用实现Runnable接口进行创建线程:
public class B {
public static void main(String[] args) {
B0 b0=new B0();
Thread t0=new Thread(b0);
t0.start();
B0 b1=new B0();
Thread t1=new Thread(b1);
t1.start();
}
}
class B0 implements Runnable {
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
中断线程
- start方法有一个可以永久结束线程的方法–stop(),但是在非特殊情况下不会使用该方法,因为他是一种类似于机动车在高速公路上高速行驶时突然急踩刹车一样,对机动车造成伤害。同样stop方法也会对代码逻辑造成伤害。
- 所以jdk更推荐使用interrupt来中断线程,同样使用interrupt也是有限制的,当线程处在sleep,wait,join状态的时候才被允许用interrupt。interrupt的作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。笔者给大家推荐一个关于详细介绍interrupt的文章。
线程池
经过前面的介绍,相信大家对线程的创建和中断已经不陌生了,但是普通创建线程的方法有一个弊端就是不能进行返回,那么接下来我将会引入一种更加方便的创建线程的方法–线程池。
使用Callable和线程池实现有返回值的线程:
- 有时我们需在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并最终汇总起来,这个时候就需要Callable接口。操作过程如下
1. 创建一个类并实现Callable接口,在call方法中实现具体的运算逻辑并返回结果
2. 创建一个线程池,一个用于接收返回结果的Future List及Callable的线程实例,使用线程池提交任务并将程序执行执行之后的结果保存在Future中,在线程执行后遍历FutureList中的Future对象,在该对象上调用get方法就可以获取Callable线程任务返回的数据并汇总结果。(大家对future或许比较陌生,如果想更加深入了解Future的请大家阅读文章一,文章二)
public class C {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service=Executors.newCachedThreadPool();
List<Future> list=new ArrayList();
for(int i=0;i<10;i++){
C0 c=new C0(i+" ");
Future f=service.submit(c);
list.add(f);
}
service.shutdown();
for(Future f:list){
System.out.println(f.get().toString());
}
}
}
class C0 implements Callable{
String name;
C0(String name){
this.name=name;
}//通过构造器把局部变量变成全局变量
@Override
public Object call() throws Exception {
return this.name+"is ok";
}
}
service.awaitTermination:
awaitTermination方法:接收人timeout和TimeUnit两个参数,用于设定超时时间及单位。
当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。
一般情况下会和shutdown方法组合使用。
public class D {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
service.execute(new Runnable() {//多线程,因为他将循环放在了外面,多次执行 Runnable()对应execute ,Callable对应submit
@Override
public void run() {
System.out.println(Thread.currentThread().getName() );
}
});
}
service.shutdown();
// shutdown方法:平滑的关闭ExecutorService,当此方法被调用时,
// ExecutorService停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。
// 当所有提交任务执行完毕,线程池即被关闭。
try {
service.awaitTermination(10, TimeUnit.SECONDS);//await等待 Termination终止
// awaitTermination方法:接收人timeout和TimeUnit两个参数,用于设定超时时间及单位。
// 当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。
// 一般情况下会和shutdown方法组合使用。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池的原理及作用
Java中的线程池主要用于管理线程组及其运行状态,以便Java虛拟机更好地利用CPU资源。
- Java线程池的的工作原理为******:
JVM先根据用户参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会返回现有可用的线程,进而再次从队列中取出任务并执行。 - 线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数、以保证高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。使用线程池启动线程:处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
线程池的优点
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
-
降低资源消耗。减少了创建和销毁次数,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
线程池的核心组件
- 线程池管理器:用于创建并管理线程
- 工作线程:线程池中执行具体任务的线程
- 任务接口:用于定义工作线程的调用和执行策略,只有线程执行了该接口,线程中的任务才能被线程池调度
- 任务队列:存储待处理的任务,新的任务会不断增加到队列中去,执行完成的任务将被从队列中移除
线程的生命周期
笔者看到过一篇更好文章来介绍线程池,请点击点击此处。
线程池的拒绝策略
线程池中核心线程数被用完而且阻塞队列已经被排满,此时线程池中资源已经耗尽,线程池中没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池中将采用拒绝策略处理新添加的线程任务。
- AbortPolicy:直接抛出异常,阻止线程正常运行
- CallerRunsPolicy:如果被丢弃的任务未关闭,则执行该线程任务
- DiscardOrdersPolicy:移除线程队列中最早的一个线程任务并尝试提交当前任务。
- DiscardPolicy:丢弃当前线程,不做任何处理。目前来看是最好的一种方法。
JDK提供常用的线程池:
ThreadPoolExecutor:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
ForkJoinPool:
- newWorkStealingPool :内部使用多个队列来减少各个线程调度产生的竞争,工作窃取指的是闲置的线程去处理本不属于他的任务。每个处理器核都有一个队列存储着需要完成的任务,对于多核处理机来说,当一个核对应的任务处理完毕后。就可以去帮助其他核处理任务。
使用newFixedThreadPool举例
public class F {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
for(int i=0;i<2;i++){
service.execute(
new Runnable() {
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println(Thread.currentThread().getName()+" :"+i);
}
}
}
);
}
}
}
使用newWorkStealingPool()举例
// 假设共有三个线程同时执行, A, B, C
// 当A,B线程池尚未处理任务结束,而C已经处理完毕,则C线程会从A或者B中窃取任务执行,
// 这就叫工作窃取
// 假如A线程中的队列里面分配了5个任务,而B线程的队列中分配了1个任务,当B线程执行完任务后,它会主动的去A线程中窃取其他的任务进行执行
// WorkStealingPool 背后是使用 ForkJoinPool实现的
public class G {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService service= Executors.newWorkStealingPool();
service.execute(new R(1000));// 我的cpu核数为12 启动13个线程,
//第一个是1s执行完毕,其余都是2s执行完毕,
// 有一个任务会进行等待,当第一个执行完毕后,会再次偷取第十三个任务执行
//看线程 有重复的线程去执行
for(int i=0;i<Runtime.getRuntime().availableProcessors();i++){
service.execute(new R(2000));
}
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class R implements Runnable{
private int time;
public R(int time){
this.time=time;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(time);//InterruptException sleep wait join
System.out.println(Thread.currentThread().getName() + " " + time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果;
使用newScheduledThreadPool()举例
public class E {
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
//第一种
// service.schedule(
// new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName());
// }
// },3, TimeUnit.SECONDS);
// service.shutdown();
// service.awaitTermination(10,TimeUnit.SECONDS);
//第二种
service.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},1, 3,TimeUnit.SECONDS);延迟一秒开始,但是每隔3秒 线程执行
}
}
schedule和scheduleAtFixedRate:
schedule和scheduleAtFixedRate的区别在于,如果指定开始执行的时间在当前系统运行时间之前,scheduleAtFixedRate会把已经过去的时间也作为周期执行,而schedule不会把过去的时间算上。
SimpleDateFormat fTime = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date d1 = fTime.parse("2005/12/30 14:10:00");
t.scheduleAtFixedRate(new TimerTask(){
public void run()
{
System.out.println("this is task you do6");
}
},d1,3*60*1000);
间隔时间是3分钟,指定开始时间是2005/12/30 14:10:00,如果我在14:17:00分执行这个程序,那么会立刻打印3次
this is task you do6 //14:10
this is task you do6 //14:13
this is task you do6 //14:16
并且注意,下一次执行是在14:19 而不是 14:20。就是说是从指定的开始时间开始计时,而不是从执行时间开始计时。
但是上面如果用schedule方法,间隔时间是3分钟,指定开始时间是2005/12/30 14:10:00,那么在14:17:00分执行这个程序,则立即执行程序一次。并且下一次的执行时间是 14:20,而不是从14:10开始算的周期(14:19)。
线程池工作流程:
给线程设定优先级
优先 线程中有10个优先级,数字越大证明优先级越高。MAX_PRIORITY为最大的优先级。
public class H {
public static void main(String[] args) {
H0 h0=new H0();
h0.setName("A");
h0.setPriority(Thread.MAX_PRIORITY);//Priority : 优先 线程中有10个优先级,数字越大证明优先级越高
H0 h1=new H0();
h1.setName("B");
h1.setPriority(Thread.MIN_PRIORITY);
h0.start();
h1.start();
}
}
class H0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:
线程A先于线程B执行的概率要大,
但不是全部的A线程都会在线程B前面。
只能说线程A在前面的多
yield
用sleep的方式使效果更加明显。不用sleep也会有礼让的趋势
public class H {
public static void main(String[] args) {
H0 h0=new H0();
h0.setName("A");
H0 h1=new H0();
h1.setName("B");
h0.start();
h1.start();
}
}
class H0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
yield();//yield的意思是“屈服、礼让”,在程序中表现为当前线程会尽量让出CPU资源来给其他线程执行
try {
Thread.sleep(100);//进程运行的太快了,用sleep使线程慢下来
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:
join
让一个线程去等待另一个线程,直到另一个线程全部执行完成后,才可以执行该线程
public class I{
public static void main(String[] args) {
I0 i0=new I0();
i0.setName("A");
for(int i=0;i<100;i++){
if(i==50){
i0.start();
try {
i0.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class I0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:将主线程调为10,A线程调为1
守护线程:
setDaemon()方法标记一个线程是守护线程
线程分为用户线程和守护线程。
他们的区别在于
- 用户线程:Java虚拟机在所有的用户线程都离开后Java虚拟机才离开。
- 守护线程:他依赖于JVM,于JVM共生死,在JVM中所有的线程都是守护线程时,JVM就可以退出了,如果有一个非守护线程,JVM都不会退出。
守护线程优先级比较低,用于为系统中其他的对象和线程提供服务,比如垃圾回收器是一个经典的守护线程,如果在我们的程序中不再有任何线程运行时,程序就不会产生垃圾,垃圾回收器也就无事可做。垃圾回收器会消灭。守护线程始终在低状态下运行,用于实时监控和管理系统中的可回收资源。
public class J {
public static void main(String[] arrgs) {
J1 j1=new J1();
j1.start();
J2 j2=new J2();
j2.setDaemon(true);//定义为守护线程
j2.start();
}
}
class J1 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"用户:"+i);
}
}
}
class J2 extends Thread{
@Override
public void run() {
for(int i=0;i<1000;i++){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"守护:"+i);
}
}
}
输出结果:(一旦用户线程停止,守护线程也会停止)
线程之间的通信:
一、为什么要线程通信?
-
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
-
当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
-
所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。
二、什么是线程通信?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。
就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
于是我们引出了等待唤醒机制:(wait()、notify())
就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);
(1)wait()方法:
线程调用wait()方法,释放它对锁的拥有权,同时他会在等待的位置加一个标志,为了以后使用notify()或者notifyAll()方法 唤醒它时,它好能从当前位置获得锁的拥有权,变成就绪状态,
要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。 在哪里等待被唤醒时,就在那里开始执行。
(2)notify/notifyAll()方法:
- notif()方法:notify()方法会唤醒一个等待当前对象的锁的线程。唤醒在此对象监视器上等待的单个线程。
- notifAll()方法:notifyAll()方法会唤醒在此对象监视器上等待的所有线程。
下面我将写一个demo来进行演示
我的demo的大致意思是,银行里在进行存取款的时候。只有存钱后,才能取钱。通过线程同步来实现线程之间的通信
- 账户类:
public class Account {
private String no;
private int balance;//本金
private boolean flag;//旗标 false:没有存款 可以存了 true:有存款 可以取了
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public synchronized void saveMoney(int money){//存钱线程调用
if(flag){//有存款
try {
wait();//存钱线程等候
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{//!flag没有存款,可以继续存钱
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance+=money;
System.out.println(this.no+"存了"+money);
flag=true;
notifyAll();//唤醒所有进程
}
}
public synchronized void getMoney(int money){//取钱线程调用
if(!flag){//无存款
try {
wait();//存钱线程等候
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{//flag有存款,可以取钱
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance-=money;
System.out.println(this.no+"取出"+money);
flag=false;
notifyAll();//唤醒所有进程
}
}
}
- 取钱类
public class Get extends Thread {
private Account account;
private int money;
public Get(Account account, int money){
this.account=account;
this.money=money;
}
@Override
public void run() {
this.account.getMoney(money);
}
}
- 存钱类
public class Save extends Thread {
private Account account;
private int money;
public Save(Account account,int money){
this.account=account;
this.money=money;
}
@Override
public void run() {
this.account.saveMoney(money);
}
}
- 测试类
public class Test {
public static void main(String[] args) {
Account account =new Account();
account.setBalance(100);
account.setNo("狗");
for(int i=0;i<10;i++){
Save save=new Save(account,100);
Get get=new Get(account,100);
save.start();
get.start();
}
}
}
输出结果:(注意将代码中的10换成了3)
这个demo很好理解,笔者主要介绍synchronized以及wait和notify这三个关键字,笔者向大家推荐一篇关于synchronized的文章。
我对synchronized进行简单总结一下:
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
sleep和wait的区别*****
- sleep方法属于Thread类,wait属于Object类
- sleep方法暂时让该线程让出CPU给其他线程,但其监控状态依然保持,在指定的时间过后会自动恢复运行状态。在调用sleep方法时,线程不会释放对象锁。
- 在调用wait方法时,暂时让该线程让出CPU给其他线程但是线程会释放对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify()方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。
Java并发–锁
如果大家对锁很感兴趣可以参考一下这篇文章:
锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁锁
Java中的锁主要用于保障并发线程情况下数据的一致性。
- 锁从乐观和悲观角度分别为乐观锁和悲观锁
- 从获取资源公平角度来看又分为公平锁和非公平锁
- 从是否共享资源角度来看又分为共享锁和独占锁
- 从锁的状态又分为偏向锁,轻量级锁和重量级锁
- 同时,在JVM中还使用了自旋锁以更快的使用CPU资源
乐观锁和悲观锁
笔者看多过一个认为很不错的关于乐观锁和悲观锁的文章。
乐观锁
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
Java中乐观锁大部分是通过CAS(Compare And Swap 比较和交换)操作来实现的。
悲观锁
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理
Java中乐观锁大部分是通过AQS( AbstractQueuedSynchronizer 抽象的队列同步器)操作来实现的。
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态到用户态之间的切换进入阻塞,挂起的状态只需在内核态等一等(也叫做自旋),在等待持有锁的线程释放锁后即可立即获得锁,这样就避免了用户在线程在内核状态的切换上导致的锁时间消耗。
线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,有时候还会发生线程永远不会获取锁,而导致CPU别用就占用,所以就需要设定一个自选等待的最大时间。在线程执行的时间超过自选等待的最大时间后,线程会退出自旋模式,并释放持有的锁。
自旋锁的优缺点
- 优点:它可以减少CPU上下文切换,对于占用锁的时间非常短,或者在锁竞争不激烈的代码来说性能大幅度提高,因为自旋的CPU耗时明显少于线程阻塞挂起再唤醒时两次CPU上下文切换所用的时间。
- 缺点:在持有锁的线程占用锁的时间过长或者锁的竞争过于激烈时,线程的自选过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖情况下不适合采用自旋锁。
自旋锁的时间阈值
JDK1.5是固定的自选时间,JDK1.6引入了适应性自旋锁。他的时间不再是固定的,而是由上一次在同一个锁上的自旋时间和锁的拥有者的状态来决定的,可基本认为一个上下文切换的时间就是一个最佳的时间。
synchronized
synchronized用于为Java对象,方法,代码块提供线程安全的操作。synchronized属于独占式的悲观锁,同时属于可重入锁。
在使用synchronized装饰对象时,同一时刻只能有一个线程对该对象进行访问;
在使用synchronized修饰方法,代码块时,同一时刻只能有一个线程对该方法体或代码块进行访问,其他线程只有等待当前线程执行完成并释放资源后才能访问该对象或者执行同步代码块。
Java中每个对象都有个monitor对象,加锁就是在竞争monitor对象对代码加锁是通过在前后加上monitorenter和monitorexit指令来实现的,对方法加锁是通过一个标记来判断的。
synchronized适用范围:
- synchronized作用于成员变量和非静态方法时,锁住的是对象实例,即this对象。
- synchronized作用于静态方法时,锁住的对象是Class实例,因为静态方法属于Class而不属于对象。
- synchronized作用于一个代码块时锁住的的是所有代码块中配置的对象
- 当把方法设为静态时,syn就相当于全局锁,会锁柱所有调用该方法的线程,因此就算是多个对象也不会争抢。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
总结:
1. 无论synchronized关键字加在方法上还是对象上:
如果它作用的对象是非静态的,则它取得的锁是对象;
如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是类,该类所有的对象同一把锁。
2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
//synchronized
public class A {
public static void main(String[] args) {
A0 a0 = new A0();
A0 a1 = new A0();
new Thread(new Runnable() {
@Override
public void run() {
a0.ok1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
a1.ok2();
}
}).start();
}
}
class A0{
void ok1(){//synchronized 锁止同一个对象
synchronized(A.class){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK1执行" + i + "次!");
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized void ok2(){
synchronized(A.class){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK2执行" + i + "次!");
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:(注意代码中的30换成了3)
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,如果没有外部干涉,他们将都无法推进下去。此时称系统处于死锁状态。
//死锁
public class B {
public static void main(String[] args) {
B0 a0=new B0();
new Thread(new Runnable(){
@Override
public void run() {
a0.ok();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
a0.ok1();
}
}).start();
}
}
class B0{
private String lockName1="A";
private String lockName2="B";
void ok(){//synchronized 锁止同一个对象
synchronized (lockName2){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK执行" + i + "次!");
Thread.sleep(300);
synchronized (lockName1){}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void ok1(){
synchronized (lockName1) {
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK1执行" + i + "次!");
Thread.sleep(300);
synchronized (lockName2) {
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程同步
Account类:
public class Account {
private double balance;
public Account(){}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}
}
getMoney类:
一个取钱系统,存入一定量的钱,分多次取出。
public class getMoney extends Thread {
private String name;
private double money ;
private Account account;
public getMoney(String name,double money,Account account){
this.name=name;
this.money=money;
this.account=account;
}
@Override
public void run() {
synchronized (this.account){
if(this.account.getBalance()>=this.money){
System.out.println("吐出"+this.money+"钱");
try {
Thread.sleep(100);
this.account.setBalance(this.account.getBalance()-this.money);
System.out.println("余额剩余"+this.account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Test类:
public class Test {
public static void main(String[] args) {
Account a=new Account();
a.setBalance(500);
for(int i=0;i<5;i++){
getMoney getmoney=new getMoney("甲",100,a);
getmoney.start();
}
}
}
运行结果:
各种锁
ReentrantLock
- ReentrantLock继承了lock接口,是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法)。
- ReentrantLock通过AQS来实现锁的获取与释放
- 独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;
- 可重入锁是指该锁能够支持一个线程对同一个资源执行多次加锁操作。
- ReentrantLock支持公平锁和非公平锁的的实现。
- ReentrantLock之所以被称为可重入锁,是因为它可以反复进入。即允许连续两次获得同一把锁,两次释放同一把锁,但如果不对称会抛出异常。
public class C implements Runnable {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
C c = new C();
Thread t = new Thread(c);
Thread t1 = new Thread(c);
t1.start();
t.start();
}
@Override
public void run() {
try {
lock.lock();
int i = 0;
for (int j = 0; j < 100; j++) {
i++;
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + ":" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
运行结果:
ReentrantLock是如何避免死锁的
-
响应中断:如果有一个线程尝试获取一把锁,有两种情况,一种是获取锁继续执行,另一种是继续等待,而 ReentrantLock还提供了可响应中断的可能,即在等待所的情况下 ,线程可以根据需求取消对锁的请求
-
可轮询锁
-
定时锁
public class D {
ReentrantLock lock = new ReentrantLock();
public void doWork(){
String name=Thread.currentThread().getName();
try{
//响应中断
// if(name.equals("B")){
//
// Thread.currentThread().interrupt();
// }
//可轮询锁
// System.out.println(name+":"+lock.tryLock());
// if(!lock.tryLock()){//获得不到 立刻返回
// return;
// }
// lock.lock();
// lock.lockInterruptibly();//可以通过interrupt方法进行中断锁 不需要通过unlock释放锁,通过interrupt进行自动释放
//定时锁
// if(!lock.tryLock(10, TimeUnit.SECONDS)){//如果在10秒钟内获得了锁,则继续工作
// return;
// }
System.out.println(name+"得到锁");
System.out.println(name+"执行");
for(int i=0;i<2;i++){
Thread.sleep(1000);
System.out.println(name+i+"ok");
}
}catch(Exception e){
System.out.println(name + " 被中断");
System.out.println("因此"+name + "线程可以做些别的事情");
}finally{
if(lock.isHeldByCurrentThread()){//如果锁住了当前线程 释放锁
lock.unlock();
}
System.out.println(name + " 正常释放锁");
}
}
public static void main(String[] args) {
D d=new D();
Thread t1= new Thread(new Runnable() {
@Override
public void run() {
d.doWork();
}
});
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
d.doWork();
}
});
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();
}
}
输出结果
响应中断:
可轮询锁:
定时锁:
ReentrantLock默认支持非公平锁。
如果大家对ReentrantLock公平锁与非公平锁源码有兴趣可以可以参考下面文章。
深入剖析ReentrantLock公平锁与非公平锁源码实现
-
公平锁指的是分配和竞争机制是公平的,既遵循先到先得原则。
-
非公平锁:JVM遵循随机,就近原则分配锁的机制,执行效率明显高于公平锁。
tryLock,lock和lockInterruptibly()的区别
- tryLock:若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待; tryLock还 可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
- lock:若有可用锁,则获取该锁并返回true,否则会一-直等待直到获取可用锁。
- lockInterruptibly:在锁中断是会抛出异常,lock不会。
synchronized和ReentrantLock区别*****
- 共同点如下:
- 都用于控制多线程对共享对象的访问。
- 都是可重入锁。
- 都保证了可见性和互斥行。
- 不同点如下:
- R显示获取和释放锁,必须在finally控制块中进行解锁;
s隐式获取和释放锁。 - R可响应中断,可轮回,为处理锁提供了更多的灵活性。
- R是API级别的,s是JVM级别的。
- R可以定义公平锁。
- 二者底层实现不同,s是同步阻塞,采用的是悲观并发策略; Lock是同步非阻塞,采用的是乐观并发策略。
- Lock是一个接口,而s是Java中的关键字,s是由内置的语言实现的。
- 我们通过Lock可以知道有没有成功获取锁,通过s却无法知道。
- Lock可以通过分别定义读写锁提高多个线程读操作的效率。
- R显示获取和释放锁,必须在finally控制块中进行解锁;
Semaphore
它是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
Semaphore对锁的申请和释放同ReentrantLock类似,通过acquire和release进行获取和释放资源。其中 acquire和R中的lockInterruotibly方法一样,是一种可响应的中断锁,也就是说在等待许可信号资源的过程中可以被Thread. interrupt方法中断而取消对许可信号的申请。
S也实现了可轮询的锁请求,定时锁的功能,以及公平锁与非公平锁的机制。S的释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
//Semphore 通过开取信号量的个数 和申请信号量的个数 来调整是否同时干 还是抢着干
// 开取的个数多,申请的少,抢着干
// 开的个数和申请的个数形同,分别干 !
public class E {
private Semaphore semaphore=new Semaphore(3);//同时开取三个信号量
public void doWork(){
String name=Thread.currentThread().getName();
try {
semaphore.acquire(1);//申请多少信号量。不写个数默认为一。如果开一个信号量,而且有多个线程,那么多个线程争抢一个信号量
System.out.println(name + " 得到锁");
System.out.println(name + " 开工干活");
for(int i=0;i<100;i++){
System.out.println(name +"干活" + i);
}
}catch(Exception e){
System.out.println(name + " 被中断");
System.out.println("因此"+name + "线程可以做些别的事情");
}finally{
semaphore.release(2);
System.out.println(name + " 正常释放锁");
}
}
public static void main(String[] args) {
E d=new E();
Thread t1=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t2=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t3=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t4=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
t3.setName("A");
t2.setName("B");
t1.setName("C");
t4.setName("D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出结果:
代码中是工作100次,输出结果是工作一次。
AtomicInteger原子性
我们知道,在多线程程序中,++i, i++等运算符都不具有原子性,因此不是安全的线程操作
我们可以通过synchronized和Reentrant Lock将该操作变成一一个原子操作,但是它们两个均属于重量级锁。慎用!
因此,JVM为此类原子操作提供了- - 些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger。
//原子 和 静态:原子是线程之间分开执行,静态是多个进程争抢执行
public class A implements Runnable {
// private static int count=0;//累加
private AtomicInteger count=new AtomicInteger(0);//原子
public static void main(String[] args) {
A a=new A();
A a1=new A();
Thread t=new Thread(a);
t.setName("A:");
t.start();
Thread t1=new Thread(a1);
t1.setName("B:");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终值:"+a.count);
}
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+(count.incrementAndGet()));
}
}
}
可重入锁-公平锁和非公平锁
公平锁(Fair Lock):指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
非公平锁(Nonfair Lock): 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时在排到队尾等待。
因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。
Java中的syn和R的lock方法都是非公平锁。
共享锁和独占锁
Java并发包提供的加锁模式分为独占锁和共享锁。
- 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现。
- 共享锁:允许多个线程同时获取该锁,并发访问共享资源ReentrantReadWriteLock中的读锁为共享锁的实现。
ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。
独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性;但是并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。
//读写锁
public class B {
private Map<String,String> map=new HashMap();
private ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
private Lock read=reentrantReadWriteLock.readLock();
private Lock write=reentrantReadWriteLock.writeLock();
private void du(String key){
read.lock();
map.get(key);
read.unlock();
}
private void xie(String key,String value){
write.lock();
map.put(key,value);
write.unlock();
}
public static void main(String[] args) {
}
}
重量级锁,轻量级锁和偏向锁
如果大家对synchronized中重量级锁、偏向锁和轻量级锁的区别有兴趣可以可以参考下面文章。synchronized中重量级锁、偏向锁和轻量级锁的区别
在几乎无竞争的条件下, 会使用偏向锁
在轻度竞争的条件下, 会由偏向锁升级为轻量级锁
在重度竞争的情况下, 会由轻量级锁升级为重量级锁
重量级的synchronized有三个用法:
普通同步方法,锁的是当前实例对象。
静态同步方法,锁的是当前类的class对象。
同步代码块,锁的是synchronized括号里配置的对象。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,偏向锁可以降低多次加锁解锁的开销。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。
轻量级锁和偏向锁总结:
综述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁在某个线程交替执行同步块时进一步提高性能。
因此,锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。
随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但是在Java中锁只能升级,不能降级!
偏向锁,轻量级锁,重量级锁的总结:
分段锁.
分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细力度化, 以提高并发效率。
- ConcurrentHashMap在内部就是使用分段锁实现的。
死锁
在有多个线程同时被阻塞时,它们之间相互等待对方释放锁资源,就会出现死锁。
为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。
如何进行锁优化
- 减少锁持有的时间:减少锁持有的时间是指只在有线程安全要求的程序.上加锁来尽量减少同步代码块对锁的持有时间。
- 减少锁粒度:减少锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减少锁粒度的最典型的案列就是ConcurrentHashMap中的分段锁。
- 锁分离:指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,谢谢互斥,保证了安全,又提高性能。
- 锁粗化:锁粗化为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分的太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。
- 锁消除:在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。
Java并发–高级的使用
CPU利用时间片轮询为没个任务都服务一定的时间,然后把当前任务的状态保存下来,继续服务下一个任务。任务的保存和再加载就叫做线程的上下文切换。
-
上下文:指线程切换时CPU寄存器和程序计数器所保存的当前线程的信息
-
寄存器:指CPU内部容量较小但是速度很快的内存区域(与之相对应的是CPU外部的相对较慢的RAM主内存(RAM和ROM的区别)(对本篇文章的补充:ROM是硬盘的一部分,用来存放硬件信息或操作系统,可读不可写;硬盘的另一部分用于存放用户的个人文件,可读可写))
-
程序计数器:是一个专用的寄存器,用来表示指令系列正在CPU的哪个位置。用来存储正在执行的命令的位置,或者下一个将被执行的指令2的位置,依赖于特定系统。
上下文切换
指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。上下文切换过程中的信息会被保存在进程控制块中(PCB – Process Control Block)
上下文切换的信息会一直被保存在CPU的内存中去直到被再次使用:
流程如下:
- 挂起一个进程,将这个进程在CPU的状态(上下文信息)存储在内存的PCB中。
- 在PCB中检索下一个进程上下文并将其在CPU的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳到进程被中断时的代码行)并恢复该进程
上下文切换示意图:
内部图:
概括图:
引起线程上下文切换的原因
- 当前任务执行完成,系统的CPU正常调度下一个任务
- 当前正在执行的任务遇到I/O等阻塞操作,被调度器挂起,执行下一任务。
- 多个任务并发争抢锁资源,当前任务并没有抢到锁资源,被调度器挂起,执行下一任务
- 用户的代码挂起当前任务执行比如线程执行sleep方法,被动让出CPU
- 硬件中断
Java的阻塞队列
声明:本文主要参考的文章如下:
深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue
- 队列是一种只允许在表的前段进行删除操作,在表的后段进行插入操作的线性表
- 阻塞队列和一般的队列区别在于阻塞队列是“阻塞”的这里的阻塞是一种状态。在阻塞队列中有两种情况
- 消息者阻塞:在队列为空时,消费者端的线程都会被自动阻塞(挂起)直到有新数据放入队列
- 生产者阻塞:在队列已满且没有可用空间时生产者端的线程都会被自动阻塞(挂起),直到队列中有空的位置腾出位置,线程会被自动唤醒并产生数据。
Java中阻塞队列有:
阻塞队列中的接口所提供的主要方法:
-
插入方法:
add(E e) : 添加成功返回true,失败抛IllegalStateException异常
offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞 -
删除方法:
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
take():获取并移除此队列头元素,若没有元素则一直阻塞。 -
检查方法:
element() :获取但不移除此队列的头元素,没有元素则抛异常
peek() :获取但不移除此队列的头;若队列为空,则返回 null。
public interface BlockingQueue<E> extends Queue<E> {
//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
//在成功时返回 true,如果此队列已满,则抛IllegalStateException。
boolean add(E e);
//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
// 将指定的元素插入此队列的尾部,如果该队列已满,
//则在到达指定的等待时间之前等待可用的空间,该方法可中断
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
//将指定的元素插入此队列的尾部,如果该队列已满,则一直等到(阻塞)。
void put(E e) throws InterruptedException;
//获取并移除此队列的头部,如果没有元素则等待(阻塞),
//直到有元素将唤醒等待线程执行该操作
E take() throws InterruptedException;
//获取并移除此队列的头部,在指定的等待时间前一直等到获取元素, //超过时间方法将结束
E poll(long timeout, TimeUnit unit) throws InterruptedException;
//从此队列中移除指定元素的单个实例(如果存在)。
boolean remove(Object o);
}
//除了上述方法还有继承自Queue接口的方法
//获取但不移除此队列的头元素,没有则跑异常NoSuchElementException
E element();
//获取但不移除此队列的头;如果此队列为空,则返回 null。
E peek();
//获取并移除此队列的头,如果此队列为空,则返回 null。
E poll();
下面分别简单介绍一下:
ArrayBlockingQueue:
是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。因为保证公平性会降低吞吐量,所以如果要处理的数据没有先后顺序,则对其可以使用非公平处理的方式。如下:true为公平锁,faLse为非公平锁。(注意:默认为非公平锁)
【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】
- LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。
public class A {
private static LinkedBlockingDeque<String> q=new LinkedBlockingDeque<>();//LinkedList的高配版
public static void main(String[] args) {
A0 a0=new A0("A");
a0.start();
A0 a1=new A0("B");
a1.start();
}
private static void printAll(){
Iterator<String> it=q.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println();
}
private static class A0 extends Thread{
public A0(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<10;i++){
String val=Thread.currentThread().getName()+":"+i;
q.add(val);
printAll();
}
System.out.println("-----------"+q.size());
}
}
}
ArrayBlockingQueue和LinkedBlockingQueue的区别:
-
队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
-
数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
-
由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
-
两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
PriorityBlockingQueue:
一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。(注意:如果两个优先级相同,则不能保证该元素的存储和访问顺序)
public class C {
public static void main(String[] args) {
PriorityQueue<C0> priorityQueue=new PriorityQueue<>();
priorityQueue.add(new C0(5));
priorityQueue.add(new C0(3));
priorityQueue.add(new C0(2));
priorityQueue.add(new C0(4));
while(!priorityQueue.isEmpty()){
System.out.println(priorityQueue.poll());
}
// Iterator<C0> it=priorityQueue.iterator();
//
// while(it.hasNext()){
// System.out.println(it.next());
// }
// Object[] os=priorityQueue.toArray();
// Arrays.sort(os);
// for(Object o :os){
// System.out.println(o);
// }
}
}
class C0 implements Comparable {
public C0(){}
public C0(int number) {
this.number = number;
}
private int number;
private String id;
@Override
public int compareTo(Object o) {
C0 that=(C0)o;
return this.number-that.number;
}
@Override
public String toString() {
return this.number+":";
}
}
输出结果:
DelayQueue:
一个实现PriorityBlockingQueue实现延迟获取的无界队列,队列中的元素必须实现Delayed接口,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询- DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)
Student类:
public class Student implements Delayed {//students实现了Delayed,students就有了Delayed的特性。说明了多态特性
private String name;
//身份证
private String id;
//截止时间
private long endTime;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public long getEndTime() {
return endTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public Student(String name, String id, long endTime) {
this.name = name;
this.id = id;
this.endTime = endTime;
}
private TimeUnit unit=TimeUnit.MILLISECONDS;
@Override
public long getDelay(TimeUnit unit) {
return endTime-System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Student that=(Student)o;
return (int)(this.getDelay(this.unit)-that.getDelay(this.unit));
}
}
netBar类:
public class NetBar implements Runnable {
private DelayQueue<Student> delayQueue=new DelayQueue();
// 上机
private void up(String name,String id,int money){
Student s=new Student(name,id,1000*money+System.currentTimeMillis());
delayQueue.add(s);
System.out.println(name+"进来了"+"上机"+money+"秒;");
}
// 下机
public void down(Student man){
System.out.println("网名"+man.getName()+" 身份证"+man.getId()+"时间到下机...");
}
@Override
public void run() {
while(true){
try {
Student s=delayQueue.take();//和队列里的put类似
down(s);
System.out.println(s.getName()+"下线了");
if(delayQueue.isEmpty()){
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("网吧开始营业");
NetBar bar=new NetBar();
Thread t=new Thread(bar);
bar.up("MIKE","001",10);
bar.up("ROSE","002",2);
bar.up("ALICE","003",5);
t.start();
}
}
SynchronousQueue:
一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
java并发之SynchronousQueue实现原理
public class D {
public static void main(String[] args) {
SynchronousQueue<Integer> q=new SynchronousQueue<>();
new D0(q).start();
new D1(q).start();
}
}
class D0 extends Thread{//生产者
private SynchronousQueue<Integer> q;
public D0(SynchronousQueue q){
this.q=q;
}
@Override
public void run() {
while(true){
int p=new Random().nextInt(100);
try {
Thread.sleep(300);
System.out.println("生产"+p);
this.q.put(p);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class D1 extends Thread{//消费者
private SynchronousQueue<Integer> q;
public D1(SynchronousQueue q){
this.q=q;
}
@Override
public void run() {
while(true){
try {
int o=this.q.take();
System.out.println("消费"+o);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
LinkedTransferQueue:
一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
Consumer类:
public class Consumer implements Runnable {
TransferQueue<String> q;
public Consumer( TransferQueue<String> q){
this.q=q;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"取出生产者生产的元素"+this.q.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Producer类:
public class Producer implements Runnable {
TransferQueue<String> q;
public Producer(TransferQueue<String> q){
this.q=q;
}
private String makeEle(){
return Thread.currentThread().getName()+"生产出【"+new Random().nextInt(100)+"】元素";
}
@Override
public void run() {
while(true){
try {
if(this.q.hasWaitingConsumer()){//有消费者吗
this.q.transfer(makeEle());//把元素投递给消费者并返回true 类似于put
}
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Test类:
public class Test {
public static void main(String[] args) {
TransferQueue<String> q=new LinkedTransferQueue() ;
Producer p=new Producer(q);
Thread t=new Thread(p);
t.setName("Producer:");
t.setDaemon(true);//守护线程
t.start();
for(int i=0;i<10;i++){
Consumer c=new Consumer(q);
Thread t1=new Thread(c);
t1.setName("Consumer:"+i);
t1.setDaemon(true);
t1.start();
try {
// 消费者进程休眠一秒钟,以便生产者获得CPU,从而生产产品
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:
在这里插入图片描述
LinkedBlockingDeque
它是基于链表结构实现的双向阻塞队列,可以在队列的两端分别执行插入和移出元素操作。这样,在多线程同时操作队列时,可以减少一半的锁资源竞争,提高队列的操作
效率。它相比其他阻塞队列,多了addFirst,addLast,offerFirst,offerLast, peekFirst, peekLast等 方法。
以First结尾的方法表示在队列头部执行插入,获取,移除;
以Last结尾的方法表示在队列的尾部执行插入,获取,移除操作。
在初始化它时,可以设置队列的大小以防止内存溢出,双向阻塞队列也常常被用于工作窃取模式。
public class A {
private static LinkedBlockingDeque<String> q=new LinkedBlockingDeque<>();//LinkedList的高配版
public static void main(String[] args) {
A0 a0=new A0("A");
a0.start();
A0 a1=new A0("B");
a1.start();
}
private static void printAll(){
Iterator<String> it=q.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println();
}
private static class A0 extends Thread{
public A0(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<10;i++){
String val=Thread.currentThread().getName()+":"+i;
q.add(val);
printAll();
}
System.out.println("-----------"+q.size());
}
}
}
CountDownLatch:
它是一个同步工具类,允许一个或多个线程一直等待其他线程的操作执行完成后再执行其他相关操作。
它基于线程计数器来实现并发访问控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。其过程为:在主线程中定义CountDownLatch,并将线程计数器的初始值设置为子线程的个数,多个子线程并发执行,每个子线程在执行完毕后都会调用countDown函数将计数器的值减1,直到线程计数器为0,表示所有的子线程任务都已执行完毕,此时再CountDownLatch.上等待的主线程将被唤醒并继续执行
public class B {
private static CountDownLatch countDownLatch=new CountDownLatch(2);
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("子线程1开始");
countDownLatch.countDown();
System.out.println("子线程1结束");
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("子线程2开始");
countDownLatch.countDown();
System.out.println("子线程2结束");
}
}).start();
try {
countDownLatch.await();
System.out.println("两个线程完事了,主线程可以运行了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
CyclicBarrier
是一个同步工具,可以实现让一组线程等待至某个状态之后再全部同时执行。在所有等待线程都被释放之后,它可以被重用。它的运行状态叫做Barrier状态,在调用await之后,线程就处于Barrier状态。
它中最重要的方法就是await方法,说明如下:
public int await(): 挂起当前线程直到所有线程都为Barrier状态再同时执行后序的任务。
public int await(long timeout, TimeUnit unit) :设置一个超时时间,在超时时间过后,如果还有线程未达到Barrier状态,则不再等待,让达到Barrier状态的线程继续执行后续任务。
public class C {
private static CyclicBarrier barrier=new CyclicBarrier(5);
public static void main(String[] args) {
for(int i=0;i<80;i++){
new Business(barrier).start();
}
}
private static class Business extends Thread{
private CyclicBarrier barrier;
public Business(CyclicBarrier barrier){
this.barrier=barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"线程执行前准备工作完成");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"等待其他线程准备工作完成!");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"所有线程准备工作均完成,执行下一项任务!");
}
}
}
输出结果:
上述代码先定义了一个CyclicBarrier,然后循环启动了多个线程,每个线程都通过构造函数将CyclicBarrier传入线程中,在线程内部开始执行第一阶段的工作,比如查询数据等;等到第一阶段的工作处理完成后,再调用CyclicBarri er. await方法等待其他线程也完成第一阶段的工作(CyclicBarrier让一组线程等待到达某个状态再一一起执行);等其他线程也执行完第一一个阶段的工作,便可执行并发操作的下一-项任务,比如数据分发等。
![在这里插入图片描述](https://img-blog.csdnimg.cn/3609![在这里插入图片描述](https://img-blog.csdnimg.cn/f00d7537bcf0494da8cd57d35363d395.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2
Semaphore
Semaphore指信号量,用于控制同时访问某些资源的线程个数,具体做法为通过调用acquire()获取一个许可,如果没有许可,则等待,在许可使用完毕后通过release()释放该许可,以便其他线程使用。Semaphore长被用于多个线程需要共享有限资源的情况,
比如办公室有两台打印机,但是有5个员工需要使用,台打印机同时只能被-一个员工使用,其他员工排队等候,且,只有该打印机被使用完毕并释放后其他员工方可使用,这时就可以通过Semaphore来实现:
public class D {
static Semaphore semaphore=new Semaphore(2);//2个打印机
//static volatile int j=100;
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Worker(i).start();
}
}
private static class Worker extends Thread{
private int num;
Worker(int num){
this.num=num;
}
@Override
public void run() {
try{
semaphore.acquire();
System.out.println("员工"+num+"工作");
System.out.println("员工"+num+"工作完毕!");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:
volatile
static volatile int j=100;//能用在变量前面,不能用在方法前面。
volatile关键字作用:
Java除了使用synchronized保证变量的同步,还是用了稍弱的同步机制,即volatile变量。 volatile也用 于确保将变量的更新操作通知到其他线程。
volatile变量具备两种特性:一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
因为在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
volatile主要适用于一一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取操作!
volatile关键字使用场景:
volatile关键字可以严格保障变量的单次读、写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次操作。volatile在某些时候可以替代synchronized,但是volatile不能完全替代syn的位置,只有在如下特殊情况才适合volatile,比如,必须同时满足下面两个条件才能保证并发环境的安全:
1.对变量的鞋操作不依赖于当前值(比如i++),或者说是淡,村的变量赋值(boolean flag=true) 。
2.该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile
volatile boolean flag=false;
volatile关键字原理:
在有多个线程对普通变量进行读写时,每个线程都首先需要将数据从内存中复制变量到CPU缓存中,如果计算机有多个CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都需要将同一个复制到不同的CPUCache中,这样在每个线程都针对同一个变量的数据做了个不同的处理后就可能存在数据不一致的情况。具体的多线程读写流程如下:
如果将变量声明为volatile的,JVM就能保证每次读取变量时都直接从内存中读取,跳过CPU Cache这一步,有效解决了多线程同步的问题。具体流程如下:
多线程如何共享数据
在Java中进行多线程通信主要是通过共享内存实现的。共享内存有三个关注点:可见性,有序性,原子性。
Java内存模型(JVM)解决了可见性和有序性的问题,而锁解决了原子性的问题。在理想情况下,我们希望做到同步和互斥来实现数据在多线程环境下的一致性和安全性。
实现多线程共享的方式有将数据抽象成一个类,并将对这个数据的操作封装在类的方法中;将Runnable对象作为一个类的内部类,将共享数据作为这个类的成员变量。
- 将数据抽象成一个类,并将对这个数据的操作封装在类的方法中,这种方式只需要在方法上加synchronzed关键字即可以做到数据同步。
在这里对数据操作的方法需要使用synchronized修饰,以保障在多个并发线程访问对象时执行加锁操作,以便同时只有一个线程有权利访问,可以保障数据的一致性
public class A {
private int data=10;
private synchronized void add(){
System.out.println(Thread.currentThread().getName()+":ADD"+(++data));
}
private static class A0 implements Runnable{
private A a;
A0(A a){
this.a=a;
}
@Override
public void run() {
a.add();
}
}
private synchronized void dec(){
System.out.println(Thread.currentThread().getName()+":DEL"+(--data));
}
private static class A1 implements Runnable{
private A a;
A1(A a){
this.a=a;
}
@Override
public void run() {
a.dec();
}
}
public static void main(String[] args) {
A a=new A();
A0 a0=new A0(a);
A1 a1=new A1(a);
for(int i=0;i<2;i++){
Thread t=new Thread(a0);
Thread t1=new Thread(a1);
t.start();
t1.start();
}
}
}
输出结果:
- 每个线程对共享数据的操作方法都被封装在该类的外部类中,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。
public class B {
private static B0 b0=new B0();
public static void main(String[] args) {
for(int i=0;i<20;i++){
new Thread(new Runnable(){
@Override
public void run() {
b0.add();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
b0.dec();
}
}).start();
}
}
}
class B0{
private int data=10;
public synchronized void add(){
System.out.println(Thread.currentThread().getName()+":"+(++data));
}
public synchronized void dec(){
System.out.println(Thread.currentThread().getName()+":"+(--data));
}
}
输出结果:
Java中的线程调度:
抢占式调度
指每个线程都以抢占的方式获取CPU资源并快速执行,在执行完毕后立刻释放CPU资源,具体哪些线程能抢占到CPU资源由操作系统控制,在抢占式调度模式下,每个线程对CPU资源的申请地位是相等的,从概率上讲每个线程都有机会获得同样CPU执行时间片并发执行。
抢占式调度适用于多线程并发执行的情况,在这种机制下一个线程堵塞不会导致整个进程性能下降。具体流程如下:
协同式调度:
协同式调度指某一个线程在执行完成后通知操作系统将CPU资源切换到另一个线程中执行。线程对CPU的持有时间由线程自身控制,线程切换更加透明,更适合多个线程交替执行某些任务的情况。协同式调度有一个缺点:如果其中一个线程因为外部原因运行阻塞,那么可能导致整个系统阻塞甚至崩溃。
Java采用抢占式调度的方式实现内部的线程调度
Java会为每个线程都按照优先级高低分配不同的CPU时间片,且优先级高的线程优先执行。优先级低的线程只是获取CPU的时间片的优先级被降低,但不会永久分配不到CPU时间片。
Java的线程调度在保障效率的前提下尽可能保障线程调度公平性。
线程让出CPU的情况
- 当前运行的线程主动放弃CPU,例如运行中的线程调用yield ()放弃CPU的使用权。
- 当前运行的线程进入阻塞状态,例如调用文件读取I/0操作、锁等待、Socket等待。
- 当前线程运行结束、即允许完run里面的任务。
进度调度算法
最短工作优先(SJF)
最短剩余时间优先(SRTF)
最高响应比优先(HRRF)
优先级调度(Priority)
轮转调度(RR)如果想进一步了解关于进城调度的相关知识,请阅读下面这篇文章:
CAS (Compare And Swap)
指比较并交换。CAS(V,E, N)包含3个参数,V表示要更新的变量,E表示要预期的值,N表示新值。
在且仅在V值等于E值时,才会将V值设为N,如果V值和E值不等,则说明已经有其他线程做了更新,当前线程只能什么也不做。最后,CAS返回当前V的真实值。
CAS特性,乐观锁: CAS操作采用了乐观锁思想,总是认为知己可以成功完成操作。在有多个线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然,也允许失败的线程放弃操作。
对CAS算法的实现有一一个重要的前提,需要取出内存中某时刻的数据,然后再下一时刻进行比较,替换,在这个时间差内可能数据已经发生了变换,这就是ABA问题。ABA指第一个线程从内存的V位置取出A,这时第2个线程也从内存中取出A,并将V位置的数据首先修改为B,接着又将V位置的数据修改为A,这时第一个线程在进行CAS操作时会发现在内存中仍然是A,然后第一个线程操作成功。尽管从第一个线程角度看,CAS操作是成功的,但是在该过程中其实V的数据发生了改变,只是第一个线程并没有感知,这在某些场景下可能出现过程数据不一致问题。
某些乐观锁主要是通过版本号来解决ABA问题,乐观锁每次是在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时都可以执行修改操作,并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。
AQS (Abstract Queued Synchronizer)
是一个抽象的队列同步器,通过维护一个共享资源状态和一个先进先出(FIF0)的线程等待队列来实现一个多线程访问共享资源的同步框架。
AQS原理:它为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,即将该线程放入线程等待队列,等待下一次资源调度,具体流程如下:
写在后面:
写到这里本片文章也就结束了。首先感谢你能读到这里,笔者很希望通过这篇文章能够解决你的问题,给你带来一些帮助。在学习的路上最重要的莫过于持之以恒。当面对一篇长文章还能耐得下性子啃下来,说明你是个很有钻劲的人。你的这一点就是比其他人优秀的地方(当然也肯定远远超过笔者,因为笔者如果看到一篇像这样的长文章也会挠头)。笔者也查了好多资料去比较代码中的某一个关键字的具体用法来完善这篇文章,但由于笔者能力有限,难免有的地方的用词有失偏颇,如发现错误烦请指教,笔者也会不断的去使这篇文章更加完整。
在笔者写这篇文章的时候,笔者看到了一些和笔者想要论述的文章的关联性比较高的且个人认为内容深度很高和易于理解能力的文章,值得大家去慢慢研磨。