三、java多线程与并发原理

1.进程和线程的区别:

进程和线程的由来:

(1)串行:初期的计算机只能串行执行任务,并且需要长时间等待用户输入;

(2)批处理:预先将用户的指令集集中成清单,批量串行处理用户指令,仍然无法并发执行;

(3)进程:进程独占内存空间,保存各自运行状态,相互间不干扰且可以相互切换,为并发处理任务提供了可能;

(4)线程:共享进程的内存资源,相互切换更快速,支持更细粒度的任务控制,时进程内的子任务得以并发执行。

所有与进程相关的资源,都被记录在CPU中;

进程是抢占处理机的调度单位;线程属于某个进程,共享其资源;

线程只由相关堆栈寄存器、程序计数器和线程控制表(TCB)组成

总结:(进程与线程的区别:)

(1)进程是资源分配的最小单位 VS 线程是CPU调度的最小单位

(2)进程可看做独立应用 VS 线程不能看作是独立应用

(3)内存分配:进程有独立的地址空间,相互不影响 VS 线程没有独立的地址空间

(4)包含关系:线程只是进程的不同执行路径

(5)开销方面:进程的切换开销大 VS 线程的切换开销较小

2.线程的start和run方法的区别:

调用start()方法会创建一个新的子线程并启动;如下图(start方法的底层)


而run()方法只是Thread的一个普通方法的调用.

3.Thread和Runnable的关系:

Thread是实现了Runnable接口的类,使得run支持多线程;

因类的单一继承原则,推荐多使用Runnable接口。

public class MyThread extends Thread{private String name;public MyThread(String name){this.name=name;}@Overridepublic void run(){for(int i=0;i<10;i++){System.out.println("Thread start:"+this.name+",i="+i);}}
}public class ThreadDemo{public static void main(String[] args){MyThread mt1=new MyThread("thread1");MyThread mt2=new MyThread("thread2");MyThread mt3=new MyThread("thread3");mt1.start();mt2.start();mt3.start();}
}
结果输出,发现线程是交互执行的;

注:Runnable是没有start方法的,但是Thread的构造方法是可以传入Runnable实例的;

public class MyRunnable implements Runnable{private String name;public MyRunnable(String name){this.name=name;}@Overridepublic void run(){for(int i=0;i<10;i++){System.out.println("Thread start:"+this.name+",i="+i);}}
}public class RunnableDemo{public static void main(String[] args){MyRunnable mr1=new MyRunnable("Runnable1");MyRunnable mr2=new MyRunnable("Runnable2");MyRunnable mr3=new MyRunnable("Runnable3");Thread t1=new Thread(mr1);Thread t2=new Thread(mr2);Thread t3=new Thread(mr3);t1.start();t2.start();t3.start();}
}

4.如何实现处理线程的返回值?(即如何获取子线程的返回值)

实现的方式主要有三种:

(1) 主线程等待法:

public class CycleWait implements Runnable{private String value;public void run(){try{Thread.currentThread.sleep(5000);}catch(InterruptedException e){e.printStaceTrace();}value="we have data now";}public static void main(String[] args){CycleWait cw=new CycleWait();Thread t=new Thread(cw);t.start();System.out.println("value:"+cw.value);}
}
此时,输出的结果为:value:null,即我们并不能精准的控制等待子任务返回结果时才去执行下一个语句。改进(主线程等待法):public static void main(String[] args){CycleWait cw=new CycleWait();Thread t=new Thread(cw);t.start();while(cw.value ==null){Thread.currentThread.sleep(100);}System.out.println("value:"+cw.value);}
输出结果为 value:we have data now"

缺点:需要自己实现循环等待的逻辑,当需要等待的变量多时,代码就会异常的臃肿;且需要循环多久就没有办法控制的。

(2)使用Thread类的join()阻塞当前线程,以等待子线程处理完毕:

 public static void main(String[] args){CycleWait cw=new CycleWait();Thread t=new Thread(cw);t.start();t.join();System.out.println("value:"+cw.value);}

简单便捷,但是力度不够细,无法更精准的控制;

(3)通过Callable接口实现:通过FutureTask Or 线程池获取

a. 下面是使用FutureTask来获取子线程返回值的:

public calss MyCallable implements Callable<String>{@Overridepublic String call(){String value="test";System.out.println("Ready to work");Thread.currentThread.sleep(5000);System.out.println("task down");return value;}
}//FutureTask实现了RunnableFuture接口(RunnableFuture继承Runnable和Future),关注其传入参数Callable实例的构造方法、get()方法【用来阻塞当前线程,知道call方法执行完毕为止】、get(timeout)【加入超时机制,超时则抛出异常】、isDown()方法【判断call()方法是否执行】public calss FutureTaskDemo{public static void main(String[] args){FutureTask<String> task=new FuturnTask<String>(new MyCallable());new Thread(task).start();if(!task.isDown()){System.out.println("task has not finished,please wait!");}System.out.println("task return:"+task.get());}
}
输出:
task has not finished,please wait!
Ready to work
task down
task return:test

