多线程1-基础、synchronized、volatile

1 . 线程

1.1 基本概述

  1. 线程的理解:一个进程里面的不同的执行路径。
  2. 并发编程的特征:
  • 可见性。多个线程之间数据同步,数据可见。
  • 有序性
  • 原子性

1.2 线程的状态

在这里插入图片描述

  1. New 状态,new Thread()
  2. Runnable状态
- Ready 就绪状态
- Running 运行状态,ready状态的线程,被线程调度器选中,进行执行。
>在running过程中,如果执行了yield方法,那么当前线程会让出一次,会到ready状态。当重新被调度器选中,获得cpu进入Running状态
或者在running过程中,线程调度器选中了另一个线程,那么当前线程被挂起,会到ready状态。当重新被调度器选中,获得cpu进入Running状态。

- wait 阻塞状态。
1 TimedWaiting(按时间来阻塞的) :Thread.sleep(time),o.wait(time), t.join(time), lockSupport.parkUntil(),lockSupport.parkNanos()。 调用这些方法,线程都会进入阻塞状态。当阻塞时间结束,会进入Runnable 状态
2  Wating:   
>o.wait() 进入阻塞,o.notify()或者o.notifyAll()唤醒线程,进入Runnable状态
>t.join()
>lockSupport.park()	进入阻塞,lockSupport.unpark() ,进入Runnable状态。

3.  Blocked ,加锁的情况,在没有获得同步锁的时候,进入阻塞队列,当获取到锁,进入Runnable。 
4.   wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。 wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
  1. Teminated 状态, 线程结束。

如何优雅的终端程序呢
总所周知,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。因为它们太暴力了,是不安全的,这种暴力中断线程是一种不安全的操作。只要线程调用stop方法,线程立刻结束。

  1. 调用stop:
package com.tzw.juc.c0;

import static java.lang.Thread.sleep;

public class C0_Thread3 {

    private static int i;
    private static int j;

    public static void main(String[] args) throws InterruptedException {

        final Object o = new Object();
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable1");
            synchronized (o){
                i=3;
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                j=3;
            }
        },"thread1");
        thread1.start();
        //停1000ms调用stop方法。
        sleep(1000);
        thread1.stop();

        //停2000s,等线程执行完打印i j的值
        sleep(2000);
        System.out.println("i="+i+",j="+j);
    }
}

运行结果为:

i=3,j=0

结果分析:

synchronied块是一个原子服务,要不都赋值成功,要不都失败。但是由于调用线程的stop()方法,暴力的停止了线程。导致j的值没有赋值成功。
  1. 如何优雅的停止线程呢。线程的interrupt方法
package com.tzw.juc.c0;

import static java.lang.Thread.sleep;

public class C0_Thread3 {

    private static int i;
    private static int j;

    public static void main(String[] args) throws InterruptedException {

        final Object o = new Object();
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable1");
            synchronized (o){
                i=3;
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                j=3;
            }
        },"thread1");
        thread1.start();
        //停1000ms调用stop方法。
        sleep(1000);
//        thread1.stop();
        thread1.interrupt();
        //停2000s,等线程执行完打印i j的值
        sleep(2000);
        System.out.println("i="+i+",j="+j);
    }
}

结果为:

	at java.lang.Thread.sleep(Native Method)
	at com.tzw.juc.c0.C0_Thread3.lambda$main$0(C0_Thread3.java:18)
	at java.lang.Thread.run(Thread.java:748)
i=3,j=3

结果分析:

调用interrupt方法,后代码并没有马上终断。而是先throw InterruptedException 异常,然后继续执行,最后i 和 j都赋值成功
  1. interrupt 说明
    interrupt() 相比stop(),是一个比较温柔的做法,与linux 中 stop server 和 kill -9 命令一样,一个是暴力停止,一个是安全停止。

它通过设置线程的中断标志位,并不断的检测这个中断标志位。如果是true,则说明这个线程应该进行中断了,但是不会中断线程,而是通知线程应该中断了,是怎么通知的呢,通过InterruptedException异常进行通知。

