第8章 Java多线程与并发

在这里插入图片描述进程是操作系统层面的任务并发,线程是更细粒度的任务控制,是进程中的子任务的并发。

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
前2个几乎没什么用,所以主要写第三个的两种方式

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        String value = "test";
        System.out.println("go");
        Thread.currentThread().sleep(5000);
        System.out.println(" over ");
        return value;
    }

}


public class FutureTaskDemo {

    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask<String>(new MyCallable());
        new Thread(futureTask).start();
        if (!futureTask.isDone()){
            System.out.println("还未完成");
        }
        try {
            System.out.println("futureTask.get() = " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在这里插入图片描述
第二种

public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Future<String> future = executorService.submit(new MyCallable());
        if (!future.isDone()){
            System.out.println("还未完成");
        }
        try {
            System.out.println("futureTask.get() = " + future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 一定要关闭
            executorService.shutdown();
        }
    }
}

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
下边是我自己的整理


线程得5种状态

新生状态,就绪状态,运行状态,阻塞状态,死亡状态。
球员身份,赛场就绪,抢到足球,被拌倒了(需要重新就绪),罚下场

新生状态—new的时候
就有了自己的工作空间,跟主存空间进行交互。
调用start进入就绪状态。

线程进入就绪状态得四种情况
1:start
2:解除阻塞
3:yield 高风亮节,调度到他又让出。完事直接进入就绪状态,不进入阻塞状态。不释放锁。
4:CPU得线程切换。

线程进行阻塞状态的四种原因 13称为等待,24称为阻塞,
1:sleep,抱着资源睡觉
2:wait,遇到红灯,站到一边,不占用资源
3:join,加入,插队等候
4:read,write,IO等阻塞。

死亡状态
1:正常执行完毕
2:强行终止

线程停止
在这里插入图片描述

/**
 * @author jx
 * @create 2018-11-09-19:15
 * 中止线程得两种方式
 * 1:线程正常执行完毕
 * 2:Stop/destory   不要使用
 * 3:加入标识
 */
public class StopThread implements Runnable {

    //加入标识 标记线程是否可以正常运行
    // //volatile可以保证是从内存中获取,而不是在寄存器中,这样多线程的时候,有变化可以第一时间知道
    private volatile boolean flag = true;
    private String name;

    public StopThread(String name) {
        this.name = name;
    }

    //对外提供改变flag的方法
    public void change() {
        this.flag = false;
    }


    @Override
    public void run() {
        //关联标识true  正常执行  false线程停止
        int i = 0;
        while (flag) {
            System.out.println(name + "-->" + i);
        }
    }


    public static void main(String[] args) {

        StopThread st = new StopThread("LALAAL");
        new Thread(st).start();

        for (int i = 0; i < 99; i++) {
            if (i == 88) {
                st.change();
                System.out.println("线程中止了!!!!!!!!!!!!");
            }
            System.out.println("main--->" + i);
        }
    }
}

线程休眠
在这里插入图片描述

/**
 * @author jx
 * @create 2018-10-21-23:44
 * 模拟龟兔赛跑
 */
public class Racer implements Runnable {

    private static String winner;//胜利者

    @Override
    public void run() {
        for (int steps = 1; steps <= 100; steps++) {

            if (Thread.currentThread().getName().equals("小兔子") && steps % 10 == 0) {
                try {
                // 线程休眠
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "---" + steps);

            //比赛是否结束
            boolean flag = gameOver(steps);
            if (flag) {
                break;
            }
        }


    }

    private boolean gameOver(int steps) {

        if (winner != null) {//存在胜利者
            return true;
        } else {
            if (steps == 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner = " + winner);
                return true;
            }
        }
        return false;
    }


    public static void main(String[] args) {
        Racer racer = new Racer();
        new Thread(racer, "小乌龟").start();
        new Thread(racer, "小兔子").start();
    }
}

礼让线程

在这里插入图片描述
礼让成功
在这里插入图片描述
写在哪个线程体中,哪个线程就会进行礼让。
在这里插入图片描述Join插队线程

在这里插入图片描述成功插队之后,必须插队的线程执行完,才能继续执行其他的线程

在这里插入图片描述线程的状态
在这里插入图片描述线程中的其他方法

在这里插入图片描述


线程安全

难点:保证安全 还要保证性能。要锁的准。

并发:同一资源 多个线程 同时操作。
在改的情况下,要考虑线程安全,光读没有关系。

解决:一是队列,只能由一个线程进行操作,那怎么才能知道这个线程有没有操作完呢?
加一个排他锁,比如比如sleep就有排他性。

两个条件,一个队列 一个锁,我们称之为线程同步

多个线程访问同一资源,并且某些线程还想修改这个对象,这个时候我们就要线程同步了,
其实就是一种等待机制,多个线程进入这个对象的等待池中形成队列,等待前边一个线程使用完毕后,下一个线程再进行使用。

在这里插入图片描述
Java中的锁synchronized关键字 ,排他锁。会引起性能的问题,(优化之后影响不大)
原因:
1:其他线程只能等待
2:加速释放锁,上下文切换(在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。)和调度延时。
3:如果一个优先级高的线程在等待优先级低的线程,会导致优先级倒置,引起性能问题。

Synchronized 锁的是对象,资源,
同步方法,同步块。

Synchronized 锁的是对象,资源,
同步方法,同步块。
Synchronized 锁的是对象,资源,
同步方法,同步块。

线程同步

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提供一套机制就能保证线程安全,这套机制就是synchronized关键字,它包括两种用法,synchronized方法和synchronized块。

在这里插入图片描述synchronized方法 控制对“成员变量”和“类变量”的访问,每个对象都有对应的一把锁,每个synchronized方法都必须获得 调用该方法的对象 的 锁 才能执行,否则线程阻塞,方法一旦执行就独占该锁,直到方法结束返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

若是将一个方法整个声明为synchronized,将大打印效果效率。


临界值

数据出现负数
在这里插入图片描述
B在这里操作最后一份资源,其他线程进来了一看,还有一份,先在这里等,等B真的操作完了,并且把资源数量改了,其他线程只能去修改为0的资源的数量,造成负数。

出现相同值
在这里插入图片描述线程把资源副本拷贝到自己的工作空间,正在操作或者返回的路上,其他线程来把资源拷贝到他自己的工作空间了,这时再修改,就会出现相同值得情况。


Synchronized锁成员方法,锁的只是this,是这个对象,方法里必须都是本对象的资源,我的锁才能锁的住。
静态方法:类.class
同步块:锁要操作的对象

注意:要锁不变的对象,比如你住房间,你锁房门,怎么改变里边东西都没事,但是你锁了家具,导致别人还是能到你房间里来,看见这个东西还是会出现线程安全问题。


同步块的效率优化:双重检测,1资源是否存在,2临界资源。

在这里插入图片描述同步块:synchronized(obj){}
1 obj称为同步监视器
2 obj可以是任何对象,但是推荐使用共享资源作为同步监视器(分布式要考虑分布式锁)
3 同步方法中无须指定同步监视器,因为同步方法的同步监视器是this该对象本身,或者类的模子。

同步监视器的执行过程
1:第一个线程访问,锁定同步监视器执行代码。
2:第二个线程访问,发现同步监视器被锁定,无法访问。
3:第一个线程访问完毕,解锁同步监视器。
4:第二个线程访问,发现同步监视器未锁,锁定并访问


Jdk1.7和 1.8得区别

线程安全操作并发容器
concurrent 并发包,由synchronized换成reentrantLock
CopyOnWriteArrayList 在写的基础上进行拷贝,保证写的时候是一个正确的拷贝
1.7
在这里插入图片描述1.8
在这里插入图片描述

生产者服务者

生产者服务者的模式就是实现应用层和服务层的解耦。不用一直盯着消费者,你有调用我帮你完成,你没有调用我就歇着。大部分是使用消息队列来实现的。(也有阻塞的,没有好与不好)

应用层–
服务层–
数据层–

线程通信
在这里插入图片描述还需要通信。
在这里插入图片描述
两种实现:
1:队列,也叫缓冲区。管程法,利用管道和容器。
在这里插入图片描述2:信号灯法,标识法。
在这里插入图片描述在这里插入图片描述在这里插入图片描述


Happen before 指令重排

在这里插入图片描述计算机得运算过程
在这里插入图片描述1:从内存中获得要执行得下一条指令
2:对指令进行解码翻译。从寄存器中取值
3:拷贝到主存中,进行一系列得运算操作
4:将结果写回到寄存器中。
在这里插入图片描述注意,计算机为了效率进行指令重排,但是在多线程中,重排后得结果可能不正确,造成线程不安全。单线程肯定是等结果,所以没有影响。

执行代码的顺序可能与编写代码的顺序不一致,既虚拟机优化代码顺序则为指令重排
Happen before编译器或者运行时环境为了优化程序性能而采取的对指令进行重排的一种手段。
在虚拟机层面,为了尽可能地减少内存操作速度远慢于CPU运行速度所带来的CPU空置影响,虚拟机将会按照自己的规则编写的程序规则打乱,写在后边的代码有可能先执行,写在前边的后执行,以尽可能充分的利用CPU。
比如有两个操作,第一个运行很慢,那就很有可能会会先执行第二个操作,提前使用CPU来加快整体效率。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
在硬件层面上,CPU会将接收到一批指令按照其规则进行重排序,同样是基于CPU速度比缓存快的原因,和上一点目的类似,只是 硬件处理的话每次只能在接收到的有限的指令范围内进行重排序,而虚拟机可以在更大的层面,更多的指令范围内进行重排序。

数据依赖

在这里插入图片描述指令重排代码
在这里插入图片描述甚至
在这里插入图片描述

按照代码预期
System.out.println(“HappenBefore.main -->” + a );
不可能执行,但是存在指令重排。不光执行了,而且结果还为1.
因为指令重排得时候a的值还没回来。


Volatile

Volatile —轻量级的synchronized
Synchronized:保证并发和同步
Volatile只保证了同步的数据的可见,实现了部分功能

保证线程之间的变量的可见性,当线程a对变量x进行修改之后,在线程a后边执行的其他线程都能看到变量x的变化。就是说要符合以下两个规则
1:线程对变量进行修改之后,要立刻写回到主内存中。
2:线程对变量读取的时候,要从主内存中读,而不是缓存中。

主内存访问过高肯定影响效率,所以工作内存有必要存在。

在这里插入图片描述
各线程的工作内存间彼此独立,互不可见,在线程启动的时候,虚拟机为每一块内存分配一块内存空间,不仅包含了内部定义的局部变量,也包含了线程需要的共享变量(非线程内构造对象)的副本,即为了提高执行效率。

Volatile能保持变量的可见性,但是不能保持原子性。(要么都执行,要么都不执行)
比如上一个例子中
在这里插入图片描述这四步就是要保证原子性操作。但是volatile不能保证。
不过可以使用CAS来控制。

不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁),仅仅set或者get的场景是适合volatile的


内存屏障

内存屏障(memory barrier)是一个CPU指令。

基本上,它是这样一条指令:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果)。

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。
插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

内存屏障另一个作用是强制更新一次不同CPU的缓存。

例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障(memory barrier)和volatile什么关系?

上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,

你必须知道:
1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

在这里插入图片描述
回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

CAS是基于乐观锁的,也就是说当写入的时候,如果寄存器旧值已经不等于现值,说明有其他CPU在修改,那就继续尝试。所以这就保证了操作的原子性。(旧值,新值,预期值)

在这里插入图片描述并不能保证num=1是原子操作,但是可以保禁止指令重排


DCL单例模式

/**
 * @author jx
 * @create 2018-11-12-16:21
 * <p>
 * 单例模式 多线程情况下,对外存在一个对象  懒汉式基础上加入并发控制
 * 1:构造器私有化-->避免外部new构造器
 * 2:内部提供一个私有的静态属性-->为了让外部可以new对象,存储对象的地址
 * 3:提供共有的静态方法-->获取属性
 */
public class DoubleCheckedLocking {

