【JavaSE】多线程基础

目录

一、进程(process/task)

1.1 进程的概念

1.2 进程控制块对象PCB

1.2.1 操作系统如何管理进程?

1.2.2 PCB里面的内容

1.3 进程调度

1.4 进程的独立性

1.5 进程间通信机制

1.6 进程的缺点

1.7 解决方案

二、线程概念

2.1 概念

为什么要实现并发编程?

多线程实现并发编程的优势:

2.2 进程和线程的区别

2.3 线程出现的问题

三、创建线程

3.1 继承Thread类

jconsole工具

3.2 实现Runnable接口

3.3 使用匿名内部类

3.4 使用lambda表达式

3.5 感受多线程的优势(增加运行速度)

四、Thread类的常见属性和常用方法

4.1 Thread的几个常见属性

4.1. run方法和start方法的区别

4.2 中断新线程

①自定义一个标志位

②采用Thread提供的标志位

 线程的代码处理interrupt方法有三种方式

​为什么不将interrupt设计为一调用就结束线程?

4.3 等待一个线程(join)

4.4 获取当前线程引用

4.5 休眠当前线程

五、线程的状态

 六、线程安全问题(重点哦)

6.1 经典线程不安全示例

6.2 线程不安全的原因

①根本原因,线程的抢占式执行(这个无可奈何)

②两个线程在修改同一个变量

③线程针对变量的修改操作不是原子性的

④内存可见性(重点)

⑤指令重排序

6.3 synchronized的具体使用用法

①直接加到一个普通的方法上,进入方法就相当于加锁,出了方法就相当于解锁。(锁对象还是this)

②加到一个代码块上,需要手动指定一个锁对象。

③加到一个static方法上,此时相当于指定了当前的类对象为锁对象。

可重入锁:

七、集合类的线程安全问题

synchronized和volatile的区别:

八、JMM(Java Memory Model,Java存储模型/Java内存模型)

九、控制线程的执行顺序-wait notify机制

注意1:wait需要搭配synchronized适用

注意2:如果有多个线程都在等待,调用一次notify,只能唤醒其中的一个线程,具体唤醒的是谁,随机的。

注意3:如果没有任何线程等待,直接调用notify,不会有副作用。


一、进程(process/task)

1.1 进程的概念

一个跑起来的程序,就称为进程。进程也可以认为是一个可执行文件跑起来之后动态的过程。

进程运行后会被系统分配一些资源。

进程是系统资源分配的基本单位。

1.2 进程控制块对象PCB

1.2.1 操作系统如何管理进程?

操作系统要保证这么多进程,每个都能正常的进行工作,而且互相之间并不影响。他的管理分为两个部分,即:管理=描述+组织。

描述:使用PCB(process control block)这样的结构体来表示一个进程的相关属性。

组织:使用一定的数据结构把正在执行的进程给串起来。(在Linux中,使用的是双向链表,windows并不开源,所以不清楚)

当我们双击一个exe运行一个进程的时候,操作系统内核就会创建PCB,并把这个PCB加到双向链表中。

当关掉一个程序时,操作系统就会找到对应的PCB,并且从链表上删掉。像任务管理器,能够显示出当前所有的进程信息,就是在遍历这个链表。

(这样写有点不准确,后文会再重新描述一下)

1.2.2 PCB里面的内容

pid:进程的身份标识。

内存指针:表示进程的代码段和数据段都在哪里。exe文件存储在硬盘上,双击运行,操作系统就会把这个硬盘上的数据加载到内存中,内存中就包含了这个程序对应的指令是啥(代码段),依赖的数据是啥(数据段)。内存指针就描述了哪里是代码段,哪里是数据段。

文件描述符表:进程打开了哪些文件。文件描述符表可以视为一个数组,数组的每个元素,是一个特殊的结构体,就描述了一个文件的信息。这个数组的下标称为文件描述符。每个进程一启动,默认就会打开三个文件,即在文件描述符表中创建三个项,分别是标准输入(System.in)、标准输出(System.out)、标准错误(System.err)。如果通过代码再打开其他文件,同样也会再文件描述符表中创建出对应的项。

状态:描述了当前这个进程,是否能去CPU上执行。如,就绪状态、睡眠状态

上下文:操作系统在执行某个进程时,如果需要把这个进程从CPU上调度走,就需要保存CPU的运行现场(当前寄存器里面的数值都是啥)到内存中,下次再调度到这个进程的时候,就可以无缝的继续从上次的位置往后执行。

