JAVA多线程初阶(1)

news2025/1/23 17:25:17

目录

  • JAVA多线程(1)
  • 1.Thread类创建与使用
    • 1.1 继承Thread类
    • 1.2 实现并发
    • 关于sleep()
    • 1.3 Runnable创建线程
    • 1.4 匿名内部类创建线程
    • 1.5 lamda表达式创建线程
  • 2.多线程提高效率
  • 3.Thread类属性和方法
    • 3.1 Thread(String name)
    • 3.2 isDaemon()
    • 3.3 isAlive()
    • 3.3 线程的重要方法
    • 3.4 中断线程
      • 3.4.1 Thread内置标志位
    • 3.5 线程等待
    • 3.6 获取当前线程引用
    • 3.7 线程休眠
  • 4.线程状态
  • 线程状态转换图
  • 5. 线程安全问题(重要)
    • 5.1 案例分析
    • 5.2 加锁解决办法
    • 5.3 Synchronized的用法
      • 5.3.1 Synchronized到底锁定的是什么元素?
      • 5.3.2 直接修饰普通的方法
      • 5.3.3 修饰一个代码块
      • 5.3.4 修饰一个静态方法
    • 5.4 产生线程不安全的原因
      • 5.4.1 内存可见性例子与解决方法
      • 5.4.2 指令重排序案例

JAVA多线程(1)

   多线程编程,在Java 标准库中就提供了一个Thread 类来表示/操作线程,Thread 类也可以视为是Java 标准库提供的API
 创建好的 Thread 实例,和操作系统中的线程是一一对应的关系,比如创建了十个thread实例,就会有十个线程。

1.Thread类创建与使用

1.1 继承Thread类

    Thread 类的基本用法通过 Thread 类创建线程,其中最简单的做法就是创建子类,继承自 Thread,并且重写 run 方法

class thread extends Thread {
    @Override
    public void run(){
        System.out.println("hello");
    }
}
public class demo1{
    public static void main(String[] args) {
        Thread t=new thread();
        t.start();
    }
}

在这里插入图片描述
    其中重写了run()方法之后,run()里面的内容就描述了这个线程内部要执行哪些代码,每个线程都是并发执行的,各自执行各自的代码,因此就需要告知这个线程,需要执行的代码是什么。
  run()是在新创建出来的线程中执行的,并不是一定义这个类,一写 run方法线程,就创建出来,相当于安排了任务,但是线程还没开始执行。

在这里插入图片描述
    让线程开始执行需要调用start 方法, 才是真正的在系统中创建了线程,才是真正开始执行上面的 run 操作,在调用 start 之前,系统中是没有创建出线程的.创建Thread 实例,就已经是把任务给准备好了,t.start是正式开始执行

1.2 实现并发

    Thread类创建的线程都是在同一个进程内部创建的,那么上面的代码只是创建了一个进程,怎么实现线程并发执行呢?下面的代码就可以实现并发运行.

class thread2 extends Thread {
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
       //创建出的thread线程
        Thread t=new thread2();
        t.start();
        
     //main线程
        while(true){
            System.out.println("hello");
            Thread.sleep(1000);
        }
    }

}

    如果在一个循环中不加任何限制,这个循环转的速度非常快,导致打印的东西太多了,根本看不过来,就可以加上一个sleep操作,强制让这个线程休眠一段时间,这个休眠操作,就是强制的让线程进入阻塞状态,单位是ms,1s之内这个线程不会到cpu上执行.

在这里插入图片描述

    现在两个线程都是打印一条,就休眠1s,当1s时间到了之后,系统先唤醒谁呢,这个顺序是随机的.
    每一轮1s时间到了之后,到底是先唤醒 main 还是 thread,这是随机的,对于操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的(抢占式执行),这个随机性会给多线程编程带来很多其他的麻烦.
在这里插入图片描述
    在一个进程中,至少会有一个线程,在一个java 进程中,也是至少会有一个调用 main 方法的线程,这个线程不是手动创建出来的,是自动创建的
    自己创建的t线程和自动创建的 main 线程就是并发执行的关系,(宏观上看起来是同时执行)此处的并发 = 并行+并发,宏观上是区分不了并行和并发的,都取决于系统内部的调度

关于sleep()

    sleep到了ms级并不是那么精确的,sleep(1000) 并不是说正好 1000ms 之后就上 cpu,意思 1000之内到不了cpu上运行,sleep(1000) 写了之后,可能是1006ms才上的cpu,sleep(1001)写了之后,可能是1002ms上的cpu
