重学 Java 基础之线程基础(二)

2023-11-28 15:35:40 浏览数 (1)

1、重学 Java 基础之线程基础(二)

1.1、volatile
1.1.1、基础概念

Java 内存模型规定了所有的变量都存储在主内存中,此处的主内存仅仅是虚拟机内存的一部分,而虚拟机内存也仅仅是计算机物理内存的一部分(为虚拟机进程分配的那一部分)。

Java 内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。

每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

read-load 从主内存复制变量到当前工作内存,use-assign 执行代码改变共享变量值,store-write 用工作内存数据刷新主存相关内容。

Java通过几种原子操作完成工作内存和主内存的交互:

  • lock:作用于主内存,把变量标识为线程独占状态。
  • unlock:作用于主内存,解除独占状态。
  • read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  • load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  • use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  • assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  • store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  • write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

由 Java 内存模型来直接保证的原子性变量操作包括 read 、load 、use 、assign 、store 和 write 六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 与 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐匿地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块--- synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。

JVM对交互指令的约束

如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行 read 和 load 操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store 和 write 操作。

Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是 read 和 load 之间,store 和 write 之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 不允许 read 和 load 、store 和 write 操作之一单独出现
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化( load 或 assign )的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。
1.1.2、作用

保证内存可见性,所有线程都能看到共享内存的最新状态。

volatile 的特殊规则就是:

  • read、load、use 动作必须连续出现。
  • assign、store、write 动作必须连续出现。

所以,使用 volatile 变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中。

防止指令重排

指令重排序是 JVM 为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。

经典问题就是懒加载单例模式与 DCL(Double Check Lock,双重检查锁)机制。

volatile如何防止指令重排

volatile 关键字通过“内存屏障”来防止指令被重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java 内存模型采取保守策略。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障
1.1.3、volatile 陷阱

volatile 使得对变量的读写有着相关的“原子性”,但是对于变量值的变化由自己本身的值引起,那么就不是原子性的。如果是由其他变量值引起的原子性就有效。比如:

代码语言:text复制
# 非原子性
public static volatile int n = 0;
n = n   1;
n  ;

# 原子性
n = m   1

其实 volatile 并不保证所有情况都不进行重排序,像下面两种情况,是允许指令重排序的:

1、普通变量的读/写操作,然后 volatile 变量的读操作

2、volatile 变量的写操作,然后普通变量的读/写操作

1.2、多线程会导致的问题以及如何解决

如果线程操作不当,可能会引起死锁。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。

产生死锁的必要条件:

  • 1、互斥条件:即当资源被一个线程使用(占有)时,别的线程不能使用。
  • 2、不可剥夺条件:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 3、请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。
  • 4、循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

1.3、线程通信

有时候我们需要多个线程进行协作完成任务,既然是合作,那么肯定少不了线程之间的通信了。我们来尝试一下多个线程间如何协作完成任务。

现在有一个需求,就是需要两个线程,对一个集合进行数据读取。要求两个线程需要进行交叉读取,比如A线程读了第一个元素,B线程就需要读取第二个元素。

最开始的尝试:

代码语言:c#复制
public class TestThread {

