JavaEE初阶 - 多线程基础篇 (线程状态和线程安全)

线程状态

  1.Java中线程的6种状态
  2. 线程状态转换图

线程安全

  1. 线程安全和线程不安全
  2. 一个线程不安全的典型案例
  3. 产生线程不安全的原因
  4. 对内存可见性影响线程安全的分析
  5. synchronized关键字

线程状态

1.Java中线程的6种状态

  1. NEW: 已经创建好了Thread对象, 但还没有调用start方法
    通过线程对象.getState()来查看当前线程的状态:
public class Demo1 {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
        });
        //通过thread.getState()方法来查看当前线程的状态
        System.out.println(thread.getState());
        thread.start();
    }
}
//结果:
NEW
  1. RUNNABLE: 也就是就绪状态. 处于这个状态的线程位于就绪队列中, 随时可以被调度到CPU上
    例如:
public class Demo {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
            //这里要让这个线程处于就绪态, 这个线程不能执行任何指令
        });
        thread.start();
        System.out.println(thread.getState());
    }
}
//结果:RUNNABLE

注意体会这两段代码的不同.

  1. BLOCKED: 当前线程在等待锁, 导致阻塞
  2. WAITING: 当前线程在等待唤醒, 导致阻塞
  3. TIMED_WAITING: 代码中调用了sleep或join等由于时间引起的阻塞, 意思是当前的线程在一定时间内是阻塞的状态, 阻塞时间过后状态解除.
    例如:
public class Demo {
    public static void main(String[] args) throws InterruptedException{
        Thread thread = new Thread(() ->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
}
//结果:
TIMED_WAITING
  1. TERMINATED: 工作完成了(操作系统中的线程已经执行完毕并销毁, 但Thread对象还在).

2. 线程状态转换图

JavaEE初阶 - 多线程基础篇 (线程状态和线程安全)_第1张图片

线程安全

1. 线程安全和线程不安全

  操作系统在进行线程调度的时候, 调度的顺序是随机的(抢占式执行), 正因为这样的随机性, 就可能导致程序的执行出现问题, 如果多线程下程序运行的结果是符合我们预期, 也就是和在单线程中的运行结果是相同的, 我们就说这个程序是线程安全的, 否则, 这个程序就是线程不安全的.

2. 一个线程不安全的典型案例

我们操作两个线程, 分别对同一个数字自增5_0000次,查看运行结果:

class Counter{
    public int count;
    public void increase(){
        count++;
    }
}
public class Demo {
    public static void main(String[] args) throws InterruptedException{
        Counter counter = new Counter();
        Thread thread1 = new Thread(() ->{
            for(int i=0;i<5_0000;++i){
                counter.increase();
            }
        });
        Thread thread2 = new Thread(() ->{
            for(int i=0;i<5_0000;++i){
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(counter.count);
    }
}
//多次运行查看运行结果:
65130
57228
54039
74729

  在上述代码中, 我们一共对count执行了10_0000次, 但结果却小了很多, 并且每次执行结果不一样, 这个程序如果在单线程中执行, 结果一定是等于10_0000的, 这时, 多线程执行的结果与单线程不相等, 这个程序就是线程不安全的.

Q:如何解决这个问题?

正确的做法是:加锁. 在thread1对count进行操作前, 为thread1加锁(lock), 此时lock就会一直处于阻塞状态, 操作完成后再解锁(unlock), 这样就能将一个乱序的并发转变为串行操作, 此时便能得到正确的结果.

注意:线程的并发性越高, 速度越快, 但同时可能就会出现一些问题, 加了锁之后, 线程间的并发性降低, 速度降低, 此时得到的数据也会更准确.

那么, 加锁之后的并发执行不就和串行一样了吗?这样执行有什么意义呢?

在实际开发中, 一个线程可能会执行多个任务, 例如, 线程一可能要执行步骤1, 2, 3, 4, 而执行过程中只有步骤4需要加锁,那么对于步骤1, 2, 3, 依然可以并发执行.

Java中加锁的方式有很多种, 其中最常用的就是synchronized这个关键字.

class Counter{
    public int count;
    //通过synchronized关键字对自增操作加锁
    synchronized public void increase(){
        count++;
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException{
        Counter counter = new Counter();
        Thread thread1 = new Thread(() ->{
            for(int i=0;i<5_0000;++i){
                counter.increase();
            }
        });
        Thread thread2 = new Thread(() ->{
            for(int i=0;i<5_0000;++i){
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(counter.count);
    }
}
//结果:
100000

  给方法加上synchronized关键字后, 此时运行此方法, 就会自动加锁. 结束这个方法, 就会自动解锁. 当一个线程对这个方法进行加锁之后, 另一个线程尝试加锁时就会触发阻塞等待(此时对应的线程就处在blocked状态), 阻塞状态会持续到占用锁的线程将锁释放为止.

3. 产生线程不安全的原因

  1. 线程是抢占式执行, 线程间的调度充满随机性. //这种操作是线程不安全的根本原因, 但无法避免
  2. 多个线程对同一个变量进行修改操作 //部分情况下可以通过修改代码来避免
  3. 针对变量的操作不具有原子性(这些操作是可以拆分的) //加锁操作就是将多条指令打包, 使其具有原子性
  4. 内存可见性影响到线程安全 //编译器优化的影响
  5. 指令重排序 //同样是编译器优化的影响, 编译器会智能地取调整代码的执行顺序, 从而提高代码的执行效率

4. 对内存可见性影响线程安全的分析

例如, 线程A在持续地读取某一个数, 此时, 线程B将这个数字进行了修改, 但线程A读取到的依然是未被修改的值.

public class Demo {
    static int flg = 0;
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
            while (flg==0){

            }
            System.out.println("循环退出, thread线程执行完毕");
        });
        thread.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入flg的值:");
        flg = scanner.nextInt();
        System.out.println("main线程执行结束");
    }
}
//结果:
请输入flg的值:
5
main线程执行结束
//程序并没有退出, 因为thread线程并没有执行完毕

  在上述代码中, thread线程开始执行后开始持续读取flg的值, 此时我们在main线程中修改了flg的值, 但thread线程并没有因为flg的修改而结束, 这是由于thread线程持续从内存中读取到了相同的数据, 但从内存中读取数据效率很低, Java编译器就对这个操作进行了优化, 使得thread线程直接从寄存器中读取数据, 但main线程是在内存中修改了flg的值, 此时thread线程无法感知到, 自然就导致了线程无法正常结束.

  读取内存中的数据是一个非常低效的操作(相比于读取寄存器中的数据来说). 在某个线程持续读取内存中的数据时, Java编译器就会对这个操作产生优化, 让线程不从内存中, 而是直接从寄存器中读取数据, 加快了执行效率(编译器会在编译过程中对代码进行调整, 在不改变代码逻辑的前提下, 加快程序的执行效率), 这种优化一般情况下不会出现问题, 但在多线程下, 这里的优化可能会造成误判, 这也就是内存可见性造成的影响.

解决方案:

  1. 使用synchronized关键字. synchronized不光能保证指令的原子性, 同时能保证内存可见性, 被synchronized包裹起来的代码, Java编译器就不会对其进行优化, 也就避免了上述问题的发生.
  2. 使用volatile关键字. volatile只会保证内存可见性, 与原子性无关.(volatile只能处理一个线程读, 另一个线程写的情况, synchronized各种情况都能处理)

对于上述问题, 使用volatile关键字解决:

public class Demo {
    //使用volatile关键字修饰flg
    static volatile int flg = 0;
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
            while (flg==0){

            }
            System.out.println("循环退出, 执行完毕");
        });
        thread.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入flg的值:");
        flg = scanner.nextInt();
        System.out.println("main线程执行结束");
    }
}
//结果:
请输入flg的值:
1
main线程执行结束
循环退出, 执行完毕
//程序成功退出

5. synchronized关键字

synchronized关键字互斥性:

  synchronized关键字会达到互斥的效果, 某个对象中的某个方法使用了synchronized关键字, 当一个线程正在执行这个对象的这个加锁的方法时, 如果其他线程也执行到了这个方法, 就会阻塞等待(注意:必须是多个线程同时执行到同一个对象的同一个加锁的方法).

synchronized关键字的用法

  1. 直接修饰普通方法
public class Demo{
	public synchronized void func(){
        System.out.println("一个加锁的普通方法");
    }
}
  1. 修饰静态方法
public class Demo{
	public static synchronized void func1(){
        System.out.println("一个加锁的静态方法");
    }
}
  1. 修饰代码块
public class Demo{
	{
		synchronized(this){
			System.out.println("一个加锁的代码块");
		}
	}
}

注意:修饰代码块时, 我们需要显式指定需要加锁的对象(Java中任何一个对象都可以作为锁对象)


The end

你可能感兴趣的:(JavaEE初阶,学习,java-ee,java)