最详细的多线程理解

计算机组成的简单概念

在这里插入图片描述
主要是由cpu和内存构成。
接下来以一个例子讲解一下:

  • 程序的执行
    当你打开QQ.exe时,它会进入内存中,这时叫一个进程。那么这个 QQ.exe内部是其它语言写的,当它进入到内存中,都会变成0和1的二进制数。,而从内存读入到cpu是经过总线的。那么怎么区分一段01组成的数据到底是数据还是指令呢?
    总线分为:控制线,地址线,数据线,也就是说从相应的总线读到cpu里的数据就是该总线对应的的数据。结合上面的图进行分析:
    cpu读到通过控制线读到一条指令到PC,然后根据相关的指令去读数据到Registers(寄存器),然后放到ALU(运算单元)它在同一时刻只能只做一件事。然后把结果放入到对应的寄存器中,最后在写回到内存中。(这个过程对于理解线程很重要)

总结:就一句话:一个程序的执行,首先把可执行文件放入到内存,找到起始(main)的地址,逐步读出指令和数据,进行计算并写回到内存。

  • 计算机的三级缓存:
    在这里插入图片描述

工作原理:
缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
什么是数据块呢?
就是当读取内存中的数据时他不回一次只读取实际需要的数据,而是把需要数据的那一整段数据都都进来,那么这一段数据放入到缓存中又被成为缓存行。(内存中的数据其实都是被分为一段一段的)那么到底多少合适呢?经过工业实践得来一段数据的大小是64个字节,一个字节8位。
那么为什么是64个字节呢?
根本原因是:当太小了,cpu从内存中读的速率会变高,但是数据在三级缓存中的命中概率会变低,当太大了,cpu从内存中读的速率会变低,但是数据在三级缓存中的命中概率会变高,权衡了两者在根据实践最终选定一段数据为64个字节。这个选定跟hashmap中的负载因子为什么是0.75和转化为红河红黑树的条件之一为什么是链表节点为8相似。有兴趣的可以看看。
补充一点:cpu的速率和内存的速率(100:1)

  • 由于缓存行的存在,当多个线程访问同意个资源时,都会从主内存中读取数据块到各自的工作内存中,当其中一个线程把数据块的一个数据修改了,那么怎么把修改后的数据同步到其它线程中的工作内存中呢?
    这时就出现了缓存一致性协议:那么这个过程是怎么实现的呢?
    简单一点就是:把修改后的数据立刻写回到主内存中,线程和主内存之间的通信是通过总线,当修改后的数据经过总线时,cpu的总线嗅探机制会时刻监听,有任何风吹草动它都能检测到同时清除工作内存中的数据,当cpu去从工作内存中读数据时发现没有,这时它会重新从主内存读,这个过程几乎时同时的,非常快。

超线程(为什么一个cpu可以执行多个线程?)

正常情况下:一个cpu只能执行一个线程,因为ALU只有一个,它在同一时刻只能执行一个指令。
大家应该都听过四核八线程。它到底是什么意思呢?
首先看一下什么是多核cpu
在这里插入图片描述
这四个核集成在同一个芯片上,来看一下Intel Core i7处理器的一个组织结构,这个微处理器芯片中有4个CPU核,每个核中都有它自己的L1和L2缓存。
在这里插入图片描述
四核八线程:
在这里插入图片描述

虽然一个ALU同一个时间只能运行一个线程,但是一个ALU可以对应两套Registers和PC,每一套可以放入一个线程,ALU在两个线程之间交互执行,不像原来一样线程来回切换。原来是这个线程必须拿出去后,其它线程才能进来,被ALU计算。多核就避免了线程的来回切换。

对象的创建过程

在这里插入图片描述

上面一张图是什么意思呢?一张图搞定
在这里插入图片描述
也就是当new一个对象时总共三步。但是这里面会有一个问题:指令重排,那么为什么会出现指令重排呢?
主要是为了使cpu的效率最大化。假如cpu执行了一个io或者其它耗时的操作时,它需要在那等待,这个等待时间cpu啥都没干,于是为了不让cpu空闲就会发生指令重排,让它先把不费时间的先执行。