b. 下面使用线程池来获取子线程的返回值:

public calss ThreadPoolDemo{public static void main(String[] args){ExecutorService newCachedThreadPool=Executors.newCachedThreadPool();Future<String> future=newCachedThreadPool.submit(new MyCallable());if(!future.isDown()){System.out.println("task has not finished,please wait!");}try{System.out.println(future.get());}catch(InterruptedException e){e.printStackTrace();}catch(ExceptionException e){e.printStackTrace();}finally{newCachedThreadPool.shutdown();}}
}
输出:
task has not finished,please wait!
Ready to work
task down
task return:test

5.线程的状态:

(1)新建new:创建后尚未启动的线程的状态;

(2)运行runnable:包含正在运行running和等待CPU分配的ready;

(3)无限期等待waiting:不会被分配CPU执行时间,需要显示被唤醒;

以下方法会使线程处于无限期等待waiting状态:没有设置Timeout参数的Object.wait()
没有设置Timeout参数的Thread.join()
LockSupport.park()

(4)限期等待timed_waiting: 在一定时间后会有系统自动唤醒;(notify,notifyAll)

以下方法会使线程处于限期等待timed_waiting状态:Thread.sleep()
设置了Timeout参数的Object.wait()
设置了Timeout参数的Thread.join()
LockSupport.parkNanos()
LockSupport.parkUntils()

(5)阻塞blocked:等待获取排它锁;

(6)结束terminated:已终止线程的状态,线程已经结束执行。

(线程运行完run方法或者主线程执行完毕后,即使线程还存活,当时不能重新启动,会抛出java.lang.IllegalThreadExeception异常)

6.sleep和wait的区别:

(1)基本的差别:

sleep是Thread类中的方法,而wait是Object类中的方法;

sleep方法可以在任何地方使用,但是wait方法只能在Synchronized方法或者Synchronized块中使用;(这是由其本质区别所决定的)

(2)最主要的本质区别:

Thread.sleep只会让出CPU,不会导致锁行为的改变;

Object.wait不但会让出CPU,而且会释放已经占有的同步资源。

举一个实际的例子说明:

public class WaitSleepDemo{public static void main(String[] args){final Object lock=new Object();new Thread(new Runnable(){@Overridepublic void run(){System.out.println("thread A is waiting to get lock");synchronized(lock){try{System.out.println("thread A get lock");Thread.sleep(20);System.out.println("thread A do wait method");lock.wait(1000);System.out.println("thread A is done");}catch(InterruptedException e){e.printStackTrace();}}}}).start();try{Thread.sleep(10);}catch(InterruptedException e){e.printStackTrace();}new Thread(new Runnable(){@Overridepublic void run(){System.out.println("thread B is waiting to get lock");synchronized(lock){try{System.out.println("thread B get lock");Thread.sleep(20);System.out.println("thread B is done");}catch(InterruptedException e){e.printStackTrace();}}}}).start();}}输出的结果是:
thread A is waiting to get lock
thread A get lock
thread B is waiting to get lock
thread A do wait method
thread B get lock
thread B is done
thread A is done

7.notify和notifyAll的区别:

(1)notifyAll 会让所有在等待池中的线程全部进入锁池,去竞争获取锁的机会,进入锁池但是没有获取锁的线程仍然会呆在锁池中,不会返回等待池;

(2)notify 只会随机选取一个处于等待池中的线程进入锁池,去竞争获取所得机会。

上面的代码中如果将 thread A的 lock.wait(1000) 替换成 lock.wait(),那么thread A将进入无限期等待的状态,必须手动停止等待,这时就需要在 thread B执行执行结束后,调用 lock.notify() 或者 lock.notifyAll().

引入:两个概念:(这两者都是针对于对象的)

锁池EntryList:

假设线程A已经拥有了某个对象(而不是类)的锁,而其他线程B,C想调用这个对象的某个Synchronized方法(或者块),由于B,C线程在进入该方法(或者块)之前,必须先获得该对象锁的拥有权,而恰巧这个对象的锁正被线程A占用,此时,B,C线程被阻塞,需要进入一个地方等待锁的释放,这个地方就是对象的锁池。

等待池WaitSet:

假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。

8.yield函数:

当调用Thread.yield()函数时,会给线程调度器一个当前线程一个暗示:表示愿意让出CPU使用,但是线程调度器可能会忽略这个暗示,也可能接收这个暗示。(给优先级相同或者优先级更高的线程机会)