Thread.sleep()写在哪个线程,哪个线程就休眠

public class demo18 {
    //测试线程t和main线程之间中断问题
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
          for(int i=0;i<3;i++){
                System.out.println("hello thread");
                System.out.println("t线程休眠2s");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("线程开始执行");
        System.out.println("main线程执行打印");
        Thread.sleep(10000);
        System.out.println("main休眠10秒");
        System.out.println("线程执行结束");

    }
}

    下图代码测试sleep休眠,其中有两个线程,t线程和main线程,main方法开始执行时,t线程和main线程并发运行,先执行main方法的代码,然后调用t线程run方法,打印3次,每次休眠2s,此时main线程也是在运行的,休眠10s,等到t线程打印完成,main线程再休眠4s后开始运行.
在这里插入图片描述
在这里插入图片描述

1.3 Runnable创建线程

   Thread 类创建线程的第二种写法:
  创建一个类实现 Runnable 接口,再创建 Runnable 实例传给 Thread 实例,通过 Runnable 来描述任务的内容,进一步的把描述好的任务交给Thread 实例

class MyRunnable implements Runnable{
    @Override
    public void run(){
        System.out.println("hello MyRunnable");
    }
}
public class demo3 {
    public static void main(String[] args) {
        Thread t=new Thread(new MyRunnable());
        t.start();
    }
}

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

1.4 匿名内部类创建线程

  第三种写法和第四种写法:使用了匿名内部类

public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run(){
                System.out.println("hello thread");
            }
        };
        t.start();
    }

在这里插入图片描述
在这里插入图片描述
    创建了一个匿名内部类,继承自Thread类同时重写 run方法,同时再new出这个匿名内部类的实例.


    new 的 Runnable,针对这个创建的匿名内部类同时 new出的Runnable实例传给Thread 的构造方法

public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread");
            }
        });
        t.start();
    }

在这里插入图片描述
在这里插入图片描述
    通常认为 Runnable 这种写法更好一点,能够做到让线程和线程执行的任务,更好的进行解耦.
    Runnable 单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,Runnable 本身并不关心,Runnable 里面的代码也不关心.

1.5 lamda表达式创建线程

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

  public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello thread");
        });
        t.start();
    }

2.多线程提高效率

    多线程能够提高任务完成的效率,假设有两个整数变量,分别要对这俩变量自增 10亿次,分别使用一个线程,和两个线程来比较速度,如下图代码所示:

public class demo7 {
    private static final int count=10_0000_0000;
    //串行执行代码
    public static void serial(){
        //记录程序执行时间
        long begin =System.currentTimeMillis();
        long a=0;
        for (long i = 0; i < count; i++) {
            a++;
        }
        long b=0;
        for (long i = 0; i < count; i++) {
            b++;
        }
        long end =System.currentTimeMillis();
        System.out.println("串行消耗时间:"+(end-begin)+"ms");
    }
    //使用多线程
    public static void ConCurrency(){
        long begin =System.currentTimeMillis();

        Thread t=new Thread(()->{
            long a=0;
            for (long i = 0; i < count; i++) {
                a++;
            }
        });
        t.start();

        Thread t2=new Thread(()->{
            long b=0;
            for (long i = 0; i < count; i++) {
                b++;
            }
        });
        t2.start();
         // 现在这个求时间戳的代码是在 main 线程中.
        // main 和 t, t2 之间是并发执行的关系,此处 t 和 t2 还没执行完呢,这里就开始记结束时间了,这显然是不准确的
        // 正确做法应该是让 main 线程等待 t 和 t2 跑完了,再来记结束时间
        // join 效果就是等待线程结束。t.join 就是让 main 线程等待 t 结束,t2.join 让 main 线程等待 t2 结束
        t.join();
        t2.join();
        long end =System.currentTimeMillis();
        System.out.println("多线程消耗时间:"+(end-begin)+"ms");
    }


