Java高并发各种概念给我冲

加油啊,去北京冲啊!

线程安全

什么是线程安全?不管业务中遇到怎样的多线程访问某对象和某方法的情况,在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是和单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。
什么情况会出现线程安全?怎么避免?运行结果错误问题(a++多线程下消失)活跃性问题(死锁,活锁,饥饿)对象发布和初始化时候的安全问题
什么是发布?把这个对象超出这个类去使用,比如public就是发布出去了,一个方法有return,脱离了本类在其他类中使用。
什么是逸出?1.方法返回了一个private对象,2.还未初始化(构造函数还没有执行完毕)就把对象提供给外界,比如在构造函数中未初始化完毕就this赋值,注册监听事件,构造函数中运行线程。
各种需要考虑线程安全的情况?1.访问共享变量和资源,会有并发风险。2.所以依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题3.不同的数据之间存在捆绑关系。4. 使用其他类,其他类并不是线程安全的时候。
多线程会导致的问题: 性能问题,什么是性能问题,比如多运行几秒,为什么多线程会带来性能问题,调度:上下文切换,协作:内存同步。
上下文切换:“上下文切换(Context Switch),性质为环境切换。上下文切换,有时也称做进程切换或任务切换,是指CPU 从一个进程或线程切换到另一个进程或线程。线程调度,在同一个cpu核中,一个线程a进入了sleep阻塞状态,然后cpu会进行线程b的运行,但是同时它会为线程a保存现场,会开辟一段内存存储,比如线程a运行到哪一句了,中间有什么变量,不但保存现场需要开销,还会有缓存开销(比如说一些重要数据丢失了,需要重新获取也需要时间),何时会导致密集的上下文切换,从场景的不同来说,主要有抢锁和IO

底层原理

JVM内存结构
Java对象模型:
方法区:JVM给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示JAVA类
栈:new创建一个对象的时候,JVM会创建一个instanceOopDesc对象, 这个对象中包含了对象头以及实例数据。
堆:存放实例

JMM

JMM是一种规范,是工具类和关键字的原理。

Java作为高级语言,屏蔽了底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存问题,但是JMM抽象了主内存和本地内存的概念。

本地内存并不是真的一块给每个线程分配的内存,而是JMM的一个抽象,对于寄存器、一级缓存和二级缓存等的抽象

在这里插入图片描述为什么需要JMM
1.C语言不存在内存模型的概念
2.依赖处理器,不同处理器的结果不一样
3.无法保证并发安全
4.需要一个标准,让多线程运行的结果可预期

为什么会有重排序问题?
重排序概念:在线程1内部的两行代码的实际执行顺序和代码在java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。
重排序好处:提高处理速度。
重排序的3种情况,编译器优化,cpu指令重排,内存的重排序

为什么会有可见性问题
1.cpu有多级缓存,导致了读的数据过期
1.如果每个线程都共用一个缓存,那么也就不存在内存可见性问题了。
2.每个核心都会将自己需要的数据读到独占缓存中,数据修改后也先写入缓存,然后才刷入主存,这才导致了某些核心读取的是过期的值。
2.高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间多了Cache层
3.线程间的共享变量的可见性问题不是直接由多核引起的,而是多缓存引起的

happens-before的各种规则:

1、程序顺序规则:一个线程中的每个操作happens-before于该线程中的任意后续操作

2、监视器锁(同步)规则:对于一个监视器的解锁,happens-before于随后对这个监视器的加锁

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

4、传递性:如果A happens-before B,且B happens-before C,那么A happens-before C.

5、start()规则:如果线程A执行操作ThreadB.start() (启动线程B),那么A线程的ThreadB.start() 操作 happens-before 于线程B中的任意操作。

6、join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before 于线程A 从ThreadB.join() 操作成功返回

例子1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
例子2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)

volatile关键字

适用的场景:
1、纯赋值操作,boolean a = true;正确
a = !a; 错误
赋值操作本身具有原子性,加上volatile实现的可见性,实现了线程安全。
2、触发器操作,因为volatile有happens-before(hb)原则,所以可以当做触发器
volatile boolean a;
Thread A{
功能;
a = true;
}
Thread B{
if(a == true){
功能;
}
}
由于hb原则,所以可以放心的等待线程A运行完之后,再运行线程B。
volatile的两点作用:1、可见性 读一个volatile变量前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,volatile属性值会立即刷入主内存。2、禁止指令重排序

