JAVA多线程初阶(1)

文章详细介绍了JAVA中创建和使用线程的不同方式,包括继承Thread类、实现Runnable接口、使用匿名内部类和Lambda表达式。讨论了线程的并发执行、sleep方法的作用,以及线程状态转换。此外,文章还强调了线程安全问题,分析了加锁(如Synchronized)解决并发问题的重要性,并给出了线程不安全的案例和解决方案。
摘要由CSDN通过智能技术生成

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()是在新创建出来的线程中执行的,并不是一定义这个类,一写 run方法线程,就创建出来,相当于安排了任务,但是线程还没开始执行。

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

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();
    }

1.6 关于run

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

run不是一个随便的方法,是重写了父类的方法,t.start是调用了操作系统里的api,start会创建新线程,然后在新线程里调用t.run,所以run可以叫做线程的入口方法.

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,main线程在调用完t2.start就结束了,代码执行速度很快,所以看不到。

在这里插入图片描述

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

3.2 isDaemon()

在这里插入图片描述

    isDaemon():是否后台线程
    如果线程是后台线程,就不影响进程退出,如果线程不是后台线程(前台线程),就会影响到进程退出.isDaemon(false)代表前台线程,true代表后台线程,前台线程会阻止java进程结束,必须要java 进程中所有的前台线程都执行完,java进程才能结束,后台线程不阻止java进程结束,哪怕后台线程还没执行完,java 进程该结束就结束了.
    创建的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(()->{
        //Thread.currentThread().isInterrupted()默认是false
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread"); 
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        // 在主线程中,调用 interrupt 方法,来中断这个线程
        // t.interrupt 的意思就是让 t 线程被中断!!
        t.interrupt();
    }
}

   1.调用上述代码,我们可以看到,线程并没有中断,而是在3s后打印了当前异常位置的调用栈,然后继续执行代码。

   2.interrupt的作用:一个是设置标志位为true,另一个是如果该线程正在阻塞中(比如在执行sleep),此时线程就会把阻塞状态唤醒,通过抛出异常的方式让sleep立即结束。

   3.但是当sleep被唤醒的时候,sleep会自动把is interrupted的标志位给清空(true->false),导致下次循环的时候,循环仍然可以继续执行。

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

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

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

3.5 线程等待

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

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

    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 了.例如上述代码锁的对象就是counter,谁调用了increase方法,谁就是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。
    重排序通过减少执行指令,从而提高整体的运行速度.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值