Java并发编程

第1章 并发编程的挑战

1.1 上下文切换

并发:多线程对相同资源进行竞争,对同一个变量同时进行读写操作

并行:对不同变量进行读写操作,没有竞争

并发有资源竞争,并行没有资源竞争

有些线程执行结束后即消失,有些线程会一直存在。

linux时间片等长,一般为20ms,windows时间片不等长。

在多线程的CPU下,线程切换达到1ms/1次

进程进入就绪态是我们自己决定。

Q:多线程一定比单线程快吗?

A: 不一定,在没有CPU浪费的情况下,多线程比单线程慢。

Q:如何查看进程信息?

**A:**用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么。

sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
// 切换管理员权限,在/opt/ifeve/java/bin/ 目录下执行jstack命令,传31177参数,创建/home/tengfei.fangtf/dump17 文件并且将线程信息传进去

grep指令(查询):

grep ' + 要查询的内容 + '  + 查询的文件 //查询内容
grep -C 1 ' + 要查询的内容 + '  + 查询的文件 //查询内容和上下一行
grep -B 1 ' + 要查询的内容 + '  + 查询的文件 //查询内容和下一行
grep -A 1 ' + 要查询的内容 + '  + 查询的文件 //查询内容和上一行
grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
//查询dump17文件以 awk '{print $2$3$4$5}' 格式输出,按文件名排序,
39 RUNNABLE 
21 TIMED_WAITING(onobjectmonitor) 
6 TIMED_WAITING(parking) 
51 TIMED_WAITING(sleeping) 
305 WAITING(onobjectmonitor) 
3 WAITING(parking)

1.2 死锁

synchronized()方法【加锁关键字】锁的只能是引用类型,不能是基本类型。

synchronized(){...}方法执行完毕开锁。

线程加锁后,只能由该线程对该资源进行读写,其他线程均没有权限对其进行读写。

即使CPU分配给该线程的时间片运行结束,CPU去运行其他线程,该锁也不会消失。

进入阻塞队列锁不释放。

Q:请举一个死锁的例子。

**A:**不同线程锁不同资源就可能出现死锁。

public class DeadLockDemo {
    private static String A = "A";
    private static String B = "B";

    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }

    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

Q:如何避免死锁?

A:

避免一个线程同时获取多个锁。

避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

第2章 Java并发机制的底层实现原理

锁的位置是在对象头部。

2.1 volatile的应用

防止指令重排序,保证读的时候是准确的。

线程可见性:在多线程的情况下,一个线程修改了volatile修饰的变量,其他线程可以立刻知晓到当前的数据。

每个线程在进行变量操作时都会拷贝一个副本数据进行操作。

内存屏障:一个约定(标记),见到该标记后就不再访问。

缓冲行:缓存中可以分配的最小存储单位(64B)。

原子操作:要么都成功,要么都失败。

緩存:把数据存到离调用更近的地方。

缓存命中:在高速缓存中找到需要的数据。

写命中:在高速缓存中对数据进行写操作(增删改)。

写缺失:一个进程对高速缓存中的数据进行写操作时,另一个进程将内存中的数据删除,导致高速缓存中的数据写回内存中时找不到内存中的数据。

事务必须具有原子性。

Q:volatile的作用是什么?volatile是如何保证线程可见性的?

**A:**当有多个线程调用内存中同一个数据时,该数据会加上地址;当其中一个线程对其进行写操作后,该数据会从总线写回到内存中,并且同时通知高速缓存中所有该地址的数据无效,其他线程想要调用时必须从内存中从新读取。

理论上,CPU有几个核就可以有几个线程同时运行。

线程操作内存中的数据时需要排队。

volatile的实现方法:

  1. 锁总线(浪费资源较大);
  2. 锁缓存(缓存锁定)。

2.2 synchronized的实现原理与应用

静态方法加锁叫类锁,会把所有静态方法锁住;非静态方法加锁叫对象锁,会把所有的对象锁住。对象锁只限制在自己的对象之内,对其他对象不影响。

类锁和对象锁互不影响。

