Java多线程整理2-线程状态-线程安全问题-synchronized关键字以及volatile关键字

多线程整理2

1. 线程的状态

1.1 观察线程所有状态

线程的状态就是用来描述当前线程属于一个什么地步,进而JVM可以根据状态来决定该线程下一步的命运。
线程可以有如下6种状态:

  • New(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)
    观察线程的所有状态:
public class ThreadState {
	public static void main(String[] args) {
		for (Thread.State state : Thread.State.values()) {   
			//线程的状态是一个枚举类型 Thread.State
			System.out.println(state);
		}
	}
}

1.2 线程状态和状态转移的意义

在这里插入图片描述

  • New:线程刚刚被创建是的状态,如

Thread t = new Thread();
t现在就是New状态

  • Runnable:一旦调用start方法,线程就处于runnable状态。这个状态实际上还可以细分为Running和Ready状态,原因是系统能分配的CPU资源是有限的,不可能所有线程都能同时分配到CPU。即Ready表明该线程拥有抢CPU的资格,而Running则表明该线程已经在CPU上了。不过JAVA的规范说明并没有区分这两个状态,而是统称为Runnable状态。当线程的数目多于处理器数目时,调度器会采用时间片机制以保证多线程并行运行。所以,某一时刻一个Runnable线程可能正在运行也可能没在运行,这也是为什么称为Runnable状态。
  • Blocked,Waiting,Timed waiting:处于这种状态时,它暂时不活动,即不再拥有抢CPU的资格,直到线程调度器重新激活它。细节取决于它时怎样达到这种状态的。
  • Terminated:线程因如下两个原因之一而被终止:
    1.因为run方法正常退出而自然死亡。
    2.因为一个没有捕获的异常终止了run方法而意外死亡。

2. 线程安全

2.1 观察线程不安全

public class Test {
    
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                n++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                n--;
            }
        }
    }

    private static long n = 0;

    public static void main(String[] args) throws InterruptedException {
        Sub A = new Sub();
        A.start();
        Add B = new Add();
        B.start();

        add.join();
        sub.join();

        System.out.println(n);
    }
}

结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们期望得到0的结果,但实际结果却不固定。

2.2 线程安全的概念

我们可以简单认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

2.3 线程不安全的原因

2.3.1 Java内存模型

在此之前需要先引入Java内存模型,为屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种不同平台下都能达到一致的内存访问效果,Java虚拟机规范定义了一种Java内存模型(Java Memory Modle, JMM)。因此Java内存模型可以和物理硬件进行类比。
在这里插入图片描述

Java内存模型规定了所有变量都储存在主内存中(类比物理硬件的主内存)。每条线程还有自己的工作内存(类比高速缓存),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写在主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如上图所示。

2.3.2 内存间的交互操作

Java内存模型中定义了以下8种操作来完成一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存等,虚拟机必须保证每一种操作都是原子的、不可再分的(double和long类型另说)。

  • lock:表示把一个变量标识为一个线程独占状态
  • unlock:表示释放lock的锁定状态,解锁
  • read:把主内存的变量读取到工作内存中,方便写入
  • load:把read指定读取过来的变量写入到工作内存的副本中
  • use:把工作内存中的变量传入给执行引擎使用,比如工作变量i,需要被执行引擎执行i+1操作,必须要先让工作内存将i的值传给执行引擎,执行引擎才能使用执行i+1操作
  • assign:表示执行引擎对工作内存中变量进行赋值的时候,将值传入工作内存中,本来工作内存中i变量为1,执行引擎执行了i=3操作后,会将结果3赋值给工作变量中的i
  • store:把工作内存中的变量传给主内存,但并未写入主内存
  • write:把store传过来的变量写入到主内存中对应的变量

Java内存模型只要求read和load、store与write的操作必须按顺序执行,而没有保证是连续执行。也就是说这两种操作之间是可以插入其他命令的。
除此之外,8种基本操作还需满足如下规则:

  • 不允许从主内存中读取了数据后不载入数据,也不允许把工作内存中的数据传入主内存中后,主内存不写入

  • 不允许工作内存中的变量数据改变了后不同步回主内存

  • 不允许一个工作内存中的变量没有进行任何修改,又同步回主内存

  • 一个新变量必须在主内存中产生

  • 一个变量在任何时候都只能被一个线程锁定,不能被多个线程锁定,但是一个线程可以锁定一个变量多次,锁定几次就必须解锁几次

  • 没有锁定的变量不允许调用unlock操作,就是没有被锁就不能去解锁,也不能去解锁其他线程锁定的变量

  • 解锁变量前必须先进行变量的同步,既先同步回主内存中去