9.interrupt函数:

如何中断线程?

  • 已经被抛弃的方法:

通过调用stop()方法停止线程;(但是这种方法太暴力,是不安全的,线程A不知道要中断的线程B中的情况,会导致线程B的一些清理工作无法完成;或者线程B会马上释放锁,会引发线程不同步的问题)

通过调用suspend()和resume()方法。

  • 目前正在使用的方法:

调用interrupt(),通知线程应该中断了:

a. 如果线程处于阻塞状态,那么线程将立即退出被阻塞状态,并抛出InterruptedException异常;

b. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true;被设置为中断标志的线程将继续正常运行,不受影响。

因此,interrupt并不能直接中断线程,需要被调用的线程配合中断。

小结(线程状态图):

10. synchronized的底层原理:

  • 线程安全问题的主要诱因:

存在共享数据(也成临界资源);

存在多条线程共同操作这些共享数据。

  • 解决问题的根本方法:

同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。

  • 互斥锁的特性:

互斥性:即在同一时间只允许一个线程持有某一个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问,互斥性也称作操作的原子性。

可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一线程是可见的(即在获得锁时应获得共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

注意:synchronized锁的不是代码,而是对象。

  • 根据获取锁的分类:获取对象锁和获取类锁

a. 获取对象锁的两种用法:

—同步代码块:synchronized(this),synchronized(类实例对象),锁是小括号()中的实例对象;

—同步非静态方法:synchronized method,锁是当前对象的实例对象;

b. 获取类锁的两种用法:

—同步代码块:synchronized(类.class),锁是小括号()中的类对象(Class对象);

—同步静态方法:synchronized static method,锁是当前对象的类对象(Class 对象);

  • 对象锁和类锁的关系进行总结:

(1)有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;

(2)若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问同步代码块的线程会被阻塞;

(3)若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问同步方法的线程会被阻塞;

(4)若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另外一个访问对象的同步方法的线程会被阻塞,反之亦然;

(5)同一个类的不同对象的对象锁互不干扰;

(6)类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只能有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;

(7)类锁与对象锁互不干扰。

synchronized底层实现原理:

实现synchronized的基础:java对象头、Monitor

Hospot对象的内存中的布局:对象头、实例数据、对齐填充

1) 对象头

一般情况下,synchronized使用的锁对象是存储在对象头里面的,其主要结构是由Mark Word和Class Metadata Address组成:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 默认存储对象的hashCode,分代年龄、锁类型、锁标志位等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据

Mark Word: 是实现轻量级锁和偏向锁的关键;由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑带JVM的空间效率,Mark Word 被设计成一个非固定的数据结构,以便存储更多有效的数据。

如下表(MarkWord):

2) Monitor:

每个java对象天生自带了一把看不见的锁,叫做内部锁,或者Monitor锁(也成为管层,或者监视器锁);我们可以理解为一个同步工具,或者一种同步机制,通常它被描述为一个对象。

Monitor可以有多种实现方式:可以和对象一起创建销毁,或者在我们试图获得对象锁时自动生成;

monitor被某个线程使用后,它便处于锁定状态。JVM中Monitor是由ObjectMonitor实现的,在ObjectMonitor.class文件中是通过C++来实现的。

打开hospot源码:https://hg.openjdk.java.net…src/share/vm/runtime/ObjectMonitor.hpp

ObjectMonitor(){_header          =NULL;_count           =0;_waiters        =0;_recuraions     =0;_object         =NULL;_owner           =NULL; //指向持有ObjectMonitor对象的线程_WaitSet        =NULL; //等待池,每个对象锁的线程否会被封装成ObjectWaiter,来保存到里面_WaitSetLock   =0;_Responsible    =NULL;_succ            =NULL;FreeNext     =NULL;_EntryList       =NULL; //锁池_SpinFreq       =0;_SpinClock      =0;OwnerIsThread   =0;_previous_owner_tid     =0;
}

Monitor锁的竞争、获取与释放:

语言表述其过程:当多个线程同时访问同一段同步代码时,首先会进入EntryList集合里,当线程获取到对象的Monitor锁之后,就进入到object区,并把Object中的owner对象设为当前线程,同时monitor中的计数器count就会加1,若线程调用wait方法,将释放当前持有的monitor,owner就恢复成NULL,count也会被减1;同时,该线程即Object实例就会被计入到WaitSet集合中,等待被唤醒;若当前线程执行完毕,那么它也将释放Monitor锁,并复位对应变量的值,以便其他线程进入获取Monitor锁

图示表示:(Monitor)