优先级:调度进程的时候,每个进程安排的时间和先后都可以存在差别。

记账信息:统计每个进程执行的时间和指令的数目,依据这个来平衡调度的效果。

1.3 进程调度

操作系统,需要对上述的进程进行调度。

并行:2个核心,同时执行两个进程的指令。

并发:1个核心,“同时”在执行多个进程的指令。是靠快速的切换。

1.9GHZ:每分钟能执行19亿条指令。

现代的操作系统,都是支持这种多任务的操作系统,多个任务都是通过并行+并发的方式来调度执行的。一般使用并发来代指并行+并发

1.4 进程的独立性

操作系统要给软件提供一个稳定的运行环境,意思就是,系统要能够保证一个进程出问题不会影响到其他的进程,更不会波及到整个系统。

操作系统给每个进程分配一个独立的“虚拟内存空间”,不同进程访问的内存没有公共区域,就能保证互相之间不产生影响。

1.5 进程间通信机制

进程之间存在独立性,但有时需要进程之间的相互配合完成一部分工作。操作系统提供了一些“进程间通信”的机制,就可以完成一些沟通交互工作。

进程间通信机制,就是专门提供了一些区域,可以让多个进程可以同时访问到(共享)。

操作系统提供的进程间通信的机制有多种,主要学习操作文件和操作网络这两种。

1.6 进程的缺点

引入进程,就是为了解决并发编程的问题。但引入了一些其他问题。

进程持有的一些系统资源比较多,创建进程需要分配资源,销毁进程需要释放资源。进程调度切换,也需要让这些资源之间进行切换。这些操作都是比较耗时间的。如果切换的频率比较频繁,这时我们的成本是比较高的。

1.7 解决方案

进程池,类似于字符串常量池

通过线程来解决问题。线程也称为轻量级进程,LWP(light weight process).

二、线程概念

2.1 概念

一个线程就是一个“执行流”,每个线程执行着自己的代码。同一个进程中的若干线程,共用同一个内存空间。(进程就像是一个工厂,线程就是工厂里面的生产线)

为什么要实现并发编程?

①单核CPU的发展遇到了瓶颈,要想提高运算力,就需要多核CPU。并发编程能更充分的利用多核CPU的资源。

有些任务场景需要等待“IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。

多线程实现并发编程的优势:

由于资源是绑定在进程上的,创建销毁线程和进程上的资源关系不大。创建线程,并不需要分配新的资源,释放线程也不需要释放旧的资源。CPU针对线程进行调度,开销也是小于进程。总结为:

创建线程比创建进程更快、销毁线程比销毁进程更快、调度线程比调度进程更快

2.2 进程和线程的区别

①进程包含线程,一个进程里有一个线程,也可以有多个线程。

②进程存在的意义就是为了解决并发编程的问题,如果频繁创建或者销毁进程,开销比较大。相比之下线程也能满足并发编程的需求,但是线程的创建和销毁开销就小很多。

③进程是系统分配资源的基本单位,线程是系统调度执行的基本单位。

④进程之间各自有各自的虚拟地址空间,一个进程崩溃了不会影响其他进程。但是同一个进程里的线程共用一个虚拟地址空间,如果一个线程挂了,很容易影响到其他线程,甚至把整个进程都搞崩溃。

2.3 线程出现的问题

如果对线程的创建和销毁的频率很高,线程也会显得比较重量了。有两种解决方案:

线程池、协程(纤程)

三、创建线程

在Java标准库中,通过Thread类来表示线程。创建的每个Thread实例都是和系统中的一个线程是对应的。

在实例化时,可以传入一个name。通过这个给线程命名,那么对于代码执行没有影响。

如果不指定name,JVM默认指定的名字就是类似于Thread-0,Thread-1等。

3.1 继承Thread类

创建一个子类,继承Thread。重写Thread的run方法。

run方法里面包含了这个线程要执行的代码。即,当线程跑起来了,就会依次来执行run方法中的代码。

class MyThread extends Thread{
    @Override
    public void run() {  // 在新的线程里就会执行这个方法
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);  // 称为休眠。在这个过程中,这个线程就不会占用CPU了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();  // 会在系统中创建一个新的线程
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

一个进程里面至少有一个线程。main方法也是通过一个线程来执行的。

以往写的代码的每个main方法,都对应了一个线程。

上面的代码通过 myThread.start();又创建了一个新的线程。

运行结果:

 从运行结果中可以看到,这两个线程的打印是交替进行的。不是打印完一个,再打印另一个,这就意味着这两个线程是并发执行的关系(在宏观上同时执行)。

 同时,这里的并发,也不是严格意义上的你一条我一条,偶尔也会出现你一条我两条、我两条你一条的情况。

多个线程执行的先后顺序,并不是完全确定。当1s时间到了后,系统先唤起哪个线程是不确定的,取决于操作系统内部调度代码的具体实现。

如果多个线程之间没有手动的控制执行先后顺序,这个时候就认为多个线程之间的执行是“随机顺序”的。这也是多线程编程的万恶之源。

jconsole工具

jconsole工具相当于一个监控程序,能够看到一个Java进程内部很多的详细信息。

在哪找:

主要是看jdk安装在哪。我的就是:C:\Program Files\Java\jdk1.8.0_192\bin

3.2 实现Runnable接口

①创建一个类,实现Runnable接口(标准库自带的),也是重写run方法。

②创建Thread实例,将刚才的Runnable实例给设置进去。

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
// 这种是通过实现Runnable接口实现的,通过这种方式,就相当于把 要执行的任务 和 Thread类,进行分离(解耦合)
public class Demo1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.3 使用匿名内部类

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.4 使用lambda表达式

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.5 感受多线程的优势(增加运行速度)

未使用多线程:

// 串行执行
    public static void serial(){
        long beggin = System.currentTimeMillis();
        long a = 0;
        for (int i = 0;i<10_0000_0000;i++){
            a++;
        }
        long b = 0;
        for (int i = 0;i<10_0000_0000;i++){
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println(end-beggin);
    }
    public static void main(String[] args) {
        serial();
    }

引入多线程:

public static void main(String[] args) {
        conCurrency();
    }
    public static void conCurrency(){
        long beggin = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            long a = 0;
            for (int i = 0;i<10_0000_0000;i++){
                a++;
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            long b = 0;
            for (int i = 0;i<10_0000_0000;i++){
                b++;
            }
        });
        t2.start();
        // 不能直接记录结束时间
        // 因为conCurrency和t1、t2是并发关系,t1和t2还没执行完呢,就直接记录结束时间不准确
        // 要记录两个线程执行的最慢的时间,作为结束时间
        // 引入join,就是等待线程结束t1.join 意思就是等到t1执行完了,才会返回,继续往下走
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(end-beggin);
    }

CPU密集型:程序里面要进行大量的运算。

并发编程的最明显的优势,就是针对“CPU密集型”的程序,能够提高效率。

四、Thread类的常见属性和常用方法

4.1 Thread的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台程序isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

补充:如果创建的线程是一个非后台的线程,即使main方法执行完了,java进程仍需要继续执行,直到所有的非后台程序都执行完,Java进程才会退出。

如果创建的是后台线程,不会影响Java进程的结束。

4.1. run方法和start方法的区别

start:创建新线程

run:执行线程入口逻辑的方法。本身不具备创建新线程的作用

线程被创建了,内核里不一定有对应的线程,start方法调用后,才会有对应的线程。

假设不调用start(),调用run(),会发生什么呢?

public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.run();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

只打印了hello thread,没有打印hello main。当前的run只是一个普通方法的调用,并没有创建出新的线程。当前代码只有main这一个线程,当run方法里面的循环执行完了,才能执行后续的代码。但run方法里面的循环是一个死循环。 

4.2 中断新线程

①自定义一个标志位

public class Demo2 {
    public static boolean isInterrupt = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isInterrupt){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("线程还没有结束");
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isInterrupt = true;
        System.out.println("线程已经结束啦");
    }
}

isInterrupt变量相当于在t线程中读取,在main方法中修改。这样写会存在一些问题,见6.2中的④内存可见性。

②采用Thread提供的标志位

public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                // isInterrupted()是线程自带的标志位,通过这个判断标志位是否为true,为true表示线程应该退出
                while(!this.isInterrupted()){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        System.out.println("线程还没有结束");
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 休眠5s后,来控制t线程终止
        t.interrupt();
        System.out.println("线程已经结束啦");
    }

 从运行结果可以看出,当触发中断方法的时候,t线程就只是输出日志,就继续往下执行了,并没有真的停下来。

主要原因是:在catch里面不作为。

在t的线程代码中,存在两种情况:

①执行打印和while循环打印(程序处于就绪状态)

②进行sleep,处于阻塞状态/休眠状态

调用interrupt方法的时候:

进程处于就绪状态:此时直接修改线程中对应的标志位