注意:为什么图片中的m是在创建对象时才被赋值呢?这就跟各种类型变量的初始化顺序有关了。

常量 -->早于 -->static -->早于 -->普通变量 。只有static在类加载过程被赋值,常量早在编译阶段就已经赋值,普通变量在创建对象的时候被赋值。因此static修饰的内容不属于对象。 但是对象可以访问static的内容。
  • 验证指令重排的存在
package xiancheng;

public class TestChongPai {
    static int a;
    static int b;
    static int x;
    static int y;
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 100000000; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread t1 = new Thread(()->{
                a = 1;
                x = b;
            });

            Thread t2 = new Thread(()->{
                b = 1;
                y = a;
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("第"+i+"次"+"x="+x+"y="+y);
            if (x == 0 && y == 0) {
                System.out.println("x="+x+"y="+y);
                break;

            }
        }


    }
}

如果是正常按顺序执行的话是不会出现x和y都等于0的,只有当两个线程都换了顺序(注意:单个线程之间的乱序的前提是不能影响最终结果)才会出现结果如下:
在这里插入图片描述

在这里插入图片描述

指令重排序遵守两个规则:
a. as-if-serial语义:

  1. 单个线程,两条语句,未必是按照顺序执行
  2. 单线程的重排序,必须保证最终一致性
  3. 那么cpu怎么知道这两条语句换了顺序之后到底影不影响最终个结果呢?其实在执行代码之前会有语义分析,它规定了如果前后两条语句之间存在依赖关系,那么就不能重排序。

b. Happens-Before原则:
点击了解详情
如何避免指令重排带来的问题呢?

  • 禁止编译器乱序
  • 使用内存屏障阻止指令乱序执行
    一个图搞定内存屏障:
    在这里插入图片描述
    这是四种内存屏障,很简单如果你想阻止两条读的指令你就在中间加一个LL屏障,如果你想阻止两条读写不要乱序,你可以加LS屏障,依次类推。volatile底层就是实现了内存屏障所以它可以保证有序性,但是synchronized底层并没有实现内存屏障

那么内存屏障是怎么映射到cpu里的呢?也就是说cpu是如何实现jvm中的内存屏障的?

其实cpu是根据汇编语言lock指令执行的。

前面讲的内容是为了更好的理解多线程,接下来讲多线程的内容

线程,进程的概念

点击详细了解

进程是静态的概念:程序进入内存,分配对应的资源:内存空间,进程进程进入内存同时产生一个主线程。
线程是动态的概念:是可执行的计算单元(任务)。
一个ALU同一个时间只能执行一个线程。
同一段代码为什么可以被多个线程执行?
因为虽然是同一段代码但是里面的数据可能并不是同一个数据了。

为什么使用多线程?

多线程指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务

更高的运行效率,——并行;
多线程是模块化的编程模型;
与进程相比,线程的创建和切换开销更小;
通信方便;
能简化程序的结构,便于理解和维护;更高的资源利用率。

多进程和多线程区别?

多进程:操作系统中同时运行的多个程序

多线程:在同一个进程中同时运行的多个任务

在学习多线程之前一定要弄清楚并发和并行

  • 并发:指两个或多个事件在同一个时间段内发生
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程的生命周期?

话不多说,来一张图,至关重要
在这里插入图片描述
线程有五种状态。

  1. 创建(New):至今尚未启动的线程的状态。线程刚被创建,但尚未启动。如:Thread t = new MyThread();
  2. 调用start方法进入就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
  3. 调用run方法将进入运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1)等待阻塞—位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。

2)同步阻塞 --位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。

3)其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

实际操作来观察这五个状态

package xiancheng;