    public static void main(String[] args) {
        serial();
        ConCurrency();
    }
}

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

    可以看到使用多线程确实提高了运行效率,但并不是说,一个线程500多ms,两个线程就是 300 多ms,这俩线程在底层到底是并行执行,还是并发执行不确定,真正并行执行的时候效率才会有显著提升.
    如果count 太小,这个时候创建线程本身也是有开销的,主要的时间就花在创建线程上了,把join()去掉,就是创建线程的时间,光创建俩线程本身就得花50ms,如果你自增本身才花 20ms,此时用多线程肯定是得不偿失
    多线程不是万能良药,不是使用了多线程,速度一定能提高,还是得看具体的场景.多线程特别适合于那种 CPU 密集型的程序.程序要进行大量的计算,使用多线程就可以更充分的利用CPU 的多核资源

3.Thread类属性和方法

3.1 Thread(String name)

    Thread(String name),这个方法是给线程(thread 对象) 起一个名字,起一个啥样的名字,不影响线程本身的执行,仅仅只是影响到调试.可以借助一些工具看到每个线程以及名字,很容易在调试中对线程做出区分

public class demo8 {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while(true){
                System.out.println("hello thread1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"thread1");
        t1.start();
        Thread t2=new Thread(()->{
            while(true){
                System.out.println("hello thread2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"thread2");
        t2.start();
    }
}

可以使用jconsole 来观察线程的名字,jconsole在JDK的bin目录中,
通过Jconsole来)
在这里插入图片描述
点击当前运行的线程,进行连接切换到线程,可以看到thread1和thread2
在这里插入图片描述

    此处的 t1 和 t2就是代码中创建的线程,java 进程一启动,不仅仅是你代码中的线程,还有一些其他的线程.(JVM 自己创建的,分别来做一些不同的工作)
在这里插入图片描述

3.2 isDaemon()

    isDaemon():是否后台线程
    如果线程是后台线程,就不影响进程退出,如果线程不是后台线程(前台线程),就会影响到进程退出.
    创建的t1和t2默认都是前台的线程,即使 main 方法执行完毕,进程也不能退出,要等 t1 和 t2 都执行完,整个进程才能退出.
    如果 t1 和 t2 是后台线程,此时如果 main 执行完毕,整个进程就直接退出t1 和 t2 就被强行终止了

3.3 isAlive()

    isAlive():操作系统中对应的线程是否正在运行
    Thread t 对象的生命周期和内核中对应的线程生命周期并不完全一致,创建出t对象之后,在调用 start 之前系统中是没有对应线程的,在 run 方法执行完了之后,系统中的线程就销毁了,但是t对象可能还存在
    通过isAlive 就能判定当前系统的线程的运行情况:
    如果调用start之后,run 执行完之前,isAlive就是返回true
    如果调用start 之前,run 执行完之后,isAlive就返回false

3.3 线程的重要方法

    run只是一个普通的方法,描述了任务的内容,start 会在系统中创建线程

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

    当调用start的时候,会创建两个线程抢占式执行,改成run()之后就变成了串行执行,run 方法只是一个普通的方法,你在main 线程里调用 run,其实并没有创建新的线程
    使用run():代码只是在一个线程中执行,代码就得从前到后的按顺序运行,先运行第一个循环,再运行第二个循环,第一个循环是死循环,所以会一直执行,
    如果把第一个循环改成有次数的循环,循环结束后会执行第二个循环。

    死循环调用run()的时候:

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

    非死循环调用run()的时候:
在这里插入图片描述
在这里插入图片描述

3.4 中断线程

    中断线程:让一个线程停下来,线程停下来的关键是要让线程对应的 run 方法执行完,(还有一个特殊的是 main 这个线程,对于 main 来说,得是 main 方法执行完,线程就完了)

    1)可以手动的设置一个标志位(自己创建的变量, boolean),来控制线程是否要执行结束

public class demo10 {
        private static boolean isQuit=false;

        public static void main(String[] args) throws InterruptedException {
            Thread t=new Thread(()->{
                while(!isQuit){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
            Thread.sleep(5000);
            isQuit=true;
            System.out.println("终止线程");
    }
}

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

    在其他线程中控制这个标志位就能影响到这个线程的结束,因为多个线程共用同一个虚拟地址空间,因此,main 线程修改的 isQuit 和 t 线程判定的 isQuit, 是同一个值

3.4.1 Thread内置标志位

    Thread.interrupted这是一个静态的方法
    Thread.currentThread.islnterrupted这是实例方法,其中 currentThread 能够获取到当前线程的实例

在这里插入图片描述

public class demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(5000);
        // 在主线程中,调用 interrupt 方法,来中断这个线程
        // t.interrupt 的意思就是让 t 线程被中断!!
        t.interrupt();
    }
}

   t.interrupt()调用这个方法可能产生两种情况:

  1. 如果 t 线程是处在就绪状态. 就是设置线程的标志位为 true

  2.如果 t 线程处在阻塞状态(sleep 休眠了),就会触发一个 InterruptException~

    这里明显是t线程处在阻塞状态了,此处的中断是希望能够立即产生效果的,如果线程已经是阻塞状态下,此时设置标志位就不能起到及时唤醒的效果。

    调用这个interrupt 方法,就会让 sleep 触发一个异常从而导致线程从阻塞状态被唤醒,当下的代码,一旦触发了异常之后, 就进入了 catch 语句.
 在catch中,就单纯的只是打了一个日志,printStackTrace 是打印当前出现异常位置的代码调用栈,打印完日志之后,就直接继续运行,所以加了一个break方法。

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

3.5 线程等待

    线程等待:多个线程之间,调度顺序是不确定的
 线程之间的执行是按照调度器来安排的.这个过程可以视为是“无序,随机这样不太好.有些时候我们需要能够控制线程之间的顺序,线程等待,就是其中一种,控制线程执行顺序的手段.

    t .join():首先,调用这个方法的线程是 main 线程,针对 t 这个线程对象调用的,此时就是让 main 等待 t,调用join 之后,main 线程就会进入阻塞状态(暂时无法在 cpu 上执行),代码执行到 join 这一行,就暂时停下了,不继续往下执行了,然后 join 啥时候能继续往下走,恢复成就绪状态呢?
    等到线程执行完毕(t 的 run 方法跑完了)通过线程等待,就是在控制让先结束,main 后结束, 一定程度上的预了这两个线程的执行顺序.

在这里插入图片描述

    join 操作默认情况下,是死等,这不合理,join 提供了另外一个版本,就是可以执行等待时间,最长等待多久,如果等不到,就不等了.
   t.join( millis: 1000),进入join 也会产生阻塞这个阻塞不会一直持续下去
    如果 10s 之内,t 线程结束了,此时 join 直接返回.
    如果 10s 之后,t 仍然不结束,此时join也就直接返回不等了.
    日常开发中涉及到的一些"等待"相关的操作,一般都不会是死等,而是会有这样的超时时间

3.6 获取当前线程引用

在这里插入图片描述

public class demo12 {
    public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println(Thread.currentThread().getName());
        });
        thread.start();
        System.out.println(Thread.currentThread().getName());
    }
}

在这里插入图片描述
在这里插入图片描述
    Thread.currentThread就能够获取到当前线程的引用(Thread 实例的引用)哪个线程调用的这个currentThread,就获取到的是哪个线程的实例

public class demo13 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run(){
                System.out.println(this.getName());
            }
        };
        t.start();
        System.out.println(Thread.currentThread().getName());
    }
}

    此处的 this 不是指向 Thread 类型了而是指向 Runnable.而 Runnable只是一个单纯的任务,没有name属性的,要想拿到线程的名字,只能通过 Thread.currentThread