由上可知,Monitor对象存在与每个对象的对象头中,synchronized就是通过这种方式获得锁的,这就是JAVA中任意对象都可以作为锁的原因

什么是重入?

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态;但当一个线程再次请求自己持有对象锁的临界资源时,这种情况就叫做重入,请求将会成功; Java中synchronized是基于原子行的内部锁机制,是可重入的;因此,在一个线程调用synchronized方法的同时,在其方法体内部调用该对象的另一个synchronized方法,是可以重入的。

11. synchronized性能的提升:

早期版本中,synchronized属于重量级锁,依赖Mutex Lock实现;线程之间切换需要从用户态切换到核心态,开销太大;

java6 之后,synchronized性能得到了很大的提升, 如下:

(1)Adaptive Spinning(自适应 自旋)

-----自旋锁:

许多情况下,共享数据的锁定状态持续时间较短,为了这段时间去挂起或者阻塞线程并不值得;如今多处理器环境下,完全可以让另一个没有获取到所得线程在门外等一会儿(即执行忙循环等待锁的释放,但不放弃CPU的执行时间),这个行为即为"自旋";

缺点:若锁被其他线程长时间占用,会带来许多性能上的开销。

因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的尝试次数,仍然没有获得到锁的话,就应该使用传统的方式去挂起线程,用户可以根据PreBlockSpin参数去更改,但是将这个参数设置一个合理的值是比较困难的,因此,有了自适应自旋锁的出现。

------自适应自旋锁:

自选的次数不再固定,由前一次同一个锁上的自旋时间及锁的拥有者状态来决定。

(2)Lock Eliminate(锁消除)

更彻底的优化;

JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁;

例如:

public class StringBufferWithoutSync{public void add(String str1,String str2){StringBuffer sb=new StringBuffer();sb.append(str1).append(str2);}
}在上面的例子中,StringBuffer 是线程安全的;并且在这个add()方法中并没有return,即sb只会在append方法中使用,不可能被其他线程引用;因此sb属于不可能共享的资源,JVM会自动消除内部的锁。

(3)Lock Coarsening(锁粗化)

另一个极端:原则上我们都希望,在加同步锁的时候,尽可能的将同步块的作用范围限制到尽量小的范围,即只在共享数据的同步应用中进行同步,这样做的目的是让同步的数据量尽可能变小,让等带锁的线程今早的拿到锁;但是如果存在一连串代码,对同一个对象反复加锁,解锁,甚至加锁操作处于循环体中,那么即使没有线程竞争,频繁的进行互斥同步锁操作也会导致性能问题;

解决方法:通过扩大加锁的范围,避免反复加锁和解锁;

例如:

public class CoarseSync{public static String copyString100Times(String target){StringBuffer sb=new StringBuffer();while(i<100){sb.append(target);}}
}JVM会将append锁粗化到整个while代码块外面,只需要加一次锁即可。

(4)Biased Locking(偏向锁):为了减少同一线程多次获得锁的代价

使用场景:大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得;

核心思想:

如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变为了偏向锁结构,当线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查:Mark Word的锁标记位为偏向锁、以及当前线程Id等于Mark Word 的ThreadID即可,这样就省去了大量有关锁申请的操作

CAS(compare and swap)

缺点:不适用于竞争比较激烈的多线程场合。

(5)Lightweight Locking(轻量级锁)

轻量级锁是由偏向锁升级而来的,偏向锁运行的一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

适应的场景: 线程交替执行代码块;

若存在多个线程(超过两个)同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁;

  • 锁的内存语义:

当线程释放锁时,java内存模型会把该线程对应的本地内存中的共享变量刷到主内存中;

而当线程获取锁时,java内存模型会把线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

  • synchronized 的四种状态:

无锁、偏向锁、轻量级锁、重量级锁(会随着竞争情况逐渐升级)

锁膨胀的方向:无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁

汇总表如下:

优点 缺点 使用场景
偏向锁 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的所撤销的消耗 只有一个线程访问同步代码块或者同步方法的场景
轻量级锁 竞争的线程不会阻塞,提高了响应速度 若线程长时间抢不到锁,自旋会消耗CPU性能 线程交替执行同步代码块或者同步方法的场景
重量级锁 线程竞争不会使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢,在多线程的情况下,频繁的获取释放锁,会带来巨大的性能消耗 追求吞吐量,同步代码块或者同步方法执行时间较长的场景

12. synchronized和ReentrantLock的区别:

  • ReentrantLock(再入锁):

java5之后出现,语义和synchronized相同

位于java.util.concurrent.locks包(J.U.C)

能够实现比synchronized更细粒度的控制,如控制fairness;

调用lock()之后,必须调用unlock()释放锁;