2.3.3 原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
比如上面观察线程不安全的代码中的 n++ 操作,了解了内存间的交互操作我们知道 n++ 是分为read、load、use、assign、store、write几部进行的。A线程在执行 n-- 时,在read之后write之前,B线程正好执行了read操作,这就导致了最终结果可能不正确。

2.3.4 可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

public class Test {

    private static boolean state = true;

    private static class A extends Thread {
        @Override
        public void run() {
            int n = 0;
            while (state) {
                n++;

            }
            System.out.println("quit");
        }
    }

    public static void main(String[] args) {
        Thread t = new A();
        t.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("please input something");
        sc.nextLine();
        state = false;

    }
}

运行结果如下:
在这里插入图片描述
从这段代码即运行结果我们可以看到当主线程修改了state的值之后,其他线程并不能即使得知这个修改,也就是说此时主内存中的state值与这个线程工作内存中的state的值不一样,这就是可见性问题。

2.3.5 有序性

程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序。看似理所当然的事情,其实并不是这样,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。但是在多线程环境下,有些代码的顺序改变,有可能引发逻辑上的不正确。

3.线程安全机制解决

3.1 synchronized关键字与监视器锁monitor lock

在之前说到的原子性问题中,是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
JVM在设计的时候,每个对象中,都实现了一把锁(lock)–monitor lock;锁可以保证被加锁的代码在执行时不被其他线程打扰。
synchronized可以起到加锁的作用

  • 加的是哪个锁:某个对象内部的monitor lock
  • 怎么确定是哪个对象?
    1. synchronized (引用) {} //引用指向的对象
    2. synchronized void method() {} //this指向的对象
    3. synchronized static void staticMethod() {} //方法属于AClass,AClass.class这个引用指向的对象的类在内存中的对象
  • 多个线程之间,如何使得某段代码具有互斥的特性,即我在执行的时候,你不能执行:
    1. 操作必须先抢锁
    2. 抢锁必须抢同一把锁

作用:

  1. 原子性 – 可以保证一组代码的原子性。 – 线程互斥的情况下
  2. 内存可见性 – 保证一定。 加锁成功时,JVM要求该线程工作内存中的数据全部失效。从主内存读取数据。
    释放锁时,JVM把所有工作内存的数据save到主内存上。

重点要理解,synchronized 锁的是什么

利用synchronized解决之前的原子性问题:

public class Test {


    static class Add extends Thread {
        private Object lock ;
        Add(Object o) {
            this.lock = o;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {
                    n++;
                }
            }
        }
    }

    static class Sub extends Thread {
        private Object lock ;
        Sub(Object o) {
            this.lock = o;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {
                    n--;
                }
            }
        }
    }

    private static long n = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Sub sub = new Sub(lock);
        sub.start();
        Add add = new Add(lock);    //传入同一个对象保证抢的是同一把锁
        add.start();

        add.join();
        sub.join();

        System.out.println(n);
    }
    
}

结果如下:
在这里插入图片描述

3.2 volatile 关键字

  1. 重点:保证内存可见性
    volatile修饰的变量,保证读或写都是在主内存上
  2. 限制代码重排序
    这个需要注意在对象的初始化上,volatile修饰保证一定是
    1. new(创建)
    2. 构造方法(初始化)
    3. 赋值引用

这个可以保证新创建的对象在初始化未完成之前不被使用。

利用volatile解决之前的可见性问题:

public class Test {

    private volatile static boolean state = true;

    private static class A extends Thread {
        @Override
        public void run() {
            int n = 0;
            while (state) {
                n++;
            }
            System.out.println("quit");
        }
    }

    public static void main(String[] args) {
        Thread t = new A();
        t.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("please input something");
        sc.nextLine();
        state = false;

    }
}

结果如下:
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值