进程处于阻塞状态:此时就会引起InterruptedException。

出现了异常,并没有对这个异常进行实质性的处理,相当于把这个异常给忽略了。加入对异常的处理:

运行结果:

 interrupt这个方法,说是中断线程,但是并不是直接就立刻马上的杀死线程,具体线程怎么退出,得线程代码自己说了算。

 线程的代码处理interrupt方法有三种方式

第一种:立刻结束

第二种:执行一些操作后再结束。

第三种:忽略。

为什么不将interrupt设计为一调用就结束线程?

以前这样设计过。由于t线程和main线程是并发执行的关系,当main执行interrupt时,并不知道t执行到哪,冒然中断会导致一些问题。线程t 受到interrupt时,怎么处理由t自身来决定,就可以保证t把任务干完,将收尾工作做好,然后再被销毁。

4.3 等待一个线程(join)

public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                for(int i = 0;i<5;i++){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        System.out.println("线程还没有结束");
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程已经结束啦");
    }
}

 在main的线程中调用t.join(),就是让main来等待t执行完毕。

当main中的代码执行到t.join()时,main线程就会进入阻塞等待状态。效果和sleep类似。等t线程执行结束了,即t中的run方法执行完了,main线程就会继续执行。

通过join可以控制线程的结束顺序。

join不带参数,就是"死等"。

join带参数:就是表示等待的最大时间。一般采取这种方式。

4.4 获取当前线程引用

在某个线程的代码中,拿到这个线程对应的thread对象的引用,才能进行后续的一些操作。很多和线程相关的操作,都是依赖这样的引用。

①如果直接继承Thread创建的线程,直接在run方法中调用this就可以拿到这个线程的实例。

 在这个代码中,通过this.isInterrupted()是不能调用的,因为run是Runnable的方法,不是Thread里的方法。此处的this没有指向Thread,当然就没有Thread类的属性和方法。

②更常见的是使用Thread里面的一个静态方法,currentThread(),哪个线程调用了这个静态方法,就能够返回哪个线程的Thread实例引用。

虽然run仍然是Runnable的方法,但是通过这个Thread的currentThread来获取线程实例。

即在哪个线程里调用Thread.currentThread(),就返回哪个线程的实例。

4.5 休眠当前线程

调用sleep的线程会阻塞等待,等待时间取决于设置的时间。

在大多数情况,一个进程中有多个线程,每个线程对应着一个PCB,一个进程对应了一组PCB。

操作系统是以PCB为单位进行调度的,所以线程是操作系统调度执行的基本单位。

五、线程的状态

NEW:把Thread对象创建出来了,但是内核里面的线程还没创建。(没有调用start方法)

TERMINATED:内核里的线程已经结束了,但是Thread对象还在。(线程执行完run方法以后)

RUNNABLE:就绪状态(随时可以被调度到CPU中运行)

TIMED_WAITING:阻塞状态,一定时间后移出阻塞。通过sleep产生

BLOCKED:阻塞状态,等待锁的时候产生的。

WAITING:阻塞状态,调用wait产生的。

 六、线程安全问题(重点哦)

6.1 经典线程不安全示例

class AddSum{
    public static long sum = 0;
    public static void increase(){
        sum++;
    }
}
public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0;i<5_0000;i++){
                AddSum.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0;i<5_0000;i++){
                AddSum.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(AddSum.sum);
    }
}

运行结果:每运行一次,得到的结果都不同。结果在5万到10万之间。这个bug是因为线程调度的随机性导致的。

为什么呢???在了解前,先认识什么是线程 不安全。

编写的多线程代码,如果当前代码因为多线程随机的调度顺序,导致程序出现了bug,就称为线程不安全。如果不管系统按照啥样的随机情况来调度,也不会导致系统出现bug,就称为线程安全。

这里的网络不安全和黑客没有关系。

看看sum++都干了啥?

sum++操作其实是三个步骤:

step1:把内存的值,读到CPU的寄存器中   load

step2:把寄存器中的0进行+1操作               add

step3:把寄存器中的1给写回到内存中   save  

这三个步骤,对应三个指令

如果两个线程同时操作这个sum,此时由于线程之间的随机调度的过程,可能产生不同的结果。

 如何让线程安全,常用方案是加锁synchronized(Java中内置的关键字),进入increase方法后,就先加锁,出了这个方法就解锁。

引入多线程,目的是为了实现并发编程,当加了锁之后,数据结果是对的,但是这里的并发性就降低了,速度也慢下来了。

 追求速度还是追求准确?准确