    public static List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    public static int point = 0;

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();

    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i  ) {
                try {
                    System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                    TestThread.point  ;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Thread2 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i  ) {
                try {
                    System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                    TestThread.point  ;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

# 结果

Thread-0:1
Thread-1:1
Thread-0:3
Thread-1:3
Thread-1:5
Thread-0:5
Thread-1:7
Thread-0:7
Thread-1:9
Thread-0:9

按理来说是两个线程读取的数据是不重复的,但是我们发现两个线程读取的数据重复了,因为我们递增操作没有同步。我们进行下一步尝试:

代码语言:scala复制
 public static class Thread1 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i  ) {
                try {
                    synchronized(TestThread.class){
                        System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                        TestThread.point  ;
                        Thread.sleep(500);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

# Thread2修改部分一样

# 结果
Thread-0:1
Thread-0:2
Thread-0:3
Thread-1:4
Thread-0:5
Thread-0:6
Thread-1:7
Thread-1:8
Thread-1:9
Thread-1:10

数字是连续了,但是线程没有交叉进行。

然后我们再次进行尝试:

代码语言:text复制
public class TestThread {

    public static List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    public static int point = 0;

    public static boolean read = false;

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();

    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i  ) {
                try {
                    synchronized(TestThread.class){
                        if (!TestThread.read){
                            TestThread.read = true;
                            TestThread.class.notify();
                        }
                        System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                        TestThread.point  ;
						Thread.sleep(500);
                        if (TestThread.point == 10){
                            TestThread.class.notifyAll();
                        }else{
                            TestThread.class.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Thread2 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i  ) {
                try {
                    synchronized(TestThread.class){
                        if (TestThread.read){
                            TestThread.read = false;
                            TestThread.class.notify();
                        }
                        System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                        TestThread.point  ;
						Thread.sleep(500);
                        if (TestThread.point == 10){
                            TestThread.class.notifyAll();
                        }else{
                            TestThread.class.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

# 打印

Thread-0:1
Thread-1:2
Thread-0:3
Thread-1:4
Thread-0:5
Thread-1:6
Thread-0:7
Thread-1:8
Thread-0:9
Thread-1:10

ok,现在是符合我们的需求了。我们来分析一下这段代码:

1、为什么 synchronized 锁的不是this?

如果锁的是 this 的话,线程 A 锁的是 Thread1 的实例,线程 B 锁的是 Thread2 的实例,两个线程在获取锁对象(对象监视器)的时候没有任何阻碍,自然也就没有同步效应了。而锁 TestThread.class ,A 线程访问同步方法就会获得同步锁,B 线程访问线程方法时就会因为获取不到同步锁而被阻塞,这时候自然就变同步了。

2、为什么要使用 TestThread.class.wait() ?

notify 和 wait 需要在同步方法中执行,不然会抛出 IllegalMonitorStateException 异常。不正确的释放锁也会抛出 IllegalMonitorStateException 异常,比如没有锁定某个对象却使用 wait 去释放锁就会抛出异常。wait 等方法是由对象锁去调用,锁的是谁,谁就可以去释放锁。

3、为什么要在最后放置 TestThread.class.wait()?

继上一个问题,同步之后,每个线程在执行完一次循环后,会因为各种因素导致线程执行的优先级不一样,进而导致可能会出现一个线程执行好几次的问题,所以在后面放置一个 wait 让线程执行完一次循环后都停止下来。

4、为什么需要添加 TestThread.class.notifyAll()?

因为这段代码不管谁执行到最后,都会执行 wait 方法,然后最后一个线程会暂停,这时已经没有线程去执行唤醒操作了,如果不添加 notifyAll 判断,这个程序会一直暂停下去,当然是用 notify 也行,虽然 notify 是随机唤醒一个 wait 中的线程,但是因为线程只有两个,一个线程因为之前就已经被唤醒了,所以使用 notify 必定唤醒仅存的一个线程。notifyAll 是唤醒所有 wait 中的线程。

5、为什么结果能保证交叉执行?

TestThread.class.notify() 之后不是会随机唤醒线程并且释放锁吗?释放锁之后 notify 之后的代码不就不是同步的了吗,这样不是很容易线程永久阻塞?这时候就要说说 notify 释放锁的机制了。notify 是会释放锁,但不是立刻释放锁,是等待执行完同步块代码后再释放锁(这个结论我就不测试了),这样就保证了当前线程的 wait 的执行。

还有个带参数的 wait(long),这个方法是如果在指定时间内有线程对其唤醒,就直接唤醒,如果没有则会自动唤醒。单位是ms。

interrupt 可以终止 wait 吗

我们之前不是学了一个 interrupt 方法吗?在线程 wait 的时候调用线程的 interrupt 方法会终止线程吗?答案是会的。

代码语言:java复制
public class TestThread {

    public static void main(String[] args) {

        Thread1 thread1 = new Thread1();
        thread1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.interrupt();
    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
          synchronized (this){
              try {
                  System.out.println("我开始运行了");
                  this.wait();
                  System.out.println("我结束了");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
        }
    }


}

1S后 thread1 线程会抛出一个 InterruptedException 异常。

interrupt 可以终止阻塞中的线程吗?

interrupt 可以终止阻塞中的线程吗?答案是 interrupt 只能终止非阻塞下的线程。

代码语言:java复制
public class TestThread {

    public static void main(String[] args) {

        Thread1 thread1 = new Thread1();
        Thread1 thread2 = new Thread1();

        thread1.start();
        thread2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
        System.out.println("执行中断");
    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
          synchronized (TestThread.class){
              try {
                  System.out.println(Thread.currentThread().getName());
                  Thread.sleep(10000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
        }
    }


}

如果 interrupt 可以终止阻塞的线程,那么在执行 thread2.interrupt(); 的时候就会抛出个异常然后直接结束,但实际上不会,异常还是照样抛出,只不过在执行完 interrupt 后,等待线程 1 执行完后,线程 2 才抛出异常。想要终止阻塞线程,必须使用 wait 方法去释放锁,这样阻塞线程才可能有机会获取锁而继续执行下去。

1.4、join

后续补充……

1.5、ThreadLocal

ThreadLocal,让每个线程都拥有自己的 “小秘密” 。每个线程 ThreadLocal 数据相对于每个线程都是隔离的。

代码语言:text复制
public class TestThread {

    public static ThreadLocal<Integer> local = new ThreadLocal<>();

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name   "设置值666");
            local.set(666);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name local.get());
        });

        Thread thread1 = new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name   "设置值777");
            local.set(777);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name local.get());
        });

        thread.start();
        thread1.start();

    }


}

#打印
Thread-0设置值666
Thread-1设置值777
Thread-0666
Thread-1777

线程 1 设置值后休眠 2 秒,这时候线程 2 执行了设置值,虽然local是全局的静态变量,但是在线程中打印出来的变量还是自己之前设置的值,没有被覆盖。

怎么设置ThreadLocal的默认值呢?

代码语言:typescript复制
public class TestThread {

    public static ThreadLocal local = new TestThreadLocal();

    public static void main(String[] args) {
        System.out.println(local.get());
    }


    public static class TestThreadLocal extends ThreadLocal{
        @Override
        protected Object initialValue() {
            return "688";
        }
    }
}

继承 ThreadLocal 然后重写 initialValue 方法,就能把 null 的默认值给覆盖掉。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