• 多线程入门知识点及代码详解及相关面试八股文


    多线程详解及相关面试八股文

    多线程

    线程概述

    * 多线程:
     进程:当前正在运行的程序,一个应用程序在内存中的执行区域
     线程:进程中的一个执行控制单元,执行路径
     * 一个进程可以有一个线程,也可以有多个线程
     * 单线程:安全性高,但是效率低
     * 多线程:安全性低,效率高
     * 多线程案例:360,迅雷等
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    进程

    进程相当于公司,多线程相当于公司的多个程序员

    线程

    单线程特点:安全性高,一件事交给一个人干,确定性的知道干到哪里了。但是效率低
    多线程特点: 安全性低因为一个人干一部分,需要对接,每个人的写法不一样,对接出现问题,一个人的错误导致整体的问题。但是效率高
    例如:杀毒软件同时做多件事情,电脑体检、木马查杀、电脑清理等。在这里插入图片描述
    多线程的优点:提高效率

    1.多线程的实现方式

    参考资料:继承Thread类 & 实现Runnable接口 使用解析

    继承Thread方法简介

    在这里插入图片描述
    详细实现步骤:

    1. 创建线程类(继承自Thread类)
    2. 复写run ()(定义线程行为)
    3. 创建线程对象(即实例化线程类)
    4. 通过线程对象控制线程的状态(如运行、睡眠。挂起1停止)

    多线程的实现方式:
    1.将类声明为Thread的子类,该子类应该重写thread类的run方法,
    2.接下来可以分配并启动该子类的实例.

    1.新建一个类,并将类声明为Thread的子类,该子类应该重写thread类的run方法。

    package Thread;
    //标准的多线程实现的类
    public class MyThread extends Thread{
        @Override
        public void run(){
            for (int i = 0; i < 100; i++) {
                System.out.println(i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.创建多线程实例

    public class ThreadDemo1 {
        public static void main(String[] args) {
    //        创建线程实例 声明实例
            MyThread t = new MyThread();
    //        启动多线程
            t.start();    //执行一次 输出0-99,t.run和普通的方法一样,所以启动多线程不能用run()
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出0-99,t.run和普通的方法一样,所以启动多线程不能用run()

    线程相关方法

    void setName(String name)
    改变线程名称,使之与参数 name 相同。
    String getName()
    返回该线程的名称 。

    getname的源码(点击ctrl+getName)

     public final String getName() {	//不能被重写,但是可以直接调用因为是public
            return name;
        }
    
    • 1
    • 2
    • 3

    void start()
    使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
    void run()
    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

    多线程实现方法一代码

    这个例子能够更加清晰每一个结果窒息的是第一个线程还是第二个线程。
    1.创建多线程实现类

    package Thread;
    //标准的多线程实现的类
    public class MyThread extends Thread{
        @Override
        public void run(){
            for (int i = 0; i < 100; i++) {
                System.out.println(getName()+i);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.分配并启动该子类的实例

    package Thread;
    /*多线程的实现方式:
    * 1.将类声明为Thread的子类,该子类应该重写thread类的run方法,
    * 接下来可以分配并启动该子类的实例*/
    public class ThreadDemo1 {
        public static void main(String[] args) {
    //        创建线程实例 声明实例
            MyThread t1 = new MyThread();
    //        启动线程
    //        t.run();    //执行一次 输出0-99,t.run和普通的方法一样,所以启动多线程不能用run() t2.run()就会先执行t1,再执行t2
           t1.setName("第一个线程");
            t1.start();
    
            MyThread t2= new MyThread();
            t2.setName("第二个线程");
            t2.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行结果:多个线程同时运行,什么时候执行1什么时候执行2?是CPU随机执行的,可以通过设置优先级,给另外一个线程睡眠时间来设置执行的顺序。
    在这里插入图片描述
    在这里插入图片描述

    2.多线程实现方法二:通过Runnable接口来实现

    Runnable接口实现多线程简介

    在这里插入图片描述

    1. 创建线程辅助类(实现Runnable接口)
    2. 复写run ()(定义线程行为)
    3. 创建线程辅助对象(即实例化线程辅助类)
    4. 创建线程对象
      - 即实例化线程类
      - 线程类= Thread类
      - 创建时传入线程辅助类对象
    5. 通过线程对象控制线程的状态(如运行、睡眠、挂起1停止)
    多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
    然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
    Thread(Runnable target)
     *	static Thread currentThread() :返回当前线程对象
    
    • 1
    • 2
    • 3
    • 4

    注意:

    • Java中真正能创建新线程的只有Thread类对象
    • -通过实现Runnable的方式,最终还是通过Thread类对象来创建线程
      所以对于实现了Runnable接口的类,称为线程辅助类Thread类才是真正的线程类
    主方法是多线程的吗?

    主方法main是单线程的。按照顺序执行方法。但是可以在主线程中调用多线程的程序。

    package com.demo01;
    /*
     * 主方法是多线程吗?
     * 		主方法是单线程的
     * 
     */
    public class ThreadDemo3 {
    	public static void main(String[] args) {
    		method();
    		function();	//顺序执行
    	}
    	public static void method() {}
    	public static void function() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Runnable是一个抽象的接口
    在这里插入图片描述

    Runnable

    API中Runnable的基本使用
    在这里插入图片描述

    在这里插入图片描述

    currentThread

    currentThread 返回对当前正在执行的线程对象的引用。

    • 当前正在执行的线程:在一个多线程程序中,有多个线程同时运行。每个线程代表了程序中的一个独立执行流。在某一时刻,只有一个线程会处于活动状态,也就是正在执行代码。这个活动的线程就被称为当前正在执行的线程。
    • 引用:则表示对这个线程对象的访问,可以用来执行操作或查询信息。
      在这里插入图片描述
    多线程实现方法二代码
    MyTheadpackage Thread.demo2;
    /*多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
    然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
    Thread(Runnable target)
     *	static Thread currentThread() :返回当前线程对象
     *	既然有了继承Thread为何还要整出来实现Runnable?*/
    public class MyThread2 implements Runnable{
        int num;
        public MyThread2(int num){
            this.num = num;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                /*Thread t = Thread.currentThread();
                System.out.println(t.getName()+":"+i);*/
    //            链式编程
                System.out.println(Thread.currentThread().getName()+":"+i+"参数"+num++);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    分配该类的实例
    package Thread.demo2;
    /*多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
    然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
    Thread(Runnable target)
     *	static Thread currentThread() :返回当前线程对象
     *	既然有了继承Thread为何还要整出来实现Runnable?*/
    public class ThreadTwo {
        public static void main(String[] args) {
            method();
        }
    
    //    创建多个实例实现多线程
        private static void method(){
    //        实例1
            MyThread2 mt = new MyThread2(1);
            Thread t = new Thread(mt);
            t.setName("李四");
            t.start();
    //实例2
            MyThread2 mt2 = new MyThread2(1);
            Thread t2 = new Thread(mt2);
            t2.setName("老王");
            t2.start();
        }
    
    //    只创建一个线程类的实例,但是多个线程对象实现多线程
        private static void method2(){
            MyThread2 mt = new MyThread2(1);
            Thread t = new Thread(mt);
            t.setName("李四");
            t.start();
    
            Thread t2 = new Thread(mt);
            t2.setName("王五");
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    运行结果:

    老王:1参数2
    老王:2参数3
    李四:7参数8
    老王:3参数4
    老王:4参数5
    老王:5参数6
    老王:6参数7
    老王:7参数8
    李四:8参数9
    李四:9参数10
    李四:10参数11
    李四:11参数12
    李四:12参数13
    老王:8参数9
    .......
    老王:95参数96
    老王:96参数97
    老王:97参数98
    李四:94参数95
    李四:95参数96
    李四:96参数97
    李四:97参数98
    李四:98参数99
    李四:99参数100
    老王:98参数99
    老王:99参数100
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    通过结果发现执行的顺序是乱的,第一个线程没有执行完就执行了第二个线程,这就是造成了线程不安全的现象。

    只实例化一个对象,启动多个线程。两个线程共用一个参数
    在这里插入图片描述

    既然有继承Thread的方法为什么还要Runnable方法?

    总结:
    1.因为java是单继承的,如果使用了继承Thread类那么就不能继承其他的类了,但是java是支持继承多个接口的
    2.继承Thread必须创建多个实例对象,这些线程就会相对独立,无法共享资源,同时也会增加和浪费资源空间,并且多次创建和销毁线程非常的消耗系统资源。
    3.Runnable避免了单继承的局限性,还可以不用构建多个实例,可以只实例化一个对象,多个线程共享一个参数,具体见方法二的代码。所以通常情况下用Runnable的情况更多。
    在这里插入图片描述

    3.多线程实现火车站卖火车票

    火车站的窗口一直开着 while(true),卖掉一张窗口休息(sleep)100毫秒,睡的过程中窗口2进来了,窗口2睡了窗口3进来了。
    1.构建一个类实现多个窗口卖票
    TicketTest线程创建类:

    package Thread.TicketTest;
    
    public class TicketTest {
        public static void main(String[] args) {
            TicketThread tt = new TicketThread();   //只创建一个线程类的实例
    //        创建多个线程对象
            Thread t1 = new Thread(tt);
            t1.setName("窗口1");
            Thread t2 = new Thread(tt);
            t2.setName("窗口2");
            Thread t3 = new Thread(tt);
            t3.setName("窗口3");
    //        启动线程对象
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.线程方法实现类

    package Thread.TicketTest;
    
    import com.sun.xml.internal.ws.api.ha.StickyFeature;
    
    /*火车站卖火车票模拟
    
     * */
    public class TicketThread implements Runnable {
        int ticket = 100;   //总的票数
    
        @Override
        public void run() {
            while (true) {  //表示售票的窗口一直都是开着的
                if (ticket > 0) {
                    try {
                        Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
                    } catch (InterruptedException e) {  //捕获线程中断时的异常
                        e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + ticket--);
                    // 打印当前线程的名称和票号,并将票号减1,表示售出一张票
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果:

    窗口3:100
    窗口2:98
    窗口1:99
    窗口2:97
    窗口3:95
    窗口1:96
    窗口1:94
    窗口3:92
    窗口2:93
    窗口2:91
    窗口1:89
    窗口3:90
    窗口3:88
    窗口1:86
    窗口2:87
    窗口1:84
    窗口2:85
    窗口3:83
    窗口2:82
    窗口3:80
    窗口1:81
    窗口1:79
    窗口2:78
    窗口3:77
    窗口3:76
    窗口2:74
    窗口1:75
    窗口3:73
    窗口1:71
    窗口2:72
    窗口1:69
    窗口2:70
    .......
    窗口2:13
    窗口3:12
    窗口2:11
    窗口1:10
    窗口3:9
    窗口1:8
    窗口2:7
    窗口2:6
    窗口1:4
    窗口3:5
    窗口3:3
    窗口1:2
    窗口2:1
    窗口3:-1
    窗口1:0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    由上面的结果可以看出,一个线程执行没有结束,第二个线程又执行了,甚至修改了上一个线程的数据,例如窗口3以为还有1张票,但是实际上没有了,窗口3还继续卖,造成了-1的结果。
    多线程卖票出现的问题原因:共享的数据被第一个线程取到后,第二个线程把这个共享数据修改了,但是第一个线程不知道被修改了。例如上厕所的例子。

    * 问题出现的原因:
     * 		要有多个线程
     * 		要有被多个线程所共享的数据 ticket火车总票数
     * 		多个线程并发的访问共享的数据 ticket火车总票数
     * 
     * 在火车上上厕所
     * 张三来了,一看门是绿的,他就进去了,把门锁上了,门就变红了
     * 李四来了,一看门是红色的,他就只能憋着
     * 张三用完了厕所,把锁打开了,门就变成了绿色
     * 李四一看门变绿了,他就进去了,把门锁上,门就变红了
     * 王五来了,一看门是红色的,他也只能憋着
     * 李四用完厕所了,把锁打开了,肚子又不舒服了,扭头回去了,又把门锁上了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    同步锁synchronized关键字|同步代码块和同步方法

    锁要被所有的线程可以共用,安全性高,效率低,没有锁效率低,安全性高
    需要加锁的代码越短越好,就说明占用的时间越短。alit+shift+m抽取出代码 再加锁
    方法里面的锁对象默认是this,就是new的线程对象的本身。
    synchronized定义:同步(锁),可以修饰代码块和方法,被修饰的代码块和方法一旦被某个线程访问,则直接锁住,其他的线程将无法访问。

    同步方法:使用关键字synchronized修饰的方法,一旦被一个线程访问,则整个方法全部锁住,其他线程则无法访问
     * synchronized
     * 注意:
     * 		非静态同步方法的锁对象是this
     * 		静态的同步方法的锁对象是当前类的字节码对象
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码块加锁(同步代码块):
    同步:安全性高,效率低
    非同步:效率高,但是安全性低

    同步代码块:
     * 			synchronized(锁对象){
     * 
     * 			}
    
    • 1
    • 2
    • 3
    • 4
    public class TicketThread implements Runnable {
        int ticket = 100;   //总的票数
        Object object = new Object();   //用一个对象当做锁对象
        @Override
        public void run() {
            while (true) {  //表示售票的窗口一直都是开着的
                synchronized (object){  //代码块加锁
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
                        } catch (InterruptedException e) {  //捕获线程中断时的异常
                            e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
                        }
                        System.out.println(Thread.currentThread().getName() + ":" + ticket--);
                        // 打印当前线程的名称和票号,并将票号减1,表示售出一张票
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    方法加锁:

    public class TicketThread implements Runnable {
        int ticket = 100;   //总的票数
        Object object = new Object();   //用一个对象当做锁对象
    @Override
       public void run() {
           while (true) {  //表示售票的窗口一直都是开着的
                   extracted();	//调用加锁的方法
           }
       }
    //方法加锁
        private synchronized void extracted() {  //方法抽取快捷键 ctrl+alt+M
            if (ticket > 0) {
                try {
                    Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
                } catch (InterruptedException e) {  //捕获线程中断时的异常
                    e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
                }
                System.out.println(Thread.currentThread().getName() + ":" + ticket--);
                // 打印当前线程的名称和票号,并将票号减1,表示售出一张票
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    静态方法
    用于执行与类相关但与特定对象无关的操作。

    package Thread.SynchronizedDemo;
    public class TicketThread implements Runnable {
      static int ticket = 100;   //总的票数
        Object object = new Object();   //用一个对象当做锁对象
      
       @Override
       public void run() {
           while (true) {  //表示售票的窗口一直都是开着的
                   extracted2();
           }
       }   
        private static synchronized void extracted2() {  //方法抽取快捷键 ctrl+alt+M
            if (ticket > 0) {
                try {
                    Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
                } catch (InterruptedException e) {  //捕获线程中断时的异常
                    e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
                }
                System.out.println(Thread.currentThread().getName() + ":" + ticket--);
                // 打印当前线程的名称和票号,并将票号减1,表示售出一张票
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    执行结果:

    窗口1:100
    窗口1:99
    窗口1:98
    窗口1:97
    窗口1:96
    窗口3:95
    窗口3:94
    窗口3:93
    窗口3:92
    窗口3:91
    窗口3:90
    窗口3:89
    窗口3:88
    窗口3:87
    窗口3:86
    窗口3:85
    窗口2:84
    窗口2:83
    窗口3:82
    窗口1:81
    窗口1:80
    窗口1:79
    窗口1:78
    窗口3:77
    窗口3:76
    窗口2:75
    窗口3:74
    窗口3:73
    窗口3:72
    窗口3:71
    窗口3:70
    窗口3:69
    窗口3:68
    窗口3:67
    。。。。。
    窗口3:25
    窗口3:24
    窗口3:23
    窗口3:22
    窗口3:21
    窗口3:20
    窗口2:19
    窗口3:18
    窗口3:17
    窗口1:16
    窗口3:15
    窗口2:14
    窗口3:13
    窗口3:12
    窗口3:11
    窗口3:10
    窗口1:9
    窗口1:8
    窗口1:7
    窗口1:6
    窗口1:5
    窗口1:4
    窗口1:3
    窗口1:2
    窗口1:1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    多线程相关的面试题总结

    参考:图解线程安全

    线程安全是什么?

    参考:一文教会你什么线程安全以及如何实现线程安全
    线程安全定义:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。
    换言之,线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了。
    要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性

    • 原子性
      在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
      就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。
    • 可见性
      当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
      若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
    • 有序性
      程序执行的顺序按照代码的先后顺序执行,在多线程编程时就得考虑这个问题。
    出现线程安全问题的原因?

    出现线程安全问题的原因:
    在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没 结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。

    如何避免线程安全问题?
    • 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性 即使用synchronizedvolatileLock
    • 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。

    线程安全通俗例子:
    线程安全可以通过一个通俗的例子来解释。
    假设你经营着一个小咖啡馆,你的咖啡馆有一个共用的账单本(记录客户点餐和付款的情况),并且有多个服务员在同时接待客人。现在,我们来看两种不同的情况,一种是线程不安全的,另一种是线程安全的:

    情况一:线程不安全

    在这种情况下,服务员没有采取任何特殊措施来确保账单本的安全。多个服务员可以同时写入账单本,但没有同步机制来保护它。这可能导致以下问题:

    • 当两名服务员同时写入账单本时,可能会出现写入冲突,其中一个服务员的记录可能会覆盖另一个的记录。
    • 当一名服务员正在写入账单本时,另一名服务员可能会同时读取它,导致不一致的数据。

    结果是,账单本可能包含不正确或丢失的订单和付款信息,这会让你的咖啡馆的财务记录变得混乱,客户可能会被多次收款或遗漏。

    情况二:线程安全

    在这种情况下,你采取了一些措施来确保账单本的线程安全。可能的方法包括:

    • 使用一个锁(例如,只允许一个服务员同时写入账单本)来确保同时只有一个服务员能够写入。
    • 或者,使用一个专门的工作人员来负责维护账单本,其他服务员只能通过这个工作人员来提交订单和付款信息。

    这样,无论多少服务员同时工作,账单本都会保持一致性,不会发生数据丢失或混乱。

    在这个例子中,线程安全就像是保护账单本的机制,防止多个服务员同时修改它,从而确保了数据的一致性和准确性。线程不安全则代表了没有这种保护机制,可能导致数据损坏和错误。在软件开发中,线程安全的概念类似于这个例子,用来确保多线程环境下数据的正确性和稳定性。

    线程同步是什么?如何保证线程同步?

    **线程同步的定义:**用于协调多个线程之间的执行,以确保数据的一致性和避免竞态条件(Race Condition)。在多线程环境中,多个线程可能会同时访问和修改共享的资源,如果不进行适当的同步措施,就会导致数据不一致、不确定性和程序错误。
    **线程同步的目标:**让多个线程协调工作,以确保它们在访问共享资源时不会发生冲突,从而保持数据的正确性。
    Java 提供了一系列的关键字和类来保证线程安全。

    synchronized关键字(见上几节同步锁synchronized)

    1. 保证方法或代码块操作的原子性
    Synchronized 保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个 Monitor(监视锁) 监视的代码,最多只能有⼀个线程在访问。
    2.保证监视资源的可见性
    保证多线程环境下对监视资源的数据同步。即任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。
    3.保证线程间操作的有序性
    Synchronized 的原子性保证了由其描述的方法或代码操作具有有序性,同一时间只能由最多只能有一个线程访问,不会触发 JMM 指令重排机制。

    Volatile 关键字

    保证被 Volatile 关键字描述变量的操作具有可见性和有序性(禁止指令重排)

    注意:
    1.Volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值
    操作和对象的引⽤赋值操作有效。
    2 对于 i++ 此类复合操作, Volatile 无法保证其有序性和原子性。
    3.相对 Synchronized 来说 Volatile 更加轻量一些。
    作者:七彩祥云至尊宝
    链接:https://juejin.cn/post/6844903890224152584

    Lock 关键字 更加灵活

    Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLockReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock (手动释放)提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作,如:

    // 创建一个可重入读写锁(ReentrantReadWriteLock)
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    // 获取读锁
    Lock readLock = lock.readLock();
    
    // 获取写锁
    Lock writeLock = lock.writeLock();
    
    // 定义一个私有整数变量 x,该变量将在多线程环境下被读取和修改
    private int x = 0;
    
    // 一个用于增加 x 值的方法,需要获取写锁
    private void count() {
        writeLock.lock(); // 获取写锁,独占写权限
        try {
            x++; // 增加 x 的值
        } finally {
            writeLock.unlock(); // 释放写锁,允许其他线程写入
        }
    }
    
    // 一个用于打印 x 值的方法,可以同时被多个线程调用,获取读锁
    private void print(int time) {
        readLock.lock(); // 获取读锁,允许多个线程同时读取
        try {
            for (int i = 0; i < time; i++) {
                System.out.print(x + " "); // 打印 x 的值
            }
            System.out.println(); // 换行
        } finally {
            readLock.unlock(); // 释放读锁,允许其他线程读取
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    锁源码解析

    synchronizedvolatileLock三者区别

    下面是关于synchronizedvolatileLock之间的区别的表格描述:

    特性synchronizedvolatileLock
    类型关键字关键字接口
    用途用于实现线程互斥用于保证可见性用于实现线程互斥和更多功能
    适用范围方法、代码块、实例变量实例变量或类变量任何代码块
    锁粒度细粒度锁,可精确指定粗粒度锁,适用于变量可以自由选择粒度
    同步方式自动加锁和解锁不需要显式加锁和解锁需要显式加锁和解锁
    性能性能相对较低性能较高,适用于可见性问题性能灵活,可根据需求选择
    可重入性支持不支持支持
    支持条件变量不直接支持不支持支持
    适用于复杂情况适用不适用适用
    锁的释放方式自动释放(退出同步块)自动释放(写入完成后释放)手动释放
    异常处理异常时自动释放锁无需处理异常需要显式处理异常
    可中断性支持不支持支持
    死锁检测不支持不支持支持
    公平性默认非公平不适用可以选择公平或非公平
    自定义锁策略有限制不适用可以自定义锁策略

    这个表格总结了synchronizedvolatileLock之间的主要区别。需要注意的是,选择哪种同步机制取决于具体的应用需求和性能要求。synchronized通常是最简单和最常见的选择,而volatile通常用于确保可见性。Lock则提供了更大的灵活性和控制,适用于复杂的同步需求。

    线程的生命周期是什么样子?

    在这里插入图片描述

    1. 新建 创建线程对象 TicketThread t = new TicketThread();
    2. 就绪 具备了执行的条件,没有具备执行的权利 只有CPU给了他执行权利才可以进入下一环节。 t.start();
    3. 运行 CPU给了线程执行的权利 执行t.start();
    4. 死亡 线程对象变成了垃圾
    5. 等待(可能存在)wait() 等待 notify()唤醒
      在这里插入图片描述
    wait()

    wait是Object类里面的方法。

    public final void wait()
                    throws InterruptedException
    
    • 1
    • 2

    定义: ==在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。 ==
    当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。

    对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:

    synchronized (obj) {
    while (<condition does not hold>)
    obj.wait();
    ... // Perform action appropriate to condition
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅 notify 方法。

    抛出:
    IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。

    notify()
    public final void notify()
    
    • 1

    定义:唤醒在此对象监视器上等待的单个线程。
    如果所有线程都在此对象上等待,则会选择**唤醒其中一个线程。**选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
    **直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。**被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
    此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:

    • 通过执行此对象的同步实例方法。
    • 通过执行在此对象上进行同步的 synchronized 语句的正文。
    • 对于 Class类型的对象,可以通过执行该类的同步静态方法。
      一次只能有一个线程拥有对象的监视器。
      抛出:
      IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。
    notifyAll()

    public final void notifyAll()唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
    此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅 notify 方法。
    抛出: IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。

  • 相关阅读:
    vue v-for 渲染大量数据卡顿的优化方案
    JS数组方法map 和 forEach 的区别
    Nginx(openresty) 开启目录浏览 以及进行美化配置
    Tomcat下载安装以及配置(详细教程)
    列化复杂的xml对应的类
    【教学类-18-01】20221123《蒙德里安红黄蓝格子画》(大班)
    【ASP.NET Core】绑定到 CancellationToken 对象
    结构体数组经典运用---选票系统
    C/C++内存管理
    关于vue中某个页面在刷新的时候报only one instance of babel-polyfill is allowed错误
  • 原文地址:https://blog.csdn.net/qq_43654669/article/details/133315893