两个并发的线程,可能各自要完成的任务很多,有很多工作能够并行进行的,整体来说,多线程还是有意义的。

6.2 线程不安全的原因

①根本原因,线程的抢占式执行(这个无可奈何)

②两个线程在修改同一个变量

如果是一个线程修改这个变量,没事;

如果是两个线程读这个变量,也没事;

如果是两个线程修改两个变量,也没事。

针对这个原因,可以在一定程度上进行处理。例如修改代码结构,避免出现这种多个线程修改一个变量的情况。

③线程针对变量的修改操作不是原子性的

原子性就是不可拆分性,例如++就不具有原子性,它可以拆分为load add save三个操作

加锁操作本质上就是把这些不是原子性的操作给打包在一起了,这种做法普适性最高,也是处理线程安全最典型的办法。

④内存可见性(重点)

假设有一个变量,一个线程快速的读取这个变量的值,另一个线程会在一定时间后,修改这个变量

public class Demo2 {
    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0){
                // 循环体里啥都不写
            }
        });
        t.start();
        // 在主线程中,通过Scanner输入一个整数,把输入的值赋值给isQuit,从而影响线程2
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main线程结束");
    }
}

main线程修改了isQuit的值,但是t线程却没有随之退出。显然,这是一个bug。

Java中的编译器优化

程序员写的代码,Java编译器在编译的时候,并不是原封不动的逐字翻译。会保证在原有逻辑不变的前提下,动态调整要执行的指令内容。这个调整能够提高程序的运行效率.

这里的优化会提高程序的运行效率,但在多线程的场景下,编译器的判断可能会存在误差,优化操作后就有可能影响到原有的逻辑。

isQuit == 0,分为load和compare两个指令。在t线程中,编译器的直观感受就是“反复的进行了太多次的load,太慢了,而且,由于编译器无法将多个线程联系在一起分析,就会觉得load得到的结果好像还是一直不变的”,因此,编译器有了一个大胆的优化操作,就是直接省略这里的load,只保留了第一次。在后续的比较操作中,就不再重新读取内存了,而是直接从寄存器中读。这样,t线程少了很多的load操作,速度就会提高不少。

load是从内存中读数据,比直接从寄存器读数据慢了3-4个数量级。

解决方案:

a. 适用synchronized,就会禁止编译器在synchronized内部产生上述的优化

b. 还可以使用另一个关键字,volatile,保证了内存的可见性,禁止了编译器的相关优化

可以采用关键字修饰对应的变量,编译器在优化的时候,就知道会禁止上述读内存的优化,保证每次都是重新从内存中读,哪怕速度慢一点。

⑤指令重排序

也是编译器的一种优化手段,保证原有代码逻辑不变,调整了指令的执行顺序,从而提高了效率。

如果是在单线程情况下,这里的判定比较准。如果在多线程情况下,这里的判断就不一定准了。

解决方案:加synchronized。编译器对于synchronized内部的代码是非常谨慎的,不会随便优化

6.3 synchronized的具体使用用法

synchronized,起到的效果,有三个方面:

互斥(将随机执行的线程顺序变为串行执行)

保证内存可见性

禁止指令重排序。后面这两点都是在提醒编译器能够优化的谨慎一点

①直接加到一个普通的方法上,进入方法就相当于加锁,出了方法就相当于解锁。(锁对象还是this)

②加到一个代码块上,需要手动指定一个锁对象。

在Java中,任何一个继承自Object类的对象,都可以作为锁对象(synchronized加锁对象操作,本质上就是操作Object对象头中的一个标志位)

③加到一个static方法上,此时相当于指定了当前的类对象为锁对象。

两个线程针对同一个对象加锁,才会产生竞争。两个线程针对不同的对象加锁,不会产生竞争。

public class Demo2 {
    private volatile static Object locker1 = new Object();
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           while (true){
               // 获取到锁之后,就让程序阻塞,通过Scanner来进行阻塞
               synchronized (locker1){
                   Scanner scanner = new Scanner(System.in);
                   System.out.println("输入一个整数");
                   int a = scanner.nextInt();
                   System.out.println(a);
               }
           }
        });
        t.start();
        // 加这个sleep,保证t1先拿到锁,先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(() -> {
            synchronized (locker1){
                System.out.println("t2");
            }
        });
        t2.start();
    }
}