在这里插入图片描述

Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
               // System.out.println(this.getName());
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
        System.out.println(Thread.currentThread().getName());

在这里插入图片描述

3.7 线程休眠

    线程休眠到底是在干啥?
    进程PCB + 双向链表, 这个说法是针对只有一个线程的进程是如此的.
    如果是一个进程有多个线程,此时每个线程都有一个 PCB一个进程对应的就是一组 PCB了,PCB 上有一个字段 tgroupld,这个id 其实就相当于进程的 id.同一个进程中的若干个线程的 groupld 是相同的
    process control block进程控制块,和线程有啥关系,其实 Linux内核不区分进程和线程,进程线程是程序猿写应用程序代码搞出来的词.实际上 Linux内核只认 PCB,在内核里 Linux 把线程称为轻量级进程

在这里插入图片描述
    如果某个线程调用了 sleep 方法, 这个 PCB 就会进入到阻塞队列,操作系统调度线程的时候就只是从就绪队列中挑选合适的 PCB到 CPU 上运行,阻寒队列里的 PCB 就只能干等着,当睡眠时间到了系统就会把刚才这个 PCB 从阻塞队列挪回到就绪队列

4.线程状态

    new和Terminated是Java 内部搞出来的状态,和操作系统中的PCB 里的状态就没啥关系
    NEW:把 Thread 对象创建好了,但是还没有调用 start