public class TestStatus {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("///");
        });

        //观察状态--创建状态
        Thread.State state = thread.getState();
        System.out.println(state);//NEW

        //启动状态--就绪状态
        thread.start();
        state = thread.getState();
        System.out.println(state);//RUNNABLE

        /*
        * 打印出线程的阻塞和结束状态
        * 具体过程说明:当进入RUNNABLE状态时会进入while循环,这时候主线程休息了0.1s
        * 于是开始执行thread线程,进入循环要休息0.3s,当主线程过了0.1s时thread线程仍在休息,
        * 于是开始获取这时的状态打印为TIMED_WAITING,然后一值重复同样的动作知道出现一个点,
        * 就是thread线程执行完了打印出///就证明此时为Running状态,最后线程进入结束状态
        * 获取该状态打印为TERMINATED
        * */
        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);
        }


    }
}

输出结果:
在这里插入图片描述

线程的三种实现方式

  1. 继承Thread类
package xiancheng;
/*
* 本测试是在八核的情况
*
* */
public class TestSanType {
    public static void main(String[] args) {
        Type1 type1 = new Type1();
        type1.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程");
        }
    }
}

class Type1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是副线程的方法");
        }
    }

}
  1. 实现Runnable接口
package xiancheng;
/*
* 本测试是在八核的情况
*
* */
public class TestSanType {
    public static void main(String[] args) {
      /*  Type1 type1 = new Type1();
        type1.start();*/
        Type2 type2 = new Type2();
        new Thread(type2).start();//通过代理模式调用start方法
        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程");
        }
    }
}

/*class Type1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是副线程的方法");
        }
    }
}*/

class Type2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是副线程的方法");
        }
    }
}
  1. 实现Callable接口
  • 实现Callable接口,需要返回值类型
  • 重写call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务
  • 提交执行
  • 获取结果
  • 关闭服务
package xiancheng;


import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/*
* 本测试是在八核的情况
*
* */
public class TestSanType {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
      /*  Type1 type1 = new Type1();
        type1.start();*/
      /*  Type2 type2 = new Type2();
        new Thread(type2).start();//通过代理模式调用start方法
        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程");
        }*/

        //创建三个对象
        Type3 t1 = new Type3("https://img2.baidu.com/it/u=1070003001,653753576&fm=26&fmt=auto&gp=0.jpg","图片1.jpg");
        Type3 t2 = new Type3("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fs6.sinaimg.cn%2Forignal%2F4a3f1f064effcc6ebf4a5&refer=http%3A%2F%2Fs6.sinaimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1633343985&t=869d08110191ec5973e919f32b7d3d01","图片2.jpg");
        Type3 t3 = new Type3("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic15.nipic.com%2F20110806%2F6990916_041336778000_2.jpg&refer=http%3A%2F%2Fpic15.nipic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1633344116&t=e46eaf06bb5a1dc582e596ea76905fa5","图片3.jpg");

        // 创建执行服务
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = executorService.submit(t1);
        Future<Boolean> r2 = executorService.submit(t2);
        Future<Boolean> r3 = executorService.submit(t3);

        //获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        //关闭服务
        executorService.shutdownNow();
    }
}

/*class Type1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是副线程的方法");
        }
    }
}*/

/*
class Type2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是副线程的方法");
        }
    }
}*/

class Type3 implements Callable{
    private String url;
    private String name;
    @Override
    public Boolean call() {
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downLoader(url,name);
        System.out.println("下载了文件名为"+name);
        return true;
    }

    public Type3(String url, String name) {
        this.url = url;
        this.name = name;
    }
}

class WebDownLoader{
    //下载方法
    public void downLoader(String url,String name)  {
        try {
            FileUtils.copyURLToFile(new URL(url),new File((name)));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("io异常");
        }
    }
} 

三种创建方式已经讲完了,接下来看各自的优缺点

请点击

接下来讲讲Thread是怎样代理的,一个程序就能明白(这里用的是静态代理)

package xiancheng;

//模拟代驾,,你喝酒了,代驾公司代理你去开车
public class TestThreadPro {
    public static void main(String[] args) {
        new Pro(new Per()).driver();
    }
}

//代驾,就是一个抽象角色:一般会使用接口或者抽象类
interface Driver{
    public void driver();
}