volatile和sychronized的关系
volatile可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,但是没有其他的操作,那么就可以用volatile就足以保证线程安全了。

volatile总结:1、volatile适用场景2、volatile是无锁的3、volatile作用于属性4、提供了可见性5、提供了hb保证。

那么,有多少种方法能保证可见性呢?
比如synchronized,Lock,并发集合都可以保证可见性。 更按理的说,基本实现了hb的都实现了可见性。

原子性
原子性:一系列的操作,要么全部成功,要么全部不成功,不会出现执行一半的情况,不可分割。
Java的一些原子操作(默认):
1、除了long和double之外的基本类型,因为long和double是64位的,有些JVM是32位的,赋值时是用俩个32位数赋值的,这肯定不是原子性
2、所有引用reference(引用)的赋值操作。
引用解析详解:https://www.cnblogs.com/cord/p/11546303.html
写到这里有点想不起来引用了,想来一下,好像引用是GC的问题。
3、Atomic包中所有类的原子操作
4、生成对象是不是原子操作?不是,生成对象有三步,第一步新建一个空的person,第二步将对象的地址指向p,第三步执行person构造函数。
由此可见,默认情况下,原子性的操作还是很少的
JMM应用实例:单例模式、单例和并发的关系。
单例模式使用场景:
1、无状态的工具类:我们只需要它帮我们记录日志信息,不管它在哪里使用,只需要一个实例对象即可。
2、全局信息类:我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录对象B上,此时需要将其变为单例。

死锁问题:
发生在并发中,当多个线程相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入了无限的阻塞,这就是死锁。
1、必然死锁的例子

package sycho;

public class MustDeadLock implements Runnable{
    static Object o1 = new Object();
    static Object o2 = new Object();
    int flag = 1;

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 2;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }
    @Override
    public void run() {
        System.out.println("flag = "+flag);
        if(flag == 1){
            synchronized (o1){
                try{
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("线程1成功拿到俩把锁");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("线程2成功拿到俩把锁");
                }
            }
        }
    }
}

2、发生死锁必然满足那些条件
1、互斥条件
2、请求与保持条件
3、不剥夺条件
4、循环等待条件
3、如何定位死锁
方法1、jstack:步骤使用命令行模式或者Sloth(Mac的软件)定位运行java程序的pid,然后在命令行执行${JAVA_HOME}/bin/jstack pid就可以使用jstack给你检测死锁错误了。
方法2、ThreadMXBean,从代码上解决这个问题。

package sycho;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MustDeadLock implements Runnable{
    static Object o1 = new Object();
    static Object o2 = new Object();
    int flag = 1;

    public static void main(String[] args) throws InterruptedException {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 2;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for(int i = 0;i<deadlockedThreads.length;i++){
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("发现死锁" +  threadInfo.getThreadName());
            }
        }
    }
    @Override
    public void run() {
        System.out.println("flag = "+flag);
        if(flag == 1){
            synchronized (o1){
                try{
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("线程1成功拿到俩把锁");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("线程2成功拿到俩把锁");
                }
            }
        }
    }
}

从代码上和问题一意义,但是加上了检测死锁机制。
4、有那些解决死锁问题的策略
1、避免策略:
2、检测与恢复策略
3、鸵鸟策略
5、讲讲经典的哲学家就餐问题

6、实际工程中如何避免死锁
1、设置超时时间(synchronized没有这个功能,需要具体工具类)
2、多使用并发类,而不用自己锁例如concurrent类和atomic
3、降低使用锁保护的范围,越小越好
4、如果能使用同步代码块就不用方法
5、起好名字,排查的时候事半功倍
6、避免锁的嵌套
7、分配资源前看能不能收回来
8、不要几个功能用一个锁
7、什么是活跃性问题?活锁、饥饿、死锁区别
什么是活锁 线程没有阻塞,始终在运行,如果程序得不到进展,线程始终运行。简单来说死锁就是两个线程都在等待互相放掉手上的资源,它只会在一开始尝试一次,而活锁则会进行不断的尝试,死锁在进行初步尝试没有结果后就会不再占用过多的资源,而活锁会。
解决活锁的方法就是设置进入随机性。
饥饿:线程需要某些资源,例如cpu,但是却始终得不到
线程的优先级设置的很低,或者某线程持有锁同时又无限循环而不释放锁,或者某程序一直占用某文件的写锁。
饥饿会导致浏览器响应器非常差,导致体验差。

synchronized各种问题

关于重入锁原理
可重入锁意味着:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值