在这里插入图片描述

Thread t=new Thread(()->{
     //线程里面什么都不做
        });
        System.out.println(t.getState());
        t.start();

在这里插入图片描述
    TERMINATED:操作系统中的线程已经执行完毕,销毁了,但是 Thread 对象还在获取到的状态
在这里插入图片描述
在这里插入图片描述

 public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{

        });
        System.out.println(t.getState());
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }

    Runnable:就绪状态,处于这个状态的线程,就是在就绪队列中随时可以被调度到 CPU 上,如果代码中没有进行 sleep,也没有进行其他的可能导致阻塞的操作.代码大概率是处在 Runnable 状态的
在这里插入图片描述

 public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while(true){

            }
        });
        //System.out.println(t.getState());
        t.start();
        //Thread.sleep(1000);
        System.out.println(t.getState());
    }

在这里插入图片描述


     time-waiting:表示排队等着其他事情,代码中调用了 sleep,就会进入到 TIMED WAITING,一定时间到了之后,阻塞状态解除这种情况就是 TIMED WAITING,也是属于阻塞的状态之一

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

线程状态转换图

在这里插入图片描述

5. 线程安全问题(重要)

    线程安全问题,是整个多线程中最重要,也最复杂的问题,如果面试官问了多线程相关的内容,就一定会问到线程安全.日常开发中如果用到多线程编程,也一定会涉及到线程安全问题.
    操作系统,调度线程的时候是随机的(抢占式执行),正是因为这样的随机性就可能导致程序的执行出现一些 bug.
    如果因为这样的调度随机性引入了 bug,就认为代码是线程不安全的!
    如果是因为这样的调度随机性,没有带来bug,就认为代码是线程安全的!

5.1 案例分析

   一个线程不安全的典型案例:使用两个线程对同一个整型变量,进行自增操作,每个线程自增5w次看最终的结果.

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