抛出异常后需要自己处理,到底是停止线程呢,还是继续运行。说白了,就是通过try…catch。将异常catch,然后在catch块中进行处理,
具体到底中断还是继续运行,应该由被通知的线程自己处理。

但是要注意的是,当抛出InterruptedException错后,中断标志位会恢复为false(非中断状态)。

调用interrupt() 并不能真正的中断线程,如果真的需要中断线程,需要和阻塞方法配置使用,或者调用Thread.interrupted()方法进行判断线程当前的中断标志位是否位true。

  • 判断检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
    判断线程Thread.interrupted(),返回boolean类型,自行处理中断逻辑。

  • 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后直接return就结束线程。)

要注意的是,interrupt不中断线程,只是修改中断标志。

当一个线程处于阻塞状态的时候(sleep,wait,join),调用interrupt方法,就会抛出一个InterruptedException异常,
通过catch这个异常进行处理,根据情况中断线程或者继续运行。

所以只用阻塞+interrupt的时候才会抛错。仅调用interrupt方法不会中断线程,也不会抛异常,不起任何作用。
package com.tzw.juc.c0;

import static java.lang.Thread.sleep;

public class C0_Thread4 {

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable1");
            for (int i = 0; ; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return;
                }
            }
        },"thread1");
        thread1.start();
        sleep(1000);
        thread1.interrupt();
    }
}

2 . 线程实现方式

  • 继承Thread
  • 实现Runnable。可以使用lambda表达式。
  • 线程池
package com.tzw.juc.c0;

public class C0_Thread {

    public static void main(String[] args) {

        C0_Thread c0 = new C0_Thread();
        //1.继承Thread,重写run方法,调用start启动线程
        MyThread myThread = c0.new MyThread();
        myThread.setName("myThread");
        myThread.start();

        //2.Thread的构造方法,实现Runnable接口,匿名内部类当错参数。
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":"+"runnable");
            }
        },"thread1");
        thread1.start();

        //将匿名内部类方式修改类Lambda表达式
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable2");
        },"thread2");
        thread2.start();

        //3. 线程池方式:后面会讲到
    }

    public class MyThread extends Thread{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":"+"myThread");
        }
    }

}

3. 线程的方法

  • sleep(time)方法, 睡眠time时间
  • yield() 方法,调用此方法会让当前线程让出一次
  • join()。 加入,将一个线程加入到另一个线程当中。
  • 通过getState()方法获取当前线程的状态。
  1. 创建两个线程,这两个线程是乱序执行的。thread1 和 thread2 交替打印,如果想先执行thread1,thread1执行完成后,才执行thread2,怎么办,没错,利用join方法。

正常启动线程,线程交替执行

package com.tzw.juc.c0;

public class C0_Thread2 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable1");
            for (int i = 0; i <10 ; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"thread1");

        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable2");
            for (int i = 0; i <10 ; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"thread2");
        thread1.start();
        thread2.start();
    }
}

  1. join方法,在A线程中调用B.join方法,意思就是在A线程中加入一个B线程,执行的时候,当A线程执行到B.join时,停止A线程,当B线程执行完成后,A线程继续执行
    所以想要保证thread1先执行完,在执行thread2,就在thread2中调用thread1.join,先执行thread1,在执行thread2.
package com.tzw.juc.c0;

public class C0_Thread2 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+"runnable1");
            for (int i = 0; i <10 ; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }

        },"thread1");

        Thread thread2 = new Thread(() -> {

            try {
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+"runnable2");
            for (int i = 0; i <10 ; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"thread2");
        thread1.start();
        thread2.start();
    }
}

4. synchronized