//你喝酒了不能开车了
class Per implements Driver{

    @Override
    public void driver() {
        System.out.println("喝酒了,找代驾帮开车");
    }
}
//代驾公司帮你开车
class Pro implements Driver{
    private Per per;
    @Override
    public void driver() {
       per.driver();
        check();
        money();
    }

    public Pro(Per per) {
        this.per = per;

    }
    //我要检查车的情况
    public void check() {
        System.out.println("代驾之前看车子的情况,避免车子有问题");
    }
    //送完顾客后要钱
    public void money() {
        System.out.println("安全到家,掏钱吧");
    }
}

/*
 * 总结:这是静态代理模式
 * 优点:1.可以使真实角色的操作更加的纯粹,不用去关注一些公共的业务
 * 2.公共业务就交给代理角色,实现了业务的分工
 * 3.公共业务发生扩展的时候,方便集中管理
 * 缺点:一个真实角色就会产生一个代理角色,代码量会翻一倍开发效率会变低
 * (这个缺点可以采用动态代理去解决 )
 *
 * */

对比Thread
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Lambda表达式

点击了解

再来一张图:
在这里插入图片描述

  • 使线程停止
  1. 不推荐使用jdk提供的stop,destroy方法(已经废弃)
  2. 推荐线程自己停下来
  3. 建议使用一个标志位进行终止变量,当flag=false,则线程终止运行
package xiancheng;
//建议线程正常停止,利用次数
//建议使用标志位
//不建议使用stop,destroy等过时的方法
public class TestStop implements Runnable{
    private Boolean falg = true;
    @Override
    public void run() {
        while (falg) {
            System.out.println("正在运行");
        }
    }
    //公共的标识位转换
    public void stopThread() {
        this.falg = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        //利用循环让线程正常停止
        for (int i = 0; i <= 1000; i++) {
            System.out.println("main"+i);
            if (i == 998) {
                //停止线程
                testStop.stopThread();
                System.out.println("停止了");
            }
        }
    }
}
  • 线程休眠
    在这里插入图片描述
package xiancheng;
//模拟网络延时
public class TestSleep implements Runnable{
    int ticker = 10;
    @Override
    public void run() {
        while (true) {
            if (ticker <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "拿走了第" + ticker--);
        }
    }

    public static void main(String[] args) {
        TestSleep testSleep = new TestSleep();
        new Thread(testSleep).start();
        new Thread(testSleep).start();
        new Thread(testSleep).start();
    }
}

package xiancheng;

import java.text.SimpleDateFormat;
import java.util.Date;

//模拟时间
public class TestSleep2 {
    public static void main(String[] args) {
        Date date = new Date(System.currentTimeMillis());
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("hh:mm:ss").format(date));
                date = new Date(System.currentTimeMillis());//更新时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

  • 线程礼让
    礼让并不一定成功,但是会增加线程被cpu调用的概率
package xiancheng;

public class TestYiled implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"停止执行");
    }

    public static void main(String[] args) {
        TestYiled testYiled = new TestYiled();
        new Thread(testYiled,"a").start();
        new Thread(testYiled,"b").start();
    }
}

在这里插入图片描述
在这里插入图片描述

  • join
package xiancheng;

public class Testjoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("vip线程执行");
        }
    }

    public static void main(String[] args) {
        Testjoin testjoin = new Testjoin();
        Thread thread = new Thread(testjoin);
        thread.start();

        for (int i = 0; i < 20; i++) {
            if (i == 10) {
                try {
                    //主线程必须要等到vip线程执行完后才能执行
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main线程");
        }
    }
}

  • 设置优先级
    它会增大cpu调用该线程的概率,并不代表一定是先执行该线程。
package xiancheng;

public class TestPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"的优先级为"+Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        TestPriority testPriority = new TestPriority();
        Thread a = new Thread(testPriority, "a");
        Thread b = new Thread(testPriority, "b");
        Thread c = new Thread(testPriority, "c");

        //设置优先级默认都为5
        a.setPriority(6);
        b.setPriority(10);
        c.setPriority(1);
        a.start();
        b.start();
        c.start();

    }
}