性能未必比synchronized高,并且也是可重入的;

  • ReentrantLock的公平性设置:

ReentrantLock fairLock=new ReentrantLock(true);

参数为true时,倾向于将锁赋予等待时间最久的线程

公平锁:获取锁的顺序,按先后调用lock()方法的顺序(慎用);类似与"排队打饭";

非公平锁:抢占锁的顺序不一定,看运气;

synchronized是非公平锁。

注意:通用场景中公平性未必有想象的那么重要,java默认的调用策略很少会导致饥饿情况的发生,若要保证公平性则会导致额外的开销,导致吞吐量下降,所以建议只有程序确实有公平性需要的时候再使用它。

public calss ReentrantLockDemo{private static ReentrantLock lock=new ReentrantLock(true);@Overridepublic void run(){while(true){try{lock.lock();System.out.println(Thread.currentThread().getName()+"get lock");Thread.sleep(1000);}catch(Exception e){e.printStackTrace();}finally{lock.unlock();}}}
}
  • ReentrantLock将锁对象化

可以判断是否有线程、或者某个特定线程在排队等待获取锁;

带超时的获取锁的尝试;

可以感知有没有成功获取锁;等等

  • 总结:(synchronized和ReentrantLock的区别)

synchronized是关键字,ReentrantLock是类;

synchronized是非公平锁,ReentrantLock可以通过参数true设置所得公平性;

ReentrantLock可以对获取锁的等待时间进行设置,避免死锁;

ReentrantLock可以获取各种锁的信息

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

机制:synchronized操作对象头中的Mark Word, lock调用Unsafe类的park()方法;

13.JMM的内存可见性:

java内存模型(JAVA memory model,简称JMM):本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中的各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式

  • JMM中的主内存和工作内存:

(1)JMM中的主内存:

存储java实例对象,包括成员变量、类信息、常量、静态变量等;

属于数据共享的区域,多线程并发操作时会引发线程安全问题;

(2)JMM的工作内存:

存储当前方法的所有本地变量信息、共享变量的副本,本地变量对其他线程不可见;

字节码行号指示器、Native方法信息;

属于线程私有的数据区域,不存在线程安全问题。

  • JMM与java内存区域划分是不同的概念层次:

JMM是一种抽象的概念,描述了一组规则:即规范了主内存和工作内存中变量的访问方式,围绕原子性,有序性,可见性展开;

相似点:都有共享区域和私有区域

  • 主内存与工作内存的数据存储类型以及操作方式归纳:

(1)方法中的基本数据类型本地变量将直接存储在工作内存的栈帧结构中;

(2)引用变量的本地变量,引用存储在工作内存中,实例存储在主内存中;

(3)实例对象中的成员变量、static变量、类信息俊辉被存储在主内存中;

(4)主内存数据共享的方式是:线程各拷贝一份数据到工作内存中,操作完成后再将工作内存中的数据刷新回主内存。

把数据从内存,加载到缓存、寄存器,然后运算结束写回主内存;当线程共享变量的时候,情况就变得复杂了,试想一下如果处理器对某个变量进行了修改,可能只是体现在该内核的缓存里,这是个本地状态,而运行在其他内核上的线程可能加载的是旧状态,这个很肯导致一次性的问题;从理论上说多线程问题引入了复杂的数据依赖性,不管编译器、处理器怎么做都必须保证数据的依赖性,否则就打破了数据的正确性,这就是JMM所要解决的问题

JMM是如何解决可见性问题的呢?

JMM的内部的实现通常是依赖于内存的屏障通过禁止某些重排序的方式,提供内存可见性保障,即确保happens-before原则。需要确保各种编译器、服务器能够保证一致的行为。

掌握下面4个知识点:

1)指令重排序需要满足的条件:无法通过happens-before原则推导出来的,才能进行指令的重排序

2)内存屏障(Memory Barrier): 是CPU指令;保证特定操作的执行顺序;保证某些变量的内存可见性;

3)happens-before:A操作的结果需要对B操作可见,则A与B必须存在happens-before关系;happens-before是判断数据是否存在竞争,线程是否安全的主要依据;

  • happens-before的八大原则:

(1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先发生于书写在后面的操作;(单线程)

(2)锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;

(3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

(4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则操作A就先行发生于操作C;

(5)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;

(6)线程中断规则:对线程的interrupt()方法的调用先行发生于被中断的线程的代码检验到中断事件的发生;

(7)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;

(8)对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

  • happens-before的概念:

如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对着两个操作进行重排序;

如果操作Ahappens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

private int value =0;
public void write(int input){value=input;
}
public int read(){return value;
}//这段代码不是线程安全的,可以在value变量前面添加volatile,或者加入synchronized锁。

4)volatile:是JVM提供的轻量级同步机制;在并发编程中很常用,但也容易被滥用;

volatile的可见性:保证volatile修饰的共享变量对所有线程总是可见的;禁止指令的重排序优化;

public class VolatileVisibility{public static volatile int value=0;public static void increase(){value++;}
}上面这段代码会引发线程安全问题,因为如果存在两个线程调用这个increase方法,那么在第一个线程读value,+1还没有赋值给value的过程中;另外一个线程同时在读value,因此数据不同步。改进如下:
public class VolatileVisibility{public static int value=0;public synchronized static void increase(){value++;}
}synchronized保证了内存屏障,会将所有工作内存中的数据刷到主内存中,等待下一个线程读取。因此这种情况下,完全可以省略volatile关键字
  • volatile变量为何立即可见?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;

当读取一个volatile变量时,JMM会把线程对应的工作内存设置为无效,直接去主内存中读取;

  • volatile如何禁止重排优化?

(1)通过插入内存屏障指令,禁止在内存屏障前后的指令执行重排序优化。

(2)强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。

小结:volatile是通过内存屏障,解决其可见性问题,以及禁止重排优化。

单例的双重检测实现:
public class Singleton{private static Singleton instance;private Singleton(){}public static Singleton getInctance(){//第一次检测if(instance==null){//同步synchronized(Singleton.class){//多线程环境下可能会出现问题的地方if(instance==null){instance=new Singleton();}}}return instance;}
}上面的代码仍然存在问题,使用javap -verbose反汇编可以看到:
memory=allocate//1.分配对象内存空间
instance(memory);//2.初始化对象
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null实际运行时会重排序:
memory=allocate//1.分配对象内存空间
instance=memory;//2.设置instance指向刚分配的内存地址,此时instance!=null
instance(memory);//3.初始化对象改进上面的代码为:
public class Singleton{private volatile static Singleton instance;private Singleton(){}public static Singleton getInctance(){//第一次检测if(instance==null){//同步synchronized(Singleton.class){//多线程环境下可能会出现问题的地方if(instance==null){instance=new Singleton();}}}return instance;}
}
  • volatile和synchronized的区别:

(1)volatile本质上是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住,直到该线程完成变量操作为止;

(2)volatile仅使用在变量级别,synchronized则可以使用在变量、方法和类级别;

(3)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized可以保证变量修改的可见性和原子性;

(4)volatile不会造成线程阻塞;synchronized可能会造成线程的阻塞;

(5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以别编译器优化。

14. CAS (Compare and Swap):

synchronized是悲观锁(独占锁),始终相信会发生并发冲突;因此,会屏蔽一切可能违反数据完整性的操作;除此之外,还有乐观锁,它假设不会发生并发冲突;因此,只在提交数据时检查数据完整性。如果提交失败则会进行重试。乐观锁最常见的就是CAS.

CAS是一种高效实现线程安全的方法,支持原子更新操作适用于计数器、序列发生器等场景属于乐观锁机制,号称lock-free(实际底层还是有锁的);CAS操作失败时有开发者决定是是继续尝试,还是执行别的操作。(因此争锁失败的线程不会被阻塞挂起)。

  • CAS思想:

包含三个操作数:内存位置(V)、预期原值(A)、新值(B)

执行CAS操作时,先比较内存位置的值(即主内存的值)和预期原值,如果相匹配,则处理器会自动将该位置的值更新为新值;否则处理器不做任何操作。

CAS操作的应用场景例子:当一个线程需要修改共享变量的值,完成这个操作:先取出共享变量的值赋值给A,然后,基于A的基础进行计算,得到性质B,需要更新共享变量的值时,就可以调用CAS方法去更新。

了解:CAS多数情况下对开发者来说是透明的:

J.U.C的atomic包提供了:常用的原子性数据类型、引用和数组等相关原子性类型、更新操作工具,是很多线程安全程序的首选;

Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患;java9之后,可以使用 Variable Handle API来替代Unsafe.

CAS缺点:

若循环时间长,则开销很大;

只能保证一个共享变量的原子操作;(如果有多个共享变量,就需要synchronized)

ABA问题(中间被改变过的变量,会被误认为从来没有改变过)。解决:AtomicStampedReference,保存变量值的版本

15.java线程池:

  • 线程池的出现:

在web开发中,服务器需要接收并处理请求,会为一个请求分配一个线程来对其进行处理;当并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,会大大降低系统的效率,可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间,要比处理实际的用户请求的资源更多。希望有一种方法重复的利用和管理线程,这就是线程池。

  • 利用J.U.C包下面的Exectors创建不同的线程池满足不同场景的需求,如下是几种常用的线程池

(1)newFixedThreadPool(int nThreads):指定工作线程数量的线程池;

(2)newCachedThreadPool(): 处理大量短时间工作任务的线程池,

1.试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
2.如果线程闲置的时间超过阈值(一般是60秒),则会终止并移除缓存;
3.系统长时间闲置的时候,不会消耗什么资源。

(3)newSingleThreadExecutor(): 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它;

(4)newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int nThreads): 定期或周期性的工作调度,两者的区别在于单一工作线程、还是多个线程;

(5)newWorkStealingPool(): jdk 8 引入的,内部会构建ForkJoinPool,利用work-stealing算法,并行地处理任务,不保证处理顺序。

简单了解:

Fork/Join框架:(java7 提出,其目的在于能够使用所有的运算能力提高性能)把大任务分割成若干小任务并执行,最终汇总每个小任务结果后得到大人物结果的框架;

Work-Stealing算法:某个线程从其他队列(双端队列的尾部)里窃取任务来执行;

  • 为什么要使用线程池?(线程池的优势)

降低资源消耗;(新建和销毁造成的消耗,重复利用)

提高线程的可管理性;(线程的稀缺资源,无限制的创建线程,会降低系统的稳定性;使用线程池可以对线程进行分配、调优、监控)

Executor的框架:

通过查看J.U.C (java.util.concurrent)中Executors的源码,可以知道:上面的几种线程池都是继承自AbstractExecutorService/ExecutorService/Executor

Executor的框架如图:根据一组执行策略调度执行和控制的异步任务框架,目的是提供一种将任务提交与任务运行分离开来机制

  • J.U.C的三个Executor接口:

(1)Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;

Thread t=new Thread();
t.start();Thread t=new Thread();
Executor.execute(t);

(2)ExecutorService: 扩展添加了具备管理执行器和任务生命周期的方法,提交机制更加完善(比较重要的方法是submit(Callable))

(3)ScheduledExecutorService: 支持Future和定期执行任务。

  • ThreadPoolExecutor:

线程池工作流程如下图(ThreadPoolExecutor):

ThreadPoolExecutor的构造函数(参数):

(1)corePoolSize:核心线程数量;

(2)maxmumPoolSize:线程不够用时,能够创建的最大线程数;

(3)workQueue:任务等待队列;

(4)keepAliveTime:允许的空闲时间,抢占的顺序不一定,看运气;

(5)threadFactory:创建新线程,Executors.defaultThreadFactory,(会使新创建的线程有相同的优先级,且设置了线程名称);

(6)handler:线程池的饱和策略,如果阻塞队列满了,并且没有空闲的线程,如果这时继续提交任务,就需要才去定一种策略处理该任务;线程池提供了4中策略:

1.AbortPolicy:直接抛出异常,这是默认策略;
2.CallerRunsPolicy: 用调用者所在的线程来执行任务;
3.DiscardOlderPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;

也可以通过实现RejectExecutorHandler接口的自定义handler。

  • 新任务提交后,executor执行后的判断:

如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;

如果运行的线程大于等于corePoolSize且小于maxmumPoolSize,则只有当workQueue满时,才能创建新的线程去处理任务;

如果设置的corePoolSize和maxmumPoolSize相等,则创建的线程池的大小事固定的,这时如果有新的任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲线程去workQueue中取任务并处理;

如果运行的线程数量大于等于maxmumPoolSize,这是如果workQueue已经满了,则通过handler所指定的策略来处理任务。

一种优雅的存值方式:

线程池用来管理线程,有一个地方用来存储当前有效的线程数,ThreadPoolExecutor将状态值和有效线程数合二为一,存储在AtomicInteger ctl变量中,方法runState用来获取状态值;workerCountOf()用来获取有效线程数,ctlOf()用来获取这两者;

  • 线程池的状态:

(1)RUNNING: 能接受新提交的任务,并且也能处理阻塞队列中的任务;

(2)SHUTDOWN:不在能接受新提交的任务,但可以处理存量任务;(RUNNING 时的线程池调用shutdown方法,会进入这个状态)

(3)STOP: 不能接受新提交的任务,并且也不能处理阻塞队列中的任务;(RUNNING 时的线程池调用shutdownnow方法,会进入这个状态)

(4)TIDYING: 所有的任务都已终止,正在进行最后的打扫工作(有效线程数为0)。

(5)TERMINATED:调用terminated()方法,就进入TERMINATED状态.(作为一个标识)

状态转换图如下(线程池状态转换图):

  • 线程池的大小如何选定?(根据经验总结)

CPU密集型:线程数=按照 “核数” 或者 “核数+1” 设定;

I/O密集型(即有较多等待任务):线程数=CPU核数 *(1+平均等待时间/平均工作时间)

java多线程与并发原理相关推荐

  1. Java多线程与并发-原理

    一.synchronized 线程安全问题的主要诱因 1.存在共享数据(也称临界资源) 2.存在多条线程共同操作这些共享数据 解决问题的根本方法: 同一时刻有且只有一个线程在操作共享数据,其他线程必须 ...

  2. Java多线程与并发相关 — 原理

    Java多线程与并发相关 - 原理 一 synchronized同步 1. 线程安全问题的主要诱因? 存在共享资源(也称临界资源); 存在多条线程共同操作这些共享数据; 2. 解决办法. 同一时刻有且 ...

  3. Java多线程与并发系列从0到1全部合集,强烈建议收藏!

    在过去的时间中,我写过Java多线程与并发的整个系列. 为了方便大家的阅读,也为了让知识更系统化,这里我单独把Java多线程与并发的整个系列一并罗列于此,希望对有用的人有用,也希望能帮助到更多的人. ...

  4. java书籍_还搞不定Java多线程和并发编程面试题?你可能需要这一份书单!

    点击蓝色"程序员书单"关注我哟 加个"星标",每天带你读好书! ​ 在介绍本书单之前,我想先问一下各位读者,你们之前对于Java并发编程的了解有多少呢.经过了1 ...

  5. JAVA Java多线程与并发库

    Java多线程与并发库 同步方式 import javax.xml.stream.events.StartDocument;public class TestSynchronized {public ...

  6. JAVA多线程和并发面试问题

    转载自   JAVA多线程和并发面试问题 Java多线程面试问题 1.进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程 ...

  7. JAVA多线程和并发基础面试问答(转载)

    JAVA多线程和并发基础面试问答 原文链接:http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-ans ...

  8. Java多线程与并发库高级应用--18_传智播客_张孝祥_java5阻塞队列的应用

    Java多线程与并发库高级应用--18_传智播客_张孝祥_java5阻塞队列的应用 原创:徐工 2018-5- 5 17.10 package cn.itcast.heima2; import jav ...

  9. java多线程与并发_漫画 | Java多线程与并发(一)

    1.什么是线程? 2.线程和进程有什么区别? 3.如何在Java中实现线程? 4.Java关键字volatile与synchronized作用与区别? volatile修饰的变量不保留拷贝,直接访问主 ...

最新文章

  1. 个人学习某个系统或平台的3问式的整理和细化指引
  2. AndroidAnnotations框架简单使用方法
  3. java链接mysql输出查询_用java做网站,java连接数据库并查询输出到页面
  4. 配置React项目的运行环境
  5. 深度学习(4)手写数字识别实战
  6. 这么简单的bug,你改了2天?
  7. java多线程做一件事_关于Java的十件事
  8. Oracle最新的Java 8更新破坏了您的工具-它是如何发生的?
  9. 重学java基础第十五课:java三大版本
  10. java类加载器_java底层内功 第一章,类加载器的任性
  11. Thinking In Java 读书笔记
  12. php 文件hash,PHP HASH算法实现代码分享
  13. Git 标签(tag)相关操作
  14. 【QT】QT从零入门教程(十四):标准颜色对话框类QColorDialog
  15. github 搜索_github 项目搜索技巧让你更高效精准地搜索项目
  16. ubuntu 11.10 因为gcc版本过高引起的错误,安装 gcc 4.4(转)
  17. C++获取C盘临时文件夹的方法
  18. 有5个学生,每个学生的数据包括学号、姓名、三门课的成绩,从键盘输入5个学生数据,要求打印出三门课总平均成绩,以及最高分的学生的数据(包括学号、姓名、三门课的成绩、平均分数)。VS2019版
  19. vga转html电脑打不开,VGA转HDMI转换器解决电脑连接投影仪的问题
  20. 人才数据报告不会写?指标不明晰?这套人力资源方案帮你统统解决

热门文章

  1. Unity3D接入第三方插件之微信登录安卓SDK
  2. Windows dss代理摄像头rtsp流 rtsp摄像头+ffmpeg+vlc
  3. 解决 Invalid component name: “404“. Component names should conform to valid custom element name ...
  4. 计算机设备的工作原理,计算机工作原理
  5. L1-1 天梯赛座位分配
  6. 计算机复制操作的方法,怎么用键盘复制粘贴?电脑使用键盘复制粘贴的方法
  7. 声音大小与振幅的关系_喇叭声音与尺寸大小的关系
  8. ae绘图未指定错误怎么办_设计高手总结47个快捷键50个CAD使用技巧,助你神速绘图拒绝加班!...
  9. 关于卷积核大小的论文与思路
  10. 彗星http_大气与彗星