4.1 synchronized

  1. synchronized的理解,synchronized是java的一个关键字,能够实现线程同步,是一个同步锁。而实现线程同步的依赖于一个对象锁。任何对象都可以作为锁。使用的这个对象对象头中的两位,用两位来表示当前这个对象锁的状态。
  2. 为什么需要同步锁。
    主要是解决多线程并发的问题。在一个系统中,存在一些共享数据,多线程会操作共享数据,这样就会导致问题,多个线程同时操作共享数据就会导致数据的结果不是想要的。eg a和b线程都操作共享数据data,a 线程将data=3,b线程有将data变成4,当a线程使用data的时候就变成了4。这样就背离的结果。所以,为了避免出现这样的问题,我们肯定想在a线程操作data数据的时候让b线程不能操作数据,也就是让a线程在一个时间段独占共享数据data,其他的线程不能操作data。同一时刻只有一个线程在能够执行。就需要同步锁。
  3. 同步锁,在同一时刻只能有一个线程获取到锁,其他的线程都在等待中,直到锁释放,其他的线程去获取锁,进行执行,一次类推。保证在同一时刻只有一个线程在执行。
  4. synchronized修饰方法。这个方式是一个同步方法,只有获取到锁的线程可以执行方法。
  5. synchronized修改块,同步块,只有获取到锁的线程可以执行块中的代码。
  6. synchronized修饰静态方法,同步静态方法
  7. 并发三大特性分析

原子性
原子性指的是一个或多个操作执行过程中不被打断的特性。被synchronized修饰的代码是具有原子性的,要么全部都能执行成功,要么都不成功。
前面我们提到过,synchronized无论是修饰代码块还是修饰方法,本质上都是获取监视器锁monitor。获取了锁的线程就进入了临界区,锁释放之前别的线程都无法获得处理器资源,保证了不会发生时间片轮转,因此也就保证了原子性。
可见性
所谓可见性,就是指一个线程改变了共享变量之后,其他线程能够立即知道这个变量被修改。我们知道在Java内存模型中,不同线程拥有自己的本地内存,而本地内存是主内存的副本。如果线程修改了本地内存而没有去更新主内存,那么就无法保证可见性。
synchronized在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
有序性 有序性是指程序按照代码先后顺序执行。
synchronized是能够保证有序性的。根据as-if-serial语义,无论编译器和处理器怎么优化或指令重排,单线程下的运行结果一定是正确的。而synchronized保证了单线程独占CPU,也就保证了有序性。

4.2 synchronized两个实现方式

4.2.1 synchronized 方法

    public synchronized void hello(){//相当于synchronized(this)
        for (int i=0;i<10;i++){
            System.out.println(i);
        }
    }

4.2.2 synchronized 块

	public void hello2(){
        synchronized (this){//任何线程想要执行synchronized块中的代码,必须拿到this的锁
            for (int i=0;i<10;i++){
                System.out.println(i);
            }
        }
    }

// 或者
	Object obj = new Object();
    public  void hello3(){
        synchronized (obj){//任何线程想要执行synchronized块中的代码,必须拿到Object的锁
            for (int i=0;i<10;i++){
                System.out.println(i);
            }
        }
    }
//或者(静态方法需要锁class对象)
    public  static void hello3(){
        synchronized (Object.class){//任何线程想要执行synchronized块中的代码,必须拿到Object.class的锁
            for (int i=0;i<10;i++){
                System.out.println(i);
            }
        }
    }

4.2.3 Synchronized 方法 和Synchronized 块

那么这两种写法有什么不同呢。
如果一个方法的方法体和synchronized块中的代码一样,那么就可以直接写成synchronized方法。
所以本质上没有区别,synchronized块能够更精确的控制一段代码的原子性。使用更灵活。

4.2.4 synchronized 相关

  1. 一个类中的所有的synchronized方法 相当于是synchronized(this),该类中所有的方法用的是同一把对象锁。
  2. synchronized是可重入锁。
    实现线程锁,就是当一个线程获取到对象锁,另外的线程就获取不到这个锁。
    可重入的意思是,当一个线程获取到一个对像锁之后,再一次获取这个对象锁,还是可以获取到。eg:A线程获取到对象锁O之后,进行代码执行,执行的过程中,A线程又一次去获取对象锁O,此时这个对象锁发现还是之前那个线程,那么A线程还是可以获取到锁的。这就是可重入。
    代码如下哦:
// 在hello方法中调用了aHi方法,可以执行成功。
// 调用的是同一个类中的synchronized方法,等同于,synchronized(this),对象锁是同一个,都是this。所以可重入。
public class C1_Synchronized2 {
    public static void main(String[] args) {
        C1_Synchronized2 c1_Synchronized2 = new C1_Synchronized2();
        c1_Synchronized2.hello();
    }