//创建线程池
class Solution {
    public static void main(String[] args) {
        Test x = new Test(); // x叫做多个线程的共享变量,相当于并发资源
        // 在线程内使用x时相当于被final修饰过了,不能看到任何修改操作。
        Thread t1 = new Thread() {
            @Override
            public void run() {
                x.m1();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                x.m2();
            }
        };
        t1.start();
        t2.start();
        System.out.println("main");
    }
}

//测试锁
public class Test {
    public synchronized void m1() {
        System.out.println("m1 start");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1 end");
    }
    public synchronized void m2() {
        System.out.println("m2 start");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2 end");
    }
    public synchronized void m3() {
        System.out.println("m3 start");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m3 end");
    }
    public static synchronized void m4() {
        System.out.println("m4 start");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m4 end");
    }
}

x.join(); //等待x线程执行结束后再执行本行之后的代码

数据往回更新后还会重新读一次。

写后读思想:A线程修改后,B线程基于A修改后的值进行叠加操作。写后读是并发编程能够进行正确操作的核心思想。只有写后读生效的场景才是并发安全的。

自旋锁加锁快,一旦资源解锁可以立刻感知到并且加锁。

当线程竞争激烈时,轻量级锁的整体性能大幅下降。自旋锁太多时,CPU利用率降低,整体进度变慢。

重量级锁几乎不会导致CPU浪费。

没有并发的使用偏向锁,无加锁流程,速度快;有轻微竞争时使用轻量级锁,自旋锁感知速度快,能立刻感知到解锁,能以最快的速度加锁;高并发时使用重量级锁,CPU浪费较低,整体执行速度快。

Q:偏向锁在哪配置?

A:

Q:自旋锁自旋多少次竞争不到会升级成重量级锁?

A:

方法执行时入栈并加锁,方法执行结束后锁释放。

2.3 原子操作的实现原理

Q:什么是原子操作?

**A:**不可被中断的一个或一系列操作。

CAS(比较并交换):CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

CAS实现需要调用操作系统底层的C接口。

现实中的流水线:一个人干同一件事,由许多人完成整个工作。

CPU流水线:任务执行顺序会改变,但会提升速度。

Java中Atomic开头的类都是并发安全的类,内部自动加锁,我们不需要给他加锁。concurrent包中的类全部都是线程安全的类。

ABA问题:因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。

第3章 Java内存模型

3.1 Java内存模型的基础

Q:线程之间如何进行通讯?

**A:**在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。

线程之间如何同步?

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Q:进程之间如何进行通讯?

**A:**进程间通信的8种方法:

1、无名管道通信

无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2、高级管道通信

高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

3、有名管道通信

有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

4、消息队列通信

消息队列(message queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5、信号量通信

信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

6、信号

信号(sinal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

7、共享内存通信

共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

8、套接字通信

套接字(socket):套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

进程间通信的方法详解

线程有的通讯方式,进程不一定有;进程有的通讯方式,线程一定有。

进程之间内存不共享,他们独占自己的内存空间。

等号右侧是变量的是读操作(Lead),等号右侧是数字的是写操作(Store)。

a = b;   d = f; //Load-Load

a = b;   e = 9; //Load-Store

w = 8;  q = 5; //Store-Store

r = 8;  h = g; //Store-Load

有依赖的不允许指令重排序。

happens-before的传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

3.4 volatile的内存语义

volatile只保证读到正确的数据,却不保证线程安全。

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写。

保证安全,有且只能有写后读操作。

3.8 双重检查锁定与延迟初始化

Servlet,Service等运行时只有一个对象。

懒加载:

饿汉式:对象使用前就加载;

懒汉式:对象使用后才加载。

public class UnsafeLazyInitialization {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) // 1:A线程执行
        instance = new Instance(); // 2:B线程执行
        return instance;
    }
}

Q:为什么getInstance() 方法需要设为静态?

**A:**getInstance() 方法不是静态无法访问到静态变量instance。

Q:为什么instance变量是私有的?

**A:**如果不设为私有,其他线程也可对他进行写操作,创建新的对象。

加锁力度越大,其他线程被挡的概率越大。

双检锁/双重校验锁(DCL,即 double-checked locking)

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**是

**是否多线程安全:**是

**实现难度:**较复杂

**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

getInstance() 的性能对应用程序很关键。

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
    if (singleton == null) {
        // 只锁这一块的原因:只有写操作才会有冲突,而读操作不会有冲突,不需要加锁
        synchronized (Singleton.class) {
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
    }
}