    //这里如果一上来就new一个,就是饿汉式,如果没有new就是懒汉式
    private static volatile DoubleCheckedLocking instance;

    private DoubleCheckedLocking() {
    }

    public static DoubleCheckedLocking getInstance(long time) {
        //dcl权限控制  双重检测
        if (null != instance) {
            return instance;
        }
        synchronized (DoubleCheckedLocking.class) {
            if (null == instance) {
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //指令重排有可能发生在此处
                instance = new DoubleCheckedLocking();
                //new一个对象的时候,可能发生3件事情
                // 1:开辟空间
                // 2:初始化对象信息
                // 3:返回对象的地址给引用
                //在2.3之间如果发生指令重排
                //没有volatile 其他线程可能会访问一个没有初始化的对象

            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println(DoubleCheckedLocking.getInstance(500));
        });
        t.start();
        System.out.println(DoubleCheckedLocking.getInstance(1000));
    }
}



Thread Local

表示的是每个线程自身的存储区域,也就是每个线程自己的一亩三分地。举个例子,一个银行,每个用户是一条线程,每个线程都有自己的Thread Local,每个用户都有自己的保险柜一样,类似于map的K-V,K是线程的信息,V就是对应的存储内容。好处就是每个线程相互独立,又可以共享这个内存区域。也可以保证在多线程的情况下,每个线程存储的数据的安全。
每个数据只有自己能看到,别的线程是看不到的。
这个大的Thread Local 官方建议用private static 来修饰。来实现线程安全,get/set/initialValue三个方法。。
一般用在跟自己线程相关的东西,比如每个线程有自己数据库的链接,操作自己的,大家不影响。或者每一个线程有自己的用户登录信息。
在这里插入图片描述在这里插入图片描述在这里插入图片描述


可重入锁

作为并发共享数据保证一致性的工具。大多数内置锁都是可重入的,也就是说,如果一个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值+1,当线程退出同步代码块的时候,计数器将会递减,当计数器的值等于0的时候,锁释放。在没有可重入锁的支持,第二次企图获得锁时会进入死锁状态。
就比如你已经有了进入房子的钥匙,那你就可以进入卫生间,进入厨房。

在这里插入图片描述

核心原理
改成可重入锁,锁可以延续使用
实现机制,就是看看这个进来的线程是不是锁定的线程,如果是,就不用等
直接用 每个锁都有一个计数器
在这里插入图片描述


CAS

锁分为两类,悲观锁和乐观锁

悲观锁:synchronized是独占锁,悲观锁,会导致其他需要锁的线程被挂起,等待持有锁的线程释放锁。
乐观锁:每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

乐观锁实现:
比较并交换

有三个值,当前内存中的值V,旧的预期值A,将要更新的值B,先获得内存中当前的内存值V,将V与A进行比较,要是相等就改为B,并返回true,否则什么都不做,返回false。
CAS是一组原子操作,不会被外部打断。
属于硬件级别的操作(利用CPU的CAS指令,同时借助JNI来完成非阻塞算法)比加锁效率高。

ABA问题:如果变量V初次读取是A,并且在准备赋值的时候还是A,那就说明他一定没被其他线程修改过吗?如果这期间他被改为其他值,之后又被修改为A,那CAS就会误认为他没有被修改过。

在这里插入图片描述
解决办法

看版本号是否相同,如果相同,才会更新难到数据库,解决ABA问题。
CAS的自旋。循环体中做了三件事
1:获得当前值
2:当前值+1,计算目标值。
3:进行CAS操作,成功则跳出循环,失败重复上述步骤。

如何保证获得的当前值是内存中的最新值呢?很简单,用volatile关键字来保证。

下文例子借鉴小灰中的例子 致敬刘欣大佬

假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。

由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。

理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。

线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。

线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。

线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50。

我钱没了50!!!!!!!!!

在这里插入图片描述
真正要做到严谨的CAS机制,我们在比较阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。

在这里插入图片描述Native JUC技术 在这里插入图片描述是硬件技术 效率很高。

在这里插入图片描述


多线程总结

其实就是多条路,有了多线程,程序和程序之间,代码和代码之间,就可以不用等待了,同时进行了。

多线程实现的方式

1:实现runnable 重写run方法
必须借用静态代理,来一个thread对象,放一个真实对象,放一个线程对象。
2:继承thread类 重写run方,调用start方法。
子类对象,调用start方法
3:实现callable接口重写call方法

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值