    public synchronized void hello(){
        System.out.println("hello");
        this.aHi();
    }
    public  synchronized void aHi(){
        System.out.println("ahi");
    }
}
  1. synchronized方法中可以调用另一个synchronized方法,synchronized 能调用非Synchronized 方法。
  2. 一个类中的所有synchronized方法,对象锁都是this,当前对象。
  3. 抛异常会释放锁,其他的线程可以进来,冲乱数据
    测试代码:
/**
 * synchronized块,当抛出异常时,会释放锁,同时线程停止,其他的线程可以进入
 *
 */
public class C1_Synchronized3 {

    public static void main(String[] args) {
        C1_Synchronized3 c3 = new C1_Synchronized3();
        Thread thread1 = new Thread(c3::hello,"thread1");
        Thread thread2 = new Thread(c3::hello,"thread2");
        thread1.start();
        thread2.start();

    }

    public synchronized void hello(){
        while (true) {
            System.out.println(Thread.currentThread().getName() + "正在运行");
            for (int i = 0; i < 10; i++) {
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
                if (i == 5) {
                    int a = 1 / 0;
                }
            }
        }
    }
}
  1. synchronized(Object) 锁的对象不能用String常量,不能用Integer Long等基础类型
  2. synchronized(o) 这个对象是不能发生改变的,和底层实现有关,判断的是o的对象头的两位。加final修饰符

4.2.5 sychronized 和 volatile

我们先看一个以下程序:

public class C1_Synchronized4 {

    boolean flag = true;
    public static void main(String[] args) {
        C1_Synchronized4 t = new C1_Synchronized4();
        new Thread(t::m, "thread1").start();
        //sleep 1s 后,将flag修改为false。
        try {
           TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.flag = false;
    }
    void m() {
        System.out.println("m start");
        //当flag为true,处与死循环状态
        //当flag为false,跳出死循环,往下执行。
        while (flag) {
        }
        System.out.println("m end!");
    }
}

程序分析:
按照理论上来说,当thread1线程启动后,thread执行m方法,处于while循环。
主线程 mian 睡眠1秒,然后将flag置为false。这样就会中断while循环,跳出死循环,继续执行。
但是实际结果则为并没有跳出循环,不能执行 m end!。

这是为什么呢?
这个问题的源头就在于可见性问题。

4.2.5.1cpu多级缓存结构

在这里插入图片描述
一个双核CPU架构可以如下图所示:
在这里插入图片描述

  1. 计算机实际上是分为多级缓存的,这样设计是因为利用缓存的速度快。
  2. 缓存分为高速缓存L1,L2,L3。当CPU1需要读取共享变量的值a时,首先会找缓存(即L1、L2、L3三级高速缓存),看看这个值是不是在L1,一级一级的找,到L2,L3找。
  3. 如果没有找到flag,只能去主内存读取共享变量的值,缓存得到共享变量的值之后,把数据交给寄存器,但是缓存留了个心眼,它把flag的值存了起来,这样下次别的线程再需要a的值时,就不用再去主内存问了。
  4. 至此,一次完整的数据访问流程走完了。L1和L2、L3都是高速缓存,从高速缓存和主内存读取数据的速度完全是两个概念。所以才会有主内存和缓存的设计。
4.2.5.2 写数据时刷新内存
  1. 针对上述模型,当CPU1读取完数据后,假如对数据进行了修改,那么它会将缓存 —> 主内存的顺序将修改后的数据刷新一遍,完成对数据的更新。此时flag=false。
  2. 既然能够将主存中的共享数据更新成功。相当于主线程已经将flag=true更新为flag=false了,那么为什么thread1线程没有停止呢。
  3. 原因是这样的。
    main线程cup1 读取数据flag=true,main线程cpu1的 的缓存中都有数据flag=true的副本
    thread1线程cpu2也执行读取操作,同样thread1线程cup2也有数据flag=true的副本
    main线程cup1修改数据flag=false,同时刷新的缓存以及共享主内存flag=false
    thread1线程cpu2再次读取flag,但是thread1在缓存中命中数据,此时flag=true
    问题到这里已经很明显了,thread1线程并不知道main线程改变了共享变量flag的值,因此造成了不可见问题。
4.2.5.3解决办法
  1. 早期的cpu中,通过在总线上直接加锁的形式来解决缓存不一致的问题。和synchronized差不多,在锁住总线期间,其他CPU无法访问内存,导致效率低下。很明显这样做是不可取的。直接加锁太粗暴了。
  2. 缓存一致性协议
    所以就出现了缓存一致性协议。缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等。
  3. 缓存一致性的原理
    最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。

核心思想是:
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。而这其中,监听和通知又基于总线嗅探机制来完成。

缓存行状态:
Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态