在这里插入图片描述
在这里插入图片描述

  • 守护线程和用户线程
    用户线程就是用户自己写的,守护线程会一直保护着用户线程直到它消亡,过一段时间jvm才会把守护线程干死,想要设置线程为守护线程可以用setDaemon()方法

点击了解详情

  • 线程同步:多个线程操作同一个资源
    在这里插入图片描述
    解决方法:synchronized关键字
    在这里插入图片描述
    解决ArrayList线程不安全问题:
package xiancheng;

import java.util.ArrayList;
import java.util.List;

public class TestArrayList{
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
//        创建100个线程,每个线程都往list中增加一个元素
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}

输出结果:
在这里插入图片描述

按程序的逻辑来说最后打印的应该是100,但为什么是98呢?这就是ArrayList的线程不安全。当两个线程执行这行代码list.add(Thread.currentThread().getName());时它只会存一个进去
具体原因是要看ArrayList源码的
点击了解详情

**synchronized有两种方法:

  1. 同步方法,就是在方法前加synchronized关键字
  2. 同步代码块就是:
    在这里插入图片描述
package xiancheng;

import java.util.ArrayList;
import java.util.List;

public class TestArrayList{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
//        创建100个线程,每个线程都往list中增加一个元素
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

synchronized关键字是隐式的锁,对于锁的对象不好确定,可能会让你困惑,还有就是不知道锁的开始和结束,于是就出现了Lock锁被成为显式锁
在这里插入图片描述
举例:多人买票导致有的人会拿到-1

package xiancheng;

public class TestLock1 implements Runnable{
    static int ticketNums = 10;
    @Override
    public void run() {
        while (true) {
            if (ticketNums > 0) {
                System.out.println(ticketNums--);
            }else {
                break;
            }
        }
    }

    public static void main(String[] args) {
        TestLock1 testLock1 = new TestLock1();
        Thread thread1 = new Thread(testLock1,"小明");
        Thread thread2 = new Thread(testLock1,"小红");
        Thread thread3 = new Thread(testLock1,"小王");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这样写,出现-1的情况几乎为0,这时可以用sleep来方法问题的发生性

package xiancheng;

public class TestLock1 implements Runnable{
    static int ticketNums = 10;
    @Override
    public void run() {
        while (true) {
            if (ticketNums > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ticketNums--);
            }else {
                break;
            }
        }
    }

    public static void main(String[] args) {
        TestLock1 testLock1 = new TestLock1();
        Thread thread1 = new Thread(testLock1,"小明");
        Thread thread2 = new Thread(testLock1,"小红");
        Thread thread3 = new Thread(testLock1,"小王");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:
在这里插入图片描述
出现了0和-1,这就出现了对于多个线程访问同一个资源会出现数据不安全的情况
可以用synchronized关键字,接下来讲的是Lock锁解决不安全的问题

package xiancheng;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock1 implements Runnable{
    static int ticketNums = 10;
    //定义锁(可重入锁)
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            lock.lock();//加锁

            if (ticketNums > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//解锁
                }
                System.out.println(ticketNums--);
            }else {
                break;
            }
        }
    }

    public static void main(String[] args) {
        TestLock1 testLock1 = new TestLock1();
        Thread thread1 = new Thread(testLock1,"小明");
        Thread thread2 = new Thread(testLock1,"小红");
        Thread thread3 = new Thread(testLock1,"小王");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:
在这里插入图片描述
结果正常,为什么会正常究其根本是让多个线程老老实实的排队了。

Lock锁的写法和与synchronized的比较
在这里插入图片描述

在这里插入图片描述

说明:CopyOnWriteArrayList数组之所以是线程安全的是因为它实现了可重入锁
在这里插入图片描述

  • 死锁
    在这里插入图片描述
package xiancheng;

//多个线程相互抱着对方的资源(锁)相互等待对方释放锁,形成了死锁
public class TestLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"姑娘1");
        Makeup g2 = new Makeup(1,"姑娘2");
        g1.start();
        g2.start();

    }
}