t1先运行,t2后运行。t1先拿到locker1这个锁,然后就阻塞。t2想拿到这个锁时,由于t1已经占用了锁,所以t2线程无法获取到锁,就只能阻塞等待

没有打印t2这个日志,当前这个t2是阻塞的。

如果两个线程针对不同的对象加锁,这两个线程之间就不会有任何的竞争关系。

public class Demo2 {
    private volatile static Object locker1 = new Object();
    private volatile static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           while (true){
               // 获取到锁之后,就让程序阻塞,通过Scanner来进行阻塞
               synchronized (locker1){
                   Scanner scanner = new Scanner(System.in);
                   System.out.println("输入一个整数");
                   int a = scanner.nextInt();
                   System.out.println(a);
               }
           }
        });
        t.start();
        // 加这个sleep,保证t1先拿到锁,先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(() -> {
            synchronized (locker2){
                System.out.println("t2");
            }
        });
        t2.start();
    }
}

 

可重入锁:

是synchronized的重要特性,如果synchronized不是可重入的,就很容易出现死锁的情况。

当调用increase时,先针对this进行加锁操作(此时this就是一个锁定的状态,将this对象头中的标志位给设置上了)

继续往下执行,也会尝试再次加锁。由于此时this已经是锁定状态了,按照之前的理解,这里的加锁就会出现阻塞,阻塞会一直持续到之前的代码把锁释放掉了 ,即:要执行完整个方法,锁才能释放,但是由于此时的阻塞,导致当前的这个方法无法再继续下去。

当有可重入锁的特点后:

首先,锁中有两个信息:当前这个 锁被哪个线程给持有了、当前这个线程被加锁了几次

当线程t已经加锁成功后,后续再次尝试加锁,就会自动的判定出,当前这把锁就是t持有的,第二次加锁不会真的加锁,只是进行一个修改计数。代码接着往下执行,出了synchronized代码段,就会触发一次解锁,也不是真的解锁,而是计数-1.当外层方法执行完之后,再次-1,减为0后,才真正的进行解锁。

死锁出现的情况:

一个线程两把锁

两个线程两把锁

N个线程M把锁(哲学家就餐问题)

七、集合类的线程安全问题

线程不安全:谨慎在多线程环境下使用,尤其是一个对象被多个线程修改的时候

ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

线程安全:(因为其核心方法上带有了synchronized关键字)

Vector、HashTable、ConcurrentHashMap、StringBuffer

String也是线程安全的。因为String是不可变对象,因此,就不能再多个线程中修改用一个String。

synchronized和volatile的区别:

synchronized:原子性、内存可见性、指令重排序

volatile:内存可见性

八、JMM(Java Memory Model,Java存储模型/Java内存模型)

JMM其实就是前面说的CPU的寄存器以及内存之间一套模型,Java将其抽象并重新命名,称为JMM

将CPU的寄存器这部分存储,称为:工作存储/工作内存(Work Memory)

将正常的内存,称为主存储/主内存(main Memory)

九、控制线程的执行顺序-wait notify机制

当某个线程调用了wait之后,就会阻塞等待,直到其他某个线程调用notify把整个线程唤醒为止。因此,可以利用wait-notify控制线程的执行顺序。

注意1:wait需要搭配synchronized适用

 wait方法里会做三件事情:

先针对o解锁

进行等待,等到通知的到来

当通知到来之后,会被唤醒,同时尝试重新获取到锁,然后再继续执行。

因此,wait需要搭配synchronized来使用

notify也是Object类的方法。哪个对象调用的wait,就需要哪个对象调用notify来唤醒。

notify同样也要调用synchronized来使用。

注意2:如果有多个线程都在等待,调用一次notify,只能唤醒其中的一个线程,具体唤醒的是谁,随机的。

注意3:如果没有任何线程等待,直接调用notify,不会有副作用。

notifyAll:以下全部唤醒,唤醒之后,这些线程再尝试竞争这同一个锁。

唤醒一个,其他线程仍然在wait中阻塞;唤醒全部,这些线程尝试竞争锁,然后按照竞争成功的顺序,依次往下执行。

    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waiter = new Thread(() -> {
            while (true) {
                synchronized (locker) {
                    System.out.println("wait 开始");
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait 结束");
                }
            }
        });
        waiter.start();

        Thread.sleep(3000);

        Thread notifier = new Thread(() -> {
            synchronized (locker) {
                System.out.println("notify 之前");
                locker.notify();
                System.out.println("notify 之后");
            }
        });
        notifier.start();
    }

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘减减

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值