Q:为什么singleton是private类型?

**A:**如果不设为私有,其他线程也可对他进行写操作,创建新的对象。

Q:为什么singleton是volatile类型?

**A:**防止指令重排序+可见性(保证其他线程读取到正确的数据)【可能导致对象没有创建成功(只分配了地址,没有数据就返回句柄,空指针异常)】。

保证写操作的原子性。(情况:1.申请空间后存入地址,还没有初始化对象;2.预定空间,但还未申请空间【地址不为空,数据为空】)。

Q:为什么singleton是static类型?

A:调用它的方法是静态的。如果属性是非静态的,那么静态方法调用不到它。

Q:为什么构造方法是私有的?

**A:**防止外部创建对象。

Q:getSingleton()方法为什么是静态的?

**A:**getSingleton()方法不是静态无法访问到静态变量singleton。

Q:为什么会有两个if判空?

**A:**第二个if是给第二个及以后的线程使用的,防止多次创建对象(有可能在第一个线程加锁之前其他线程第一个if已经判断为空,如果没有第二个if判空就可能会导致多次创建对象)。

ReentrantLock 可以用于解决死锁问题。

加锁和解锁过程

state表示加锁的次数,解锁时可以按加锁次数来解锁。加锁次数 == 解锁次数。

线程安全的类有哪些?

  1. 通过synchronized 关键字给方法加上内置锁来实现线程安全

Timer,TimerTask,Vector,Stack,HashTable,StringBuffer

  1. 原子类Atomicxxx---包装类的线程安全类
  1. BlockingQueue 和BlockingDeque
  1. CopyOnWriteArrayList和 CopyOnWriteArraySet
  1. Concurrentxxx,最常用的就是ConcurrentHashMap,当然还有ConcurrentSkipListSet和ConcurrentSkipListMap等等。
  1. ThreadPoolExecutor
  1. ollections中的synchronizedCollection(Collection c)方法可将一个集合变为线程安全,其内部通过synchronized关键字加锁同步

Final

被final修饰的对象不能修改。防止一定程度上的指令重排序。

完成等价写后读保证线程安全。

CAS 比较并交换

读取后先记录原始值,操作后返回,如果记录的值和内存中的值相等则操作有效,反之无效。

ABA问题

如果内存的值 A -> B -> A,CAS感知不到。

解决ABA问题:增加版本号。

指令重排序

相邻两行代码,如果没有依赖关系,那么执行顺序不固定。

happens-before原则

1)A happens-before B

2)B happens-before C

3)A happens-before C

这里的第3个happens-before关系,是根据happens-before的传递性推导出来的。

对象锁

同一个对象里面的多个非静态加锁方法,只能同时被一个线程调用。

线程竞争对象锁需要CAS。

在竞争对象锁时,如果一个线程竞争失败会进入阻塞队列,直到锁被释放才会重新回到就绪态。

第4章 Java并发编程基础

线程本质为栈。

多线程运用场景

Tomcat,网络爬虫,数据库线程池。

线程简介

多线程20~40个线程性能最快。

线程优先级没有用,程序正确性不能依赖线程的优先级高低。

sleep(0),sleep(1):快速让出CPU。

Thread.yield():让出CPU并不是立即让出,会有一定的延迟。如果未执行完,切换回来会继续执行。

线程优先级由操作系统决定。

线程状态

新建,就绪,运行,等待,阻塞,死亡。

必须事先持有锁才能进入等待状态。

sleep()和wait()区别

sleep() 不会释放锁,wait()会释放锁。

如何进行Java优化?

能够看到Java底层,了解底层运作方式。

有中断机制才会有响应机制。

相关推荐
HoneyMoose1 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热3 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余4 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp5 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea
.Ayang7 分钟前
tomcat 后台部署 war 包 getshell
java·计算机网络·安全·web安全·网络安全·tomcat·网络攻击模型
一直学习永不止步13 分钟前
LeetCode题练习与总结:最长回文串--409
java·数据结构·算法·leetcode·字符串·贪心·哈希表
hummhumm27 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
chusheng184031 分钟前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
宁静@星空33 分钟前
006-自定义枚举注解
java·开发语言
hummhumm43 分钟前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架