//口红
class Lipstick{

}
//镜子
class Mirror{

}
//化妆
class Makeup extends Thread{
    //需要的资源只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice;//选择
    String girlName;//使用化妆品的人

    Makeup(int choice,String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //相互持有对方的锁
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick){
                System.out.println(this.girlName+"获的口红的锁");
                Thread.sleep(1000);
                synchronized (mirror) {
                    System.out.println(this.girlName+"获得镜子的锁");
                }
            }

        }else {
            synchronized (mirror){
                System.out.println(this.girlName+"获得镜子的锁");
                Thread.sleep(2000);
                synchronized (lipstick) {
                    System.out.println(this.girlName+"获的口红的锁");
                }
            }

        }
    }
}

在这里插入图片描述
如何避免死锁?

  1. 避免一个线程同时获取多个锁
  2. 尽量缩小锁的作用域范围
  3. 使用定时锁

死锁的概念已经讲完了

线程通信

  1. 可以用标志位(信号灯法),当一个线程达到了触发另一个线程的开始条件,就可以实现通信
package xiancheng.tongxin;

//根据条件实现通信
public class Tset1 {
    static  int i;
    public static void main(String[] args) {


        Thread thread1 = new Thread(() -> {
            for (i = 0; i < 10; i++) {
                System.out.println(i);
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (i == 10) {
                System.out.println("线程thread1通知输出" + i);
                break;
            }
        });
thread1.start();
thread2.start();
    }
}

在这里插入图片描述

重点:不知道你们注意到没,这个程序中的i竟然实现了可见性,其实是因为println这个方法,它用了synchronized关键字,它会把工作内存修改过的数据同步到主内存,同时强行把另一个线程中的工作内存中的该值同步为主内存数据
在这里插入图片描述
但是我遇到了一个问题:
在这里插入图片描述
i竟然还是可见性,如果有知道的请说说你的想法!!!!