  1. 使用以上解决办法思想分析:
    嗅探机制其实就是一个监听器,回到我们刚才的流程,如果是加入MESI缓存一致性协议和总线嗅探机制之后
    main线程cup1 读取数据flag=true,main线程cpu1的 的缓存中都有数据flag=true的副本,该缓存行(可以理解为这个变量flag)状态为E
    thread1线程cpu2也执行读取操作,同样thread1线程cup2也有数据flag=true的副本,所以持有该数据的缓存行为S状态,cup1中的缓存行页变成S状态
    main线程cup1修改数据flag=false,同时刷新的缓存以及共享主内存flag=false,该缓存行变为M状态
    thread1线程cpu2再次读取flag,但是thread1在缓存中命中数据,此时flag=true,这个就会变成I状态。
    当执行while(flag)的时候,到缓存行中找,发现状态为I无效,所以就会重新到主存中读修改后的值,保证数据可见性。

  2. 而在一个变量上加volatile,就是告诉CPU:这个变量需要使用MESI和总线嗅探机制处理。从而也就保证了可见性。
    所以修改以上代码,在变量之前加volatile修饰符。

volatile boolean flag = true;
4.2.5.4 synchronized 可实现原子性和可见性

原子性,synchronized能保证线程独占,不会被其他的线程中断,通过锁机制来实现。
可见性,synchronized在释放锁之前能够讲更新值刷新到内存中,保证其他线程可读。

4.2.5.5 volatile只能实现可见性

通过缓存一致性协议MESI和总线嗅探机制实现可见性。

4.3 锁升级

锁对象的状态有四种:无锁,偏向锁,轻量级锁,重量级锁

  1. 无锁态,当没有线程争抢锁的时候,锁对象为无锁状态
  2. 偏向锁,当有一个线程A获取锁后,锁对象进入偏向锁状态。
    偏向锁状态,锁对象在对象头和栈帧中记录偏向的锁的threadID线程号。只有一个线程的时候多对象为偏向锁。
    当另一个线程B也要争抢锁,那么就需要看一下,A线程是否还执行中,还持有锁(偏向锁不会主动释放,只有线程停止,才会释放)。
    如果A线程停止了,释放了偏向锁,那么对象锁就变成无锁状态,此时B线程获取锁,锁又变为偏向锁,但此次记录的B线程的线程号。
    如果A线程还在执行中,B线程又来争抢锁,此时就进入轻量级锁(也叫自旋锁)状态。
  3. 轻量级锁(也叫自旋锁),俩个线程同时争抢锁,A线程抢到锁还在执行中,B线程又来争抢锁,就对象锁进入轻量级锁(也叫自旋锁)状态。轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。适用于适用于线程数少,加锁代码执行速度快的场景
  4. 重量级锁,虽然自旋锁有好处,但是当A线程一直不释放对象锁,不可能让B线程一直进行自旋,不停的占用cup资源,所以会有一个自旋上限,例如10次 20次等等,当自旋次数到达这个数值时,就升级为重量级锁,让B线程进入阻塞状态。
    适用于线程数多,加锁代码执行时间长的场景。

4.4 synchronized优化

  1. 锁的细化。
    锁的代码量越少越好。
  2. 锁的粗化。
    如果程序中有很多细小的锁,还不如来一个大锁。将这些细小的锁的内容包含在一个大方法中。

5. Volatile

volatile的作用:

  1. 保证线程的可见性。
    上述4.2.5章已经讲过了。
    volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。什么意思呢,就是volatile修饰一个引用类型
    时,只能保证引用本身的可见性,不能保证这个引用的引用的可见性,原因是对于属性a和b我们都是分别的读取,是由先后顺序的。分别读取就会导致在这个对象的属性不是同一时刻的属性值。
    eg: 线程A修改Data属性,同时线程A读取Data属性,但是读取属性的时候Data的属性还在变化,调用data.a 和调用data.b的执行时间是不同的,所以获取的值并不是同一时刻Data对象的值。
    所以我认为,volatile 并不是不能保证引用类型对象属性的可见性,只是获取值的时候是分别获取属性的,在获取其中一个属性的时候,另一个属性发生了变化,然后获取变化的属性的时候,所以获取的这两个属性不是同一时刻的对象的属性。这个理解是我个人的观点。

测试代码:

package com.tzw.juc.c1_synchronized;

import java.util.Arrays;

import static java.lang.Thread.sleep;


public class C1_Synchronized7 {

    private static class Data {
        int  a, b;

        public Data(int a, int b) {
            this.a = a;
            this.b = b;
        }

        public String get(){
            return a+"---"+b;
        }

    }

    static volatile Data data;
    public static void main(String[] args) {
        Thread writer = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                data = new Data(i, i);
            }
        });

        Thread reader = new Thread(()->{
            //volatile修饰data变量,当另一个线程修改data变量时,data变量可见,data不等于null,跳出循环
            while (data==null) {
            }
            //data.a 和 data.b获取属性是,不是同时获取的,在获取data.a的同时,另外一个线程在修改data
            //当获取data.b时,此时获取的就是另一个值,就是说获取的data 的a b两个属性的值不是同一时刻的值。
            //可可能就是所谓的不可见吧,但是我认为这并不是不可见,只是获取的时间不一致,只需要获取的时刻保持一致,就能解决这个问题。
            System.out.println("data 可见"+data.a+"----"+data.b);
            while(data.a==data.b){
            }
            String s = data.get();
            String[] split = s.split("---");
            System.out.println(split[0]+"---"+split[1]);
            //这两个值永远相同,不会结束
            while(split[0]==split[0]){

            }
        });

        reader.start();
        try {
            sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        writer.start();
    }
}

如何解决这个问题呢:根据以上的理解,解决这个问题就是让获取的时候,获取同一时刻的对象的值。
解决一: data.a 和 data.b 不是同时获取的,所以可以同时返回data.a 和 data.b的值。提供一个get方法。同时将a,b的值返回。
解决二:利用ActomicReference类。
测试代码:

public class C1_Synchronized8 {

    private static class Data {
        int  a, b;
        public Data(int a, int b) {
            this.a = a;
            this.b = b;
        }
    }

    private static AtomicReference<Data> data = new AtomicReference<>();
    public static void main(String[] args) {
        Thread writer = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                data.compareAndSet(null,new Data(i,i));
            }
        });

        Thread reader = new Thread(()->{
            //volatile修饰data变量,当另一个线程修改data变量时,data变量可见,data不等于null,跳出循环
            while (C1_Synchronized8.data.get()==null) {
            }
            //此时dataaa的两个值永远相同。
            Data dataaa = C1_Synchronized8.data.get();
            while(dataaa.a==dataaa.b){

            }
        });

        reader.start();
        try {
            sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        writer.start();
    }
}
  1. 禁止指令重排序
    利用的是加内存屏障。load read为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型处理器的重排序,在JMM中,内存屏障的插入策略如下:

在每个volatile写操作之前插入一个StoreStore屏障
在每个volatile写操作之后插入一个StoreLoad屏障
在每个volatile读操作之前插入一个LoadLoad屏障
在每个volatile读操作之后插入一个LoadStore屏障
StoreStore屏障可以保证在volatile写之前,前面所有的普通读写操作同步到主内存中
StoreLoad屏障可以保证防止前面的volatile写和后面有可能出现的volatile度/写进行 排序
LoadLoad屏障可以保证防止下面的普通读操作和上面的volatile读进行重排序
LoadStore屏障可以保存防止下面的普通写操作和上面的volatile读进行重排序

  1. volatile只能保证可见性和重排序,不能够保证原子性,volatile 不能代替synchronized。
    volatile不能保证线程独占。没有锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值