class Counter{
    int count;
    public void increase(){
        count++;
    }
}
public class demo15 {
    private static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
        //线程安全
        Thread t1=new Thread(()->{
            int count=0;
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            int count=0;
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

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

   执行顺序:
在这里插入图片描述
在这里插入图片描述
   也有可能出现先t2后t1的情况
在这里插入图片描述
    也有很多其他情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.2 加锁解决办法

   这时候就要使用加锁来解决线程安全问题,在自增之前,先加锁,在自增之后,再解锁

在这里插入图片描述
   t1已经把锁给占用了,此时 t2 尝试lock就会发生阻塞,lock 会一直阻塞直到 t1 线程执行了unlock.

   通过阻塞,就把乱序的并发变成了一个串行操作,运算结果就对了,加了锁之后,并发程度就降低了,数据更靠谱了,但是速度也就慢了.

   实际开发中,一个线程中要做的任务是很多的,例如,线程里要执行:步骤1,步骤2,步骤3,步骤4,其中很可能只有 步骤4 才涉及到线程安全问题,只针对 步骤4 加锁即可.此时上面的123步骤都可以并发执行.

5.3 Synchronized的用法

    synchronized为同步的意思,这个同步这个词在计算机中是存在多种意思的,不同的上下文中,会有不同的含义.
    比如,在多线程中,线程安全中,同步其实指的是"互斥".
   比如,在IO或者网络编程中,同步相对的词叫做"异步",此处的同步和互斥没有任何关系,和线程也没有关系了,表示的是消息的发送方,如何获取到结果.

5.3.1 Synchronized到底锁定的是什么元素?

在这里插入图片描述

5.3.2 直接修饰普通的方法

    1.直接修饰普通的方法
在这里插入图片描述
在这里插入图片描述
    给方法直接加上 synchronized 关键字,此时进入方法就会自动加锁,离开方法,就会自动解锁.如果直接修饰普通方法,也就相当于把锁对象指定为 this 了.

    当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程 就处在 BLOCKED 状态),阻塞会一直持续到占用锁的线程把锁释放为止.

    使用 synchronized 的时候,本质上是在针对某个"对象"进行加锁,此时锁对象就是 this,在 Java 中,每个类都是继承自 Object ,每个 new 出来的实例,里面一方面包含了设置好的属性,一方面包含了“对象头”,对象的一些元数据

在这里插入图片描述

5.3.3 修饰一个代码块

    需要显式指定针对哪个对象加锁.(Java 中的任意对象都可以作为锁对象)
    这种随手拿个对象都能作为所对象的用法,这是 Java 中非常有特色的设定(别的语言都不是这么搞.正常的语言都是有专门的锁对象)

在这里插入图片描述
在这里插入图片描述
    this代表当前对象的引用,Counter类只创建了一个counter,调用方法时两个都是counter,都调用increase方法,会有两个线程都对this加锁,就会产生锁的竞争,代码执行从无序到有序.
    当两个线程同时针对一个对象加锁,才会产生竞争,如果两个线程针对不同对象加锁,就不会有竞争

5.3.4 修饰一个静态方法

相当于针对当前类的类对象加锁
Counter.class


5.4 产生线程不安全的原因

  产生线程不安全的原因:

  不是所有的多线程代码都要加锁.(如果这样了,多线程的并发能力就形同虚设)

  1.线程是抢占式执行,线程间的调度充满随机性.
     线程不安全的万恶之源,虽然这是根本原因,但是咱们无可奈何

  2.多个线程对同一个变量进行修改操作.
     如果是多个线程针对不同的变量进行修改没关系,如果多个线程针对同一个变量读也没关系,可以通过调整代码结构,使不同线程操作不同变量

  3.针对变量的操作不是原子的
     针对有些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的,通过加锁操作,也就是把好几个指令给打包成一个原子的了,加锁操作就是把这里的多个操作打包成一个原子的操作.

  4.内存可见性
     内存可见性,也会影响到线程安全.针对同一个变量,一个线程进行读操作(循环进行很多次)一个线程进行修改操作(合适的时候执行一次).

  5.指令重排序也会影响到线程安全问题
     指令重排序,也是编译器优化中的一种操作,咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓,编译器就会智能的调整这里代码的前后顺序从而提高程序的效率,保证逻辑不变的前提再去调整顺序.
 如果代码是单线程的程序,编译器的判定一般都是很准,但是如果代码是多线程的,编译器也可能产生误判,synchronized不光能保证原子性同时还能保证内存可见性,同时还能禁止指令重排序

5.4.1 内存可见性例子与解决方法

在这里插入图片描述
    如图所示,t1这个线程,在循环读变量.
    读取内存操作,相比于读取寄存器是一个非常低效的操作,因此在 t1 中频繁的读取这里的内存的值就会非常低效.
    如果t2线程迟迟不修改内存的值,t1线程读到的值始终是一样的值.
    因此t1就有了一个大胆的想法,就不会再从内存读数据了,而是直接从寄存器里读,不执行load了.
    一旦t1做出了这种大胆的假设,此时万一t2修改了count值t1就不能感知到了.

在这里插入图片描述

public class demo16 {
    //private static volatile  int isQuit=0;
    private static int isQuit=0;
    public static void main(String[] args) {
        Thread t=new Thread(()->{
           while(isQuit==0){

           }
            System.out.println("t线程执行结束");
        });
        t.start();
        Scanner sc=new Scanner(System.in);
        System.out.println("请输入isQuit的值:");
        isQuit=sc.nextInt();
        System.out.println("main线程执行结束");
    }
}

在这里插入图片描述
    从上述代码可以看到,t线程一直执行,不从内存中读取值了,改变isQuit的值也无济于事,那么该怎么解决呢?

  1.使用 synchronized 关键字

    synchronized不光能保证指令的原子性,同时也能保证内存可见性,被synchronized 包裹起来的代码,编译器就不敢轻易的做出上述假设,相当于手动禁用了编译器的优化

  2.使用 volatile 关键字

  禁止编译器做出上述优化,编译器每次执行,判定相等都会重新从内存读取isQuit的值.

在这里插入图片描述

    内存可见性,是属于编译器优化范围中的一个典型案例,编译器优化本身是一个玄学的问题,啥时候优化,啥时候不优化,很难说,像这个代码循环中加上sleep这里的优化就消失,也就没有内存可见性问题了
在这里插入图片描述
在这里插入图片描述

5.4.2 指令重排序案例

重排序的好处:提高处理速度
在这里插入图片描述
    图中左侧是 3 行 Java 代码,右侧是这 3 行代码可能被转化成的指令。
    可以看出 a = 100 对应的是 Load a、Set to 100、Store a,意味着从主存中读取 a 的值,然后把值设置为 100,并存储回去,同理, b = 5 对应的是下面三行 Load b、Set to 5、Store b,最后的 a = a + 10,对应的是 Load a、Set to 110、Store a。
    如果你仔细观察,会发现这里有两次“Load a”和两次“Store a”,说明存在一定的重排序的优化空间。

在这里插入图片描述
    重排序后, a 的两次操作被放到一起,指令执行情况变为 Load a、Set to 100、Set to 110、 Store a。
    下面和 b 相关的指令不变,仍对应 Load b、 Set to 5、Store b。
    可以看出,重排序后 a 的相关指令发生了变化,节省了一次 Load a 和一次 Store a。
    重排序通过减少执行指令,从而提高整体的运行速度.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/151334.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构:图

文章目录图内存中存储图数据结构邻接矩阵存储方法用邻接矩阵&#xff08;Adjacency Matrix&#xff09;来表示一个图的缺点&#xff1a;浪费空间优点邻接表存储方法&#xff08;Adjacency List&#xff09;广度优先算法Breadth-First-Search&#xff08;BFS&#xff09;深度优先…

Android——GT库-日志工具

GT库在创造出来初期&#xff0c;里面的日志工具就一直存在的&#xff0c;经历了很久的迭代变更&#xff0c;当目前的最新版本&#xff0c;日志工具已经创造出更高级的调试日志方式了&#xff0c;接下来咋们来看看GT库中的日志工具具体使用方法吧。 使用GT库里的&#xff0c;当然…

web表单设计器的优点体现在哪?

在数字化管理越来越规范的当下&#xff0c;拥有一款优质高效的低代码开发平台&#xff0c;确实能给企业提质增效带来更大的帮助。很多客户朋友会问道&#xff1a;web表单设计器都有哪些特点&#xff1f;为什么能在企业的现代化办公管理中起到巨大的作用&#xff1f;今天&#x…

Linux终端远程工具xshell,xftp,mobasterm

目录 软件介绍 1.xshell 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 第四步&#xff1a; 第5步&#xff1a; 2.xftp 第一步&#xff1a; 第二部&#xff1a; 第三步&#xff1a; 3.mobasterm 全能终端神器——MobaXterm 第一步&#xff1a; 第二步&a…

C1083无法打开包括文件: “atlbase.h”: No such file or directory

在打开别人的项目的过程中遇到了“atlbase.h”无法打开的问题&#xff0c;在此记录一下。1.下载ATL生成工具与缓解只下载ATL生成工具后面还会报错&#xff0c;直接下载下载ATL生成工具与缓解一步到位。下载的入口在&#xff1a;工具--->获取工具与功能。需要注意的是&#x…

Guitar Pro2023Win/Mac中文吉他/贝斯打谱识谱软件

Guitar Pro 是一款曲谱阅读器。以 GTP 结尾的曲谱文件都必须用 Guitar Pro 才能打开。Guitar Pro 凭借着其便利的制谱和读曲谱环境&#xff0c;在各大谱库论坛里都占据着一席之地&#xff0c;喜欢吉他的朋友一定略有耳闻。早几年该作者将它移植到了移动平台&#xff0c;现在你也…

7-2国王游戏

题目&#xff1a; 恰逢 H 国国庆&#xff0c;国王邀请 n 位大臣来玩一个有奖游戏。 首先&#xff0c;他让每个大臣在左、右手上面分别写下一个整数&#xff0c;国王自己也在左、右手上各写一个整数。 然后&#xff0c;让这 n 位大臣排成一排&#xff0c;国王站在队伍的最前面。…

应用层——Web和HTTP

目录 1. HTTP概况 1.1 Web页面简介 1.2 URL-统一资源定位器 1.3 HTTP协议 2. HTTP连接的两种类型 2.1 HTTP非持久性连接(Non-persistent HTTP) 2.2 HTTP持久性连接(Persistent HTTP) 2.2.1 无流水(pipelining)的持久性连接 2.2.2 带有流水机制的持久性连接 3. HT…

一站式开发平台赋能办公全场景

近几年&#xff0c;数字化办公迎来了新的机遇&#xff0c;根据亿欧智库《2022中国数字化办公市场研究报告》推算&#xff0c;数字化办公2021年的市场规模达到973.89亿元&#xff0c;至2025年将达到1768.16亿元&#xff0c;整体增速保持平稳&#xff0c;2018-2025年的CAGR为15.8…

Mybatis 框架搭建封装JDBC,实现sql语句

目录 1、maven新建一个工程​编辑 2、添加POM.XML配置文件 3、创建实例包 4、创建一个环境资源根目录 5、配置环境文件 6、创建接口&#xff0c;添加方法 7、编写sql语句 8、创建测试类 8.1 、定义工厂模式 8.2 、定义会话 8.3、定义对象 8.5、获取Builder建造工厂 …

LAB3 EIGRP1实验

1 实验拓扑&#xff1a; 2 实验要求&#xff1a; 1>.R1-R3环回口0:192.168.100.x/32。 2>.R1上采用手动汇总的命令&#xff0c;汇总4条环回口成一条。 3>.R1上下发一条默认路由。 4>.实现R1到R2的环回口路由非等价负载。 5>.as 90都使用eigrp认证。 6>…

css动画效果之transition

transition-property规定设置过渡效果的 CSS 属性的名称。属性名属性值none没有属性会获得过渡效果。all所有属性都将获得过渡效果。property定义应用过渡效果的 CSS 属性名称列表&#xff0c;列表以逗号分隔。使用方式transition-property: width,background;/* 多个效果可用逗…

设计模式之装饰模式

1.前言 装饰模式&#xff1a;动态的给一个类添加一些额外职责&#xff0c;就增加功能来说&#xff0c;装饰模式比生成子类更加灵活。 装饰模式属于结构型模式&#xff0c;它是作为现有的 类的⼀个包装&#xff0c;允许向⼀个现有的对象添加新的功能&#xff0c; 同时⼜不改变其…

Spring创建和使用 (存储和读取) -- 1

Spring创建和使用 存储和读取 -- 1一、创建 Spring 项目1.1 创建⼀个 Maven 项目1.2 添加 Spring 框架支持1.3 添加启动类二、存储 Bean 对象2.1 创建 Bean2.2 将 Bean 注册到容器三、获取并使用 Bean 对象3.1 创建 Spring 上下文3.2 获取指定的 Bean 对象3.3 使用 Bean四、总结…

数据结构-第六期——并查集(Python)

目录 认识并查集 经典应用: 应用场景 并查集的操作 初始化 代码实现 合并 代码实现 查找 代码实现 查找代码【图解】 有多少个集&#xff08;帮派&#xff09;? 复杂度 查询的优化:路径压缩 【代码】用递归实现 并查集:初始化、查找、合并代码 蓝桥杯…

ES6之Promise

Promise是异步操作的一种解决方案 // 1.认识Promisedocument.addEventListener(click,()>{console.log(这里是异步的);});console.log(这里是同步的); Promise一般用来解决层层嵌套的回调函数&#xff08;回调地狱&#xff09;的问题 <!DOCTYPE html> <html lan…

JVM垃圾回收机制、JVM垃圾回收算法、JVM CMS与G1垃圾收集,JVM内存模型

C C 需要自己回收垃圾 重复回收&#xff1a; 回收掉别人的东西 忘记回收&#xff1a; 内存泄漏 Java虚拟机做自动化回收 垃圾回收器 Root Searching&#xff08;根可达&#xff09; GC Algorithms(垃圾回收算法) Mark-Sweep(标记清除) 缺点&#xff1a;碎片化&#xff0c;一…

Lua C接口编程(一)

引言 skynet 和 openresty 都是深度使用lua的典范&#xff0c;学习lua不经要学会基本语法&#xff0c;还要学会C语言与Lua交互。lua的一大优点就是能和c/c无缝连接&#xff0c;而且可以在不需要重复编译c/c的情况下可以修改lua文件并且起作用&#xff0c;当我们的项目文件很大…

【面试题】做了一份前端面试复习计划,保熟~

大厂面试题分享 面试题库前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面试题库前言以前我看到面试贴就直接刷掉的&#xff0c;从不会多看一眼&#xff0c;直到去年 9 月份我开始准备面试时&#xff0c;才发现很多面试经验贴…

Kubernetes(k8s) 笔记总结(二)

提示&#xff1a;针对kubernetes的工作均衡学习。 文章目录1. Kubernetes 创建资源方式2. Kubernetes 操作NameSpace3. Kubernetes的 Pod应用3.1 Pod的 解释3.2 通过命令行来创建一个pod3.3 配置文件方式创建一个Pod3.4 dashboard 可视化操作Pod3.5 针对Pod的一些细节操作3.6 P…