  1. 用join的方式通信:
    join其实可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。
  2. volatile实现通信:其实在上面一个程序中如果没有了println方法就不能实现通信了,当你加上volatile就可以保证可见性实现通信。
  3. synchronized或者Lock锁也可以实现通信,
    点击了解详情
  4. 等待/通知机制
    在这里插入图片描述
package xiancheng.tongxin;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}
//生产者
class Productor extends Thread{
    SynContainer container;
    public  Productor(SynContainer container) {
        this.container = container;
    }
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了"+i+"只鸡");
            container.push(new Chicken(i));
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container) {
        this.container = container;
    }
    //消费
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了"+container.pop().id+"只鸡");
        }
    }
}
class Chicken{
    int id;//产品编号
    public Chicken(int id) {
        this.id = id;
    }
}
//缓冲区
class SynContainer{
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];
    //容器计数器
    int count = 0;
    //生产者放入产品
    public synchronized void push(Chicken chicken) {
        //如果容器满了,就需要等到消费者消费
        if (count == chickens.length) {
            //通知消费者消费,生产者等待
            try {
                this.wait();//释放了锁。sleep不会释放锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果没有满,就往里继续放入产品
        chickens[count] = chicken;
        count++;

        //可以通知消费者了
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Chicken pop() {
        //判断能否消费
        if (count == 0) {
            //等待生产者生产消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消费
        count--;
        Chicken chicken = chickens[count];

        //吃完了,通知生产者生产
        this.notifyAll();

        return chicken;
    }
}

至此多线程的通信讲完了

线程池

线程池面试题

JMM内存模型

几张图搞定
在这里插入图片描述

在这里插入图片描述
以下这张图是volatile可见性的原理图:

在这里插入图片描述
上面的实现是根据下面这张图:
在这里插入图片描述

具体过程请点击

锁的介绍

点击了解详情

在了解有关多线程锁的内容之前务必点击上方的链接:本人看完上方的链接内容后做了一点小的总结:主要是对偏向锁/轻量级锁/重量级锁的一个理解:这三种锁其实是锁的三种不同的状态。

  1. 偏向锁就是指当多个线程同时访问一把锁的时候,最先获得这把锁的那个线程,在之后的访问中这把锁就钟爱于你了。该线程会自动的获取锁。可以降低获取锁的代价。
  2. 轻量级锁其实就是指当锁为偏向锁时,又被其它线程访问,那么这时锁的状态就变为轻量级锁。其它线程会以自旋的方式去尝试访问该锁。不会阻塞,可以提高性能。何为自旋呢?其实就是为了减少线程挂起和恢复的开销。其实大部分线程在等待锁的时候,很短的时间段就可以获得,如果在这一时间段让线挂起然后在恢复反而效率和性能会变低,所以就采用自旋的方式让该线程在等待的时候不要立刻就进入阻塞状态,而是让它不断的循环去尝试获得该锁
  3. 重量级锁其实就是指当锁的状态为轻量级锁,被其它线程访问时,这时线程会自旋不断的尝试获取该锁。但是自旋肯定是有次数的,当自旋的次数已经完了,但是还是没有获取到该锁,那么该线程就会发生阻塞,这时锁就会膨胀为重量级锁。降低性能。
    来兴趣了,在来讲讲悲观锁和乐观锁
  4. 悲观锁就是对于锁的一种非常非常消极的状态。它认为对于同一个数据进行并发操作时,该数据肯定会发生改变,造成数据的不一致或者脏读的情况。那么它认为这肯定是不行的,不允许这样的情况出现。于是就对该数据加锁,多被用于有大量的写操作时的场景下。在java中的使用就是使用java中的各种锁。
  5. 乐观锁就是对于锁的一种非常非常积极的状态。它认为对于同一个数据进行并发操作时,该数据肯定不会发生改变的。适用于读操作很多的场景下,当然了它肯定不会用java中的各种锁,它是通过编程去控制的,也被成为无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

在带你们了解一下CAS算法:

点击了解详情

它的实现逻辑:
在这里插入图片描述
具体代码:(这个代码只是讲解了CAS的逻辑实现)

package xiancheng;

import java.util.concurrent.atomic.AtomicInteger;

public class TestCAS {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();//原子操作
        atomicInteger.incrementAndGet();//底层的实现其实跟以下几行代码的逻辑是一样的


        while (true) {//当比较时发现不相等,那么该线程就会更新主内存中的值到该线程的工作副本中(底层是用volatile实现的所以它具有可见性),然后重新比较直到把
            //本线程的值更新到主内存中为止
            int oldValue = atomicInteger.get();//获取主内存中的值
            int newValue = oldValue + 1;//把该值加1
            if (atomicInteger.compareAndSet(oldValue,newValue)) { //把从主内存中拿出来的值和主内存的该值进行比较
                //如果相同说明没有其它线程对该值修改,那么就把现在主内存的该值替换成这个处理之后的值
                break;
            }

        }
    }
}

在这里插入图片描述

看一下源码:
incrementAndGet()的源码;
在这里插入图片描述

在这里插入图片描述

compareAndSet(oldValue,newValue)的源码:

在这里插入图片描述

为什么它是原子性的呢?这是因为在最底层它使用汇编指令cmpxchg执行的,cpu对于汇编指令当然是一次性执行了。java中一句代码是由多个指令完成的,所以会有指令重排,多线程访问同一数据造成不安全的情况。

由于是一次性就执行完了,所以原子操作效率很高。但是它的循环时间可能会很长,具有局限性(只能保证一个共享变量的原子操作,也就是说一次只能进行一个操作,那么若有多个操作都需要进行原子操作就需要用到锁了),会出现ABA问题,其实就是:当两个线程都拷贝了主内存的值到自己的工作内存中,其中一个线程对该值进行了很多的操作,但是最后该值还是主内存中的值,那么这时另一个线程在比较时自然就发现相等了,但是其实该值已经被操作很多次了。就是一种假象。(点击了解详情)。

至此多线程就讲解完了,后期会不定期的继续扩充多线程的知识。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值