目录
2.4.2 ABA问题的解决(AtomicStampedReference 类似于时间戳)
1.2.3 新版⽣产者消费者写法 ReentrantLock.Condition
1.2.2 newSingleThreadExecutor线程池
一、认识并发编程
1.1 线程
1.1.1 进程和线程
进程
进程:进程指正在运行的程序,进程拥有一个完整的、私有的基本运行资源集合。通常,每个进程都有自己的内存空间。
进程往往被看做是程序或者应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互协作的进程集合。
为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如pips和sockets。IPC不仅支持统一系统上的通信,也支持不同的系统。IPC通信方式包括管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Stream等方式,其中Socket和Streams支持不用主机上的两个进程IPC。
线程
线程有时也被成为轻量级的进程。进程和线程都提供了一个执行环境,但创建可一个新的线程比创建一个新的进程需要的资源要少。
线程是在进程中存在的 -- 每个进程最少有一个线程。线程共享进程的资源,包括内存和代开的文件。这样提高了效率,但潜在的问题就是线程间的通信。多线程的执行时Java平台的一个基本特征。每个应用都至少有一个线程或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个,叫主线程。
简而言之:一个程序运行后至少有一个进程。一个进程中可以包含多个线程。
1.1.2 线程实践
1.1.2.1 线程的创建
两种方式
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String[] args) {
new HelloThread().start();
}
}
结果:
方式二:
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String[] args) {
HelloRunnable helloRunnable = new HelloRunnable();
Thread thread = new Thread(helloRunnable);
thread.start();
}
}
结果:
启动线程
调用start方法
停止线程
线程自带的stop方法,一方面已经过时,另一方面,不会对停止的线程做状态保存,是的线程中设计的对象处于位置的状态,如果这些状态,其他线程也会使用,将会使得其他线程出现无法预料的异常,所以,停止程序的功能,需要自己实现。
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
StopThread stopThread = new StopThread();
stopThread.start();
Thread.sleep(1000L);
stopThread.stop();
while(stopThread.isAlive()){}
stopThread.print();
}
private static class StopThread extends Thread {
private int x = 0;
private int y = 0;
public void run(){
synchronized (this) {
++x;
try{
Thread.sleep(3000L);
}catch (InterruptedException e){
e.printStackTrace();
}
++y;
}
}
public void print() {
System.out.println("x=" + x + " y =" + y);
}
}
}
结果:上述代码中,run方法里是一个同步的原子操作,x和y必须要共同增加,然而这里如果调用thread.stop()方法强制中断线程,输入如下:
没有异常,也破坏了我们的预期。如果这种问题出现在我们的程序中,会引发难以预期的异常。因此这种不安全的方式很早就被废弃了。
自定义stop线程
public class MyRunnable implements Runnable {
private boolean doStop = false;
public synchronized void doStop(){
this.doStop = true;
}
private synchronized boolean keepRunning() {
return this.doStop == false;
}
@Override
public void run() {
while(keepRunning()){
System.out.println(Thread.currentThread().getName());
System.out.println("Running");
}
try {
Thread.sleep(3L * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyRunnableMain {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(10L * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
myRunnable.doStop();
}
}
结果:
1.1.3 线程暂停和中断
暂停
Java中线程的暂停就是调用java.lang.Threa类中的sleep方法。该方法会使当前正在执行的线程暂停指定的一段时间,如果线程持有锁,sleep方法结束前并不会释放锁。
中断
java.lang.Thread类有一个interrupt方法,该方法直接对线程调用。当被interrupt的线程正在sleep或wait时,会抛出InterruptedException异常。
事实上,interrupt方法只是改变目标线程的中断状态(interrupt status),而那些会抛出InterruptedException异常的方法,如wait、sleep、join等,都是在方法内部不断地检查中端状态的值;
- interrupted方法
Thread实例方法:必须由其他线程获取被调用线程的实例后,进行调用。实际上,只是改变了被调用线程的内部中断状态;
- Thread.interrupted方法
Thread类方法:必须在当前执行线程内调用,该方法返回当前线程的内部中断状态,然后清除中断状态(置为false);
- isInterrupted方法
Thread实例方法:用来检查指定线程中的中断状态。当线程为中断状态是,会返回true;否则返回false.
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
StopThread stopThread = new StopThread();
stopThread.start();
Thread.sleep(1000L);
stopThread.interrupt();
while(stopThread.isAlive()){}
stopThread.print();
}
private static class StopThread extends Thread {
private int x = 0;
private int y = 0;
public void run(){
synchronized (this) {
++x;
try{
Thread.sleep(3000L);
}catch (InterruptedException e){
e.printStackTrace();
}
++y;
}
}
public void print() {
System.out.println("x=" + x + " y =" + y);
}
}
}
interrupt底层源码
/**
* 中断此线程。
* <p>线程可以中断自身,这是允许的。在这种情况下,不用进行安全性验证({@link #checkAccess() checkAccess} 方法检测)
* <p>若当前线程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然后线程的中断状态将被清除,并且将收到 {@link InterruptedException}。
* <p>如果线程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那么通道 channel 将会关闭,
* 并且线程的中断状态将被设置,线程将收到一个 {@link java.nio.channels.ClosedByInterruptException} 异常。
* <p>如果线程由于在 {@link java.nio.channels.Selector} 中而阻塞,那么线程的中断状态将会被设置,它将立即从选择操作中返回。
*该值可能是一个非零值,就像调用选择器的{@link java.nio.channels.Selector#wakeupakeup}方法一样。
*
* <p>如果上述条件均不成立,则将设置该线程的中断状态。</p>
* <p>中断未运行的线程不必产生任何作用。
* @throws SecurityException 如果当前线程无法修改此线程
*/
public void interrupt() {
//如果调用中断的是线程自身,则不需要进行安全性判断
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 只是设置中断标志
b.interrupt(this);
return;
}
}
interrupt0();
}
// 静态方法,这个方法有点坑,调用该方法调用后会自动清除中断状态
public static boolean interrupted(){
return currentThread.isInterrupted(true);
}
// 这个方法不会清除中断状态
public boolean isIntruppted(){
return isInterrupted(false);
}
// 上面两个方法会调用这个本地方法,参数代表是否清除中断状态
private native boolean isInterrupted(boolean CleanInterrupted);
interrupt():
- interrupt中断操作时,非自身打断需要先检测是否有中断权限,这由jvm的安全机制配置;
- 如果线程处于sleep,wait,join等状态,那么线程将立即开启退出被阻塞状态,并抛出一个InterruptedException异常;
- 如果线程处于I/O阻塞状态,将会抛出ClosedByInterruptException(IOException的子类)异常;
- 如果线程处于Selector上被阻塞,select方法将立即返回;
- 如果非以上情况,将直接标记interrupt状态
注意:interrupt操作不会打断所有阻塞,只有上述阻塞情况下才在jvm的打断范围内,入出入锁阻塞的线程,不会搜interrupt中断;
阻塞情况下中断,抛出异常后线程恢复非中断状态,即interrupted = false
public class ThreadTest1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Task("mytask"));
t.start();
t.interrupt();
}
static class Task implements Runnable{
String name;
public Task(String name){
this.name = name;
}
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread has been interrupt");
}
System.out.println("interrupted:" + Thread.currentThread().isInterrupted());
System.out.println("task " + name + " is over");
}
}
}
调用Thread.interrupted()方法后居然恢复非中断状态
public class ThreadTest2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Task("mytask"));
t.start();
t.interrupt();
}
static class Task implements Runnable{
String name;
public Task(String name){
this.name = name;
}
public void run() {
System.out.println("first:" + Thread.interrupted());
System.out.println("second:" + Thread.interrupted());
System.out.println("task " + name + " is over");
}
}
}
1.1.2.4 线程的状态
Java线程可能的状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不用于waiting,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
迁
1.2 多线程
线程是进程的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
1.2.1 并发和并行
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一事件间隔发生。
- 并行值在不同实体上的多个事件,并发是在同一个实体上的多个事件。
- 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
1.2.2 多线程好处
提高CPU的利用率
单线程
5 seconds reading file A
2 seconds processing file B
5 seconds reading file B
2 seconds processing file B
----------------------------------------
14 seconds total
多线程
5 seconds reading file A
5 seconds reading file B + 2 seconds processing file A
2 seconds processing file B
--------------------------------------------
12 seconds total
一般来说,在等待磁盘IO,网络IO或者等待用户输入时,CPU可以同时去处理其他业务
更高效的响应
多线程技术使程序的响应速度更快,因为用户界面可以在进行其他工作的同时一直处于活动状态,不会造成无法响应的现象。
公平使用CPU资源
当前没有进行处理的任务,可以将处理器时间让给其他任务,也可以定期将处理时间让给其他任务;通过对CPU时间的划分,使得CPU时间片可以在多个线程之间切换,避免需要长时间处理的线程独占CPU,导致其他线程长时间等待。
1.2.3 多线程的代价
更复杂的设计
共享数据的读取,数据的安全性,线程之间的交互,线程的同步等
上下文环境切换
线程切换,cpu需要保存本地数据、程序指针等内容
更多的资源消耗
每个线程都需要内存维护自己的本地栈信息,操作系统也需要资源对线程进行管理维护;
1.3 线程安全
1.3.1 基本概念
何谓静态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件,如使用synchronized或者加锁机制。
何谓线程安全
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。
1.3.2 对象的安全
局部基本类型变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
public class ThreadTest3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyThread());
for (int i = 0; i < 50; i++) {
new Thread(t,"线程"+i).start();
}
}
static class MyThread implements Runnable{
public void run(){
int a = 0;
++a;
System.out.println(Thread.currentThread().getName() + ":" + a);
}
}
}
无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据。
局部的对象引用
对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没哟存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(即该对象不会被其方法获得,也不会被非局部变量引用到)该方法。那么它是线程安全的。实际上,哪怕将这个对象作为参数传递给其他方法,只要别的线程获取不到这个对象,那它仍是线程安全的。
public void method1(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localhost);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
对象成员(成员变量)
对象成员存储在堆上,如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
public class ThreadTest{
public static void mian(String[] args){
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
}
}
class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
}
public void run(){
this.instance.add(" "+Thread.currentThread().geyName());
System.out.println(this.instance.builder.toString());
}
class NotThreadSafe{
StringBuffer builder = new StringBuffer();
public void add(Sttring text){
this.builder.append(text);
}
}
如果两个线程同时调用同一个NotThreadSafe实例上的add方法,就会有竞态条件问题。
1.3.3 不可变性
通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下实例:
public class ImmutableValue{
private int value =0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。
第二章 JUC并发介绍
1.1 并发包介绍
1.2 JMM(Java Memory Model)
由于 JVM 运⾏程序的实体是线程,⽽每个线程创建时 JVM 都会为其创建⼀个 ⼯作内存 (有些地⽅成为栈空间),⼯作内存是每个线程的私有数据区域,⽽ Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必 须在⼯作内存中进⾏,⾸先要将变量从主内存拷⻉到⾃⼰的⼯作内存空间,然后对变量进⾏操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的⼯作内存中存储着主内存中的变量副本拷⻉,因此不同的线程间⽆法访问对⽅的⼯作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:
- JMM可能带来可⻅性、原⼦性和有序性问题。
- 所谓可⻅性,就是某个线程对主内存内容的更改,应该⽴刻通知到其它线程。
- 所谓原⼦性,是指⼀个操作是不可分割的,不能执⾏到⼀半,就不执⾏了。
- 所谓有序性,就是指令是有序的,不会被重排。
1.3 volatile关键字
volatile关键字是Java提供的一种轻量级同步机制。
- 它能保证可见性和有序性
- 但不能保证原子性
- 禁止指令重排
1.3.1 可见性
class MyData{
int number = 0;
public void setTo60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
System.out.println("可见性的测试");
MyDatamyData = new MyData();
// 启动一个线程,操作 number 变量
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "\t 执行");
// 修改number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setTo60();
System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);
},"线程A").start();
// 在mian线程中,执行操作
while (myData.number == 0){
// 如果检测到的number值为0 main线程无线执行
}
System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
}
}
可见性的测试
线程A 执行
线程A 更新number的值===60
/**
* Volatile 关键字
*/
class MyData{
volatile int number = 0;
public void setTo60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
System.out.println("可见性的测试");
MyDatamyData = new MyData();
// 启动一个线程,操作 number 变量
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "\t 执行");
// 修改number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setTo60();
System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);
},"线程A").start();
// 在mian线程中,执行操作
while (myData.number == 0){
// 如果检测到的number值为0 main线程无线执行
}
System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
}
}
可见性的测试
线程A 执行
线程A 更新number的值===60
main 获取到number的值===60
1.3.2 原子性
原⼦性指的是什么意思?不和分割,完整性,也即某个线程正则做某个具体业务时,中间不可以被加塞或者被分割。需要 整体完整,要么同时成功,要么同时失败。
class MyData1{
// volatile并不能保证操作的原⼦性。这是因为,⽐如⼀条number++的操作,会形成3条指令。
// int number = 0;
volatile int number = 0;
// 此时number前⾯已经加了volatile,但是不保证原⼦性
public void addPlusPlus(){
number++;
}
}
public class ActomicDemo {
public static void main(String[] args) {
//volatileVisibilityDemo();
atomicDemo();
}
private static void atomicDemo() {
System.out.println("原⼦性测试");
MyData1 myData = new MyData1();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int类型最终number值: " + myData.number);
}
}
javap -c 包名.类名
javap -c MyData
public void addPlusPlus();
Code:
0: aload_0
1: dup
2: getfield #2 // Field number:I //读
5: iconst_1 //++常量1
6: iadd //加操作
7: putfield #2 // Field number:I //写操作
10: return
假设有3个线程,分别执⾏number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进⾏操作。假设线程0执⾏完毕,number=1,也⽴刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。
解决的⽅式就是:
- 对 addPlusPlus() ⽅法加锁。
- 使⽤ java.util.concurrent.AtomicInteger 类。
class MyData {
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
public void atomicPlusPlus(){
atomicInteger.incrementAndGet();
}
}
public class VolatileDemo {
public static void main(String[] args) {
// volatileTest();
System.out.println("原子性测试");
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.atomicPlusPlus();
}
},String.valueOf(i) ).start();
}
// 当20个线程都执行完毕后,打印number的值
while(Thread.activeCount() > 2){ // main线程, gc垃圾回收期线程
Thread.yield();
}
System.out.println("main中打印最终的 number的值===" + myData.number);
System.out.println("main中打印最终的 atomicInteger的值===" + myData.atomicInteger);
}
private static void volatileTest() {
System.out.println("可见性的测试");
MyData myData = new MyData();
// 启动一个线程,操作 number 变量
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "\t 执行");
// 修改number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setTo60();
System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);
},"线程A").start();
// 在mian线程中,执行操作
while (myData.number == 0){
// 如果检测到的number值为0 main线程无线执行
}
System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
}
}
1.3.3 有序性
单线程环境⾥⾯确保程序最终执⾏结果和代码顺序执⾏的结果⼀致;
处理器在进⾏重排序时必须要考虑指令之间的数据依赖性;多线程环境中线程交替执⾏,由于编译器优化重排的存在,两个线程中使⽤的变量能否保证⼀致 性是⽆法确定的,结果⽆法预测。
观看下⾯代码,在多线程场景下,说出最终值a的结果是多少? 5或者6我们采⽤ volatile 可实现禁⽌指令重排优化,从⽽避免多线程环境下程序出现乱序执⾏的现象
public class ResortSeqDemo {
volatile int a = 0;
volatile boolean flag = false;
/*
多线程下flag = true可能先执⾏,还没⾛到a=1就被挂起。
其它线程进⼊method02的判断,修改a的值=5,⽽不是6。
*/
public void method01(){
a = 1;
flag = true;
}
public void method02(){
if (flag){
a += 5;
System.out.println("*****最终值a: " + a);
}
}
public static void main(String[] args) {
ResortSeqDemo resortSeq = new ResortSeqDemo();
new Thread(()->{resortSeq.method01();},"ThreadA").start();
new Thread(()->{resortSeq.method02();},"ThreadB").start();
}
}
*****最终值a: 6
- ⼀个是保证特定操作的顺序性
- ⼆是保证变量的可⻅性。
由于编译器和处理器都能够执⾏指令重排优化。所以,如果在指令间插⼊⼀条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插⼊内存屏障可以禁⽌在内存屏障前后的指令进⾏重排序优化。内存屏障另外⼀个作⽤是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。
- 传统
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
// 改为多线程操作测试
public static void main(String[] args) {
//main线程操作
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
- 改为多线程操作测试
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//多线程操作
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},Thread.currentThread().getName()).start();
}
}
}
- 调整后,采⽤常⻅的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会有线程安全问题。
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class){
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//多线程操作
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},Thread.currentThread().getName()).start();
}
}
}
这个漏洞⽐较tricky,很难捕捉,但是是存在的。 instance=new SingletonDemo(); 可以⼤致分为三步
instance = new SingletonDemo();
public static thread.SingletonDemo getInstance();
Code:
0: getstatic #11 // Field instance:Lthread/SingletonDemo;
3: ifnonnull 37
6: ldc #12 // class thread/SingletonDemo
8: dup
9: astore_0
10: monitorenter
11: getstatic #11 // Field instance:Lthread/SingletonDemo;
14: ifnonnull 27
17: new #12 // class thread/SingletonDemo 步骤1
20: dup
21: invokespecial #13 // Method "<init>":()V 步骤2
24: putstatic #11 // Field instance:Lthread/SingletonDemo;步 骤3
底层Java Native Interface中的C语⾔代码内容,开辟空间的步骤
memory = allocate(); //步骤1.分配对象内存空间
instance(memory); //步骤2.初始化对象
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance!= null
memory = allocate(); //步骤1. 分配对象内存空间
instance(memory); //步骤2.初始化对象
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null
memory = allocate(); //步骤1. 分配对象内存空间
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成!
instance(memory); //步骤2.初始化对象
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class){
if (instance == null) {
instance = new SingletonDemo(); //多线程情况下,可能发⽣指令重排
}
}
}
return instance;
}
private static volatile SingletonDemo instance=null;
第二章 CAS
2.1 CAS是什么?
class MyData {
volatile int number = 0;
AtomicInteger atomicInteger=new AtomicInteger();
public void addPlusPlus(){
number++;
}
public void addAtomic(){
atomicInteger.getAndIncrement();
}
public void setTo60() {
this.number = 60;
}
}
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t 当前数据值 : "+ atomicInteger.get());
//修改失败
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t 当前数据值 : "+ atomicInteger.get());
}
}
CAS并发原语体现在JAVA语⾔中就是sum.misc.Unsafe类中的各个⽅法。看⽅法源码,调⽤UnSafe类中的CAS⽅法,JVM会帮我们实现出CAS汇编指令。这是⼀种完全依赖于硬件的功能,通过它实现了原⼦操作。再次强调,由于CAS是⼀种系统原语,原语属于操作系统⽤语范畴,是由若⼲条指令组成的,⽤于完成某个功能的⼀个过程,并且原语的执⾏是连续的,在执⾏过程中不允许被中断,也就是说 CAS是⼀条CPU的原⼦指令,不会造成所谓的数据不⼀致问题。
2.2 CAS底层原理?
AtomicInteger内部的重要参数
参数介绍var1 AtomicInteger 对象本身var2 该对象值的引⽤地址var4 需要变动的数量var5 是通过 var1 和 var2 ,根据 对象 和 偏移量 得到在 主内存的快照值 var5
2.3 CAS缺点
2.4 CAS会导致“ABA问题”
⾼频⾯试题 |
---|
1. 原⼦类AtomicInteger的ABA问题谈谈?原⼦更新引⽤你知道吗? |
2. 我们知道ArrayList是线程不安全,请编码写⼀个不安全的案例并给出解决⽅案 |
3. 公平锁/⾮公平锁/可重⼊锁/递归锁/⾃旋锁谈谈你的理解?请⼿写⼀个⾃旋锁 |
4. CountDownLath/CyclicBarrier/Semaphore使⽤过吗? |
5. 阻塞队列知道吗? |
6. 线程池⽤过吗?ThreadPoolExecutor谈谈你的理解?⽣产上你如何设置合理参数 |
7. 死锁编码及定位分析 |
2.4.1 AtomicReference原⼦引⽤
public class AtomicReferenceDemo {
public static void main(String[] args) {
User user1 = new User("Jack",25);
User user2 = new User("Tom",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get()); // true
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get() ); //false
}
}
2.4.2 ABA问题的解决(AtomicStampedReference 类似于时间戳)
ThreadA 100 1 2020 2ThreadB 100 1 111 2 100 3
参数说明:
V expectedReference , 预期值引⽤V newReference , 新值引⽤int expectedStamp , 预期值时间戳int newStamp , 新值时间戳
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("======ABA问题的产⽣======");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get().toString());
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("======ABA问题的解决======");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第⼀次版本号: " + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第⼆次版本号: " + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第⼀次版本号: " + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException
e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2020,
stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改成功与否:"+result+" 当前最新版本号"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前实际值:"+atomicStampedReference.getReference());
}, "t4").start();
}
}
第三章 阻塞队列
1.1 阻塞队列概述
- 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),⼀旦条件满⾜,被挂起的线程⼜会⾃动被唤醒。
- 阻塞队列 是⼀个队列,在数据结构中起的作⽤如下图:
当队列是空的,从队列中获取( Take )元素的操作将会被阻塞当队列是满的,从队列中添加( Put )元素的操作将会被阻塞试图中空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插⼊新的元素试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除⼀个或多个元素或者完全清空,使队列变得空闲起来后并后续新增
类名 | 作⽤ |
---|---|
ArrayBlockingQueue | 由数组结构构成的有界阻塞队列 |
LinkedBlockingQueue |
由
链表结构
构成的
有界(但默认值为
Integer.MAX_VALUE
)
阻塞队列
|
PriorityBlockingQueue | ⽀持优先级排序的⽆界阻塞队列 |
DelayQueue | 使⽤优先级队列实现的延迟⽆界阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,也即单个元素的队列 |
LinkedTransferQueue | 由链表构成的⽆界阻塞队列 |
LinkedBlockingDeque | 由链表构成的双向阻塞队列 |
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);
addAndRemove(blockingQueue);
offerAndPoll(blockingQueue);
putAndTake(blockingQueue);
outOfTime(blockingQueue);
}
private static void outOfTime(BlockingQueue<String> blockingQueue) throws InterruptedException {
System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
}
private static void putAndTake(BlockingQueue<String> blockingQueue)throws InterruptedException {
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("d");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
private static void offerAndPoll(BlockingQueue<String> blockingQueue) {
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("e"));
System.out.println(blockingQueue.peek());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
private static void addAndRemove(BlockingQueue<String> blockingQueue) {
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.add("e"));
System.out.println(blockingQueue.element());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
}
1.2.1 传统模式
class Aircondition{
private int number = 0;
//⽼版写法
public synchronized void increment() throws Exception{
//1.判断
if (number != 0){
this.wait();
}
//2.⼲活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws Exception{
//1.判断
if (number == 0){
this.wait();
}
//2.⼲活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
this.notifyAll();
}
}
/**
* 题⽬:现在两个线程,可以操作初始值为零的⼀个变量,
* 实现⼀个线程对该变量加1,⼀个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 知识⼩总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo {
public static void main(String[] args) {
Aircondition aircondition = new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
}
}
- ⽣产者消费者防⽌虚假唤醒
1.2.2 ⽣产者消费者防⽌虚假唤醒(执⾏原理分析)
1.2.3 新版⽣产者消费者写法 ReentrantLock.Condition
新模式使⽤ Lock 来进⾏操作,需要⼿动加锁、解锁。详⻅ProdConsTradiDemo。
class Aircondition{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//新版写法
public void increment() throws Exception{
lock.lock();
try{
//1.判断
while (number != 0){
condition.await();
}
//2.⼲活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception{
lock.lock();
try{
//1.判断
while (number == 0){
condition.await();
}
//2.⼲活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 题⽬:现在两个线程,可以操作初始值为零的⼀个变量,
* 实现⼀个线程对该变量加1,⼀个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 知识⼩总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo {
public static void main(String[] args) {
Aircondition aircondition = new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"D").start();
}
}
1.2.4 精准通知顺序访问
class ShareData{
private int number = 1;//A:1,B:2,C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void printc1(){
lock.lock();
try {
//1.判断
while (number != 1){
c1.await();
}
//2.⼲活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 2;
//通知第2个
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc2(){
lock.lock();
try {
//1.判断
while (number != 2){
c2.await();
}
//2.⼲活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 3;
//如何通知第3个
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc3(){
lock.lock();
try {
//1.判断
while (number != 3){
c3.await();
}
//2.⼲活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 1;
//如何通知第1个
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 备注:多线程之间按顺序调⽤,实现A->B->C
* 三个线程启动,要求如下:
* A打印5次,B打印10次,C打印15次
* 接着
* A打印5次,B打印10次,C打印15次
* 来10轮
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.多线程交互中,防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 4.标志位
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc1();
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc2();
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc3();
}
},"C").start();
}
}
1.2.5 Synchronized和Lock的区别
1.2.6 阻塞队列模式⽣产者消费者
public class ProdConsBlockQueueDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(5));
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t⽣产线程启动");
try {
myResource.myProd();
} catch (Exception e) {
e.printStackTrace();
}
}, "prod").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t⽣产线程启动");
try {
myResource.myProd();
} catch (Exception e) {
e.printStackTrace();
}
}, "prod-2").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
try {
myResource.myCons();
} catch (Exception e) {
e.printStackTrace();
}
}, "cons").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
try {
myResource.myCons();
} catch (Exception e) {
e.printStackTrace();
}
}, "cons-2").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("5秒钟后,叫停");
myResource.stop();
}
}
class MyResource {
private volatile boolean FLAG = true; //默认开启,进⾏⽣产+消费
private AtomicInteger atomicInteger = new AtomicInteger();
private BlockingQueue<String> blockingQueue = null;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd() throws Exception {
String data = null;
boolean retValue;
while (FLAG) {
data = atomicInteger.incrementAndGet() + "";//++i
retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "\t"+ "插⼊队列" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t"+ "插⼊队列" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "\t⽼板叫停了,FLAG已更新为false,停⽌⽣产");
}
public void myCons() throws Exception {
String res;
while (FLAG) {
res = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (null == res || "".equals(res)) {
// FLAG = false;
System.out.println(Thread.currentThread().getName() + "\t超过2秒钟没有消费,退出消费");
return;
}
System.out.println(Thread.currentThread().getName() + "\t\t消费队列" + res + "成功");
}
}
public void stop() {
this.FLAG = false;
}
}
运⾏结果
第四章 线程池
1.1 线程池基本概念
10 年前单核 CPU 电脑,假的多线程,像⻢戏团⼩丑玩多个球, CPU 需要来回切换。现在是多核电脑,多个线程各⾃跑在独⽴的 CPU 上,不⽤切换效率⾼。
1.2 线程池三种常⽤创建⽅式
1.2.1 newFixedThreadPool线程池
1.2.2 newSingleThreadExecutor线程池
1.2.3 newCachedThreadPool线程池
1.2.4 线程池代码演示
/**
* 线程池代码演示
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
//System.out.println("=======Fixed Thread Pool========");
//⼀个池⼦有5个⼯作线程,类似银⾏有5个受理窗⼝
//threadPoolTask( Executors.newFixedThreadPool(5) );
// System.out.println("======Single Thread Pool=========");
// //⼀个池⼦有1个⼯作线程,类似银⾏有1个受理窗⼝
// threadPoolTask( Executors.newSingleThreadExecutor() );
// System.out.println("=====Cached Thread Pool=======");
// //不定量线程,⼀个池⼦有N个⼯作线程,类似银⾏有N个受理窗⼝
// threadPoolTask( Executors.newCachedThreadPool() );
System.out.println("=====Custom Thread Pool=======");
threadPoolTask( new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
));
}
private static void threadPoolTask(ExecutorService threadPool) {
//模拟有10个顾客来办理业务
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
1.3 线程池创建的七个参数
1.4 线程池底层原理
原理图:上⾯银⾏的例⼦,实际上就是线程池的⼯作原理。
流程图:
1.5 线程池的拒绝策略
1.5.1 AbortPolicy拒绝策略
1.5.2 CallerRunsPolicy拒绝策略
1.5.3 DiscardOldestPolicy拒绝策略
1.5.4 DiscardPolicy拒绝策略
1.6 实际⽣产使⽤哪⼀个线程池?
ExecutorService threadPool = new ThreadPoolExecutor(
2,
80*2,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
⾃定义线程池参数选择
IO 密集型,即该任务需要⼤量的 IO ,即⼤量的阻塞。在单线程上运⾏ IO 密集型的任务会导致浪费⼤量的 CPU 运算能⼒浪费在等待。所以在 IO 密集型任务中使⽤多线程可以⼤⼤的加速程序运⾏,及时在单核 CPU 上,这种加速主要就是利⽤了被浪费掉的阻塞时间。IO 密集型时,⼤部分线程都阻塞,故需要多配置线程数:参考公式: CPU 核数 / ( 1 - 阻塞系数 ) 阻塞系数在 0.8~0.9 之间⽐如 8 核 CPU : 8/(1 - 0.9) = 80 个线程数
第五章 死锁编码和定位
2.1 死锁编码
代码演示
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA; 123456
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t⾃⼰持有:" + lockA + "\t尝试获取:" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t⾃⼰持有:" + lockB + "\t尝试获取:" + lockA);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB), "ThreadA").start();
new Thread(new HoldLockThread(lockB, lockA), "ThreadB").start();
}
}
2.2 死锁定位
jps指令: jps -l 可以查看运⾏的Java进程 。
jstack指令: jstack pid 可以查看某个Java进程的堆栈信息,同时分析出死锁。