Java多线程(2)

1.jconsole.exe

在介绍多线程状态之前,我们先来认识一个Java JDK自带的工具"jconsole.exe"

它可以让我们很好地观察Java线程的状态.

首先我们需要找到自己安装jdk的目录

然后进入bin目录下.找到名为"jconsole.exe"的程序文件
Java多线程(2)_第1张图片

然后打开程序

这里我们使用一段代码来帮助观察

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(30000);//t1线程睡眠30秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1");
        t1.start();
        Thread.sleep(60000);//main线程睡眠1分钟
        System.out.println(t1.getState());
    }
}

打开工具后会是如下页面,我们运行程序后,就可以在本地进程中观察到我们运行的进程.

Java多线程(2)_第2张图片

双击这个进程

就会进入观察页面,会提醒一个是否以不安全状态连接,我们直接点击"不安全的连接"就好
Java多线程(2)_第3张图片

点击"不安全的连接"之后

就正式进入到了监视窗口,点击线程栏,就可以在左下角的线程栏中观察到我们这个程序的线程.

选择想查看的线程,就可以看到线程的状态以及一些信息

Java多线程(2)_第4张图片

2.Java中线程的状态

Java中线程的状态是Java内部的状态.与其他操作系统的状态会有所差异.

官方文档中有6种状态,也可以细分为7种状态

  • NEW: 安排了工作,还未开始行动

  • RUNNABLE: 可工作的.又可以分成正在工作中和即将开始工作

    可以细分为运行态和就绪态

  • BLOCKED: 这几个都表示排队等着其他事情

  • WAITING: 这几个都表示排队等着其他事情

  • TIMED_WAITING: 这几个都表示排队等着其他事情

  • TERMINATED: 工作完成了


先认识一个方法:getState(),获取线程的状态(谁调用获取谁的)

//此处就是获取当前运行代码的线程的状态.
Thread.currentThread().getState();

NEW:安排了工作还没开始行动

把Thread对象创建好了,但是还没有调用start运行

public class Test {
    public static void main(String[] args) {
        //NEW
        Thread t = new Thread(()->{});
        System.out.println(t.getState());
    }
}

RUNNABLE:可工作的,又可以分为正在工作和即将开始工作

就绪状态:处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上(或者正在运行中)

如果代码没有进行sleep,也没有其他导致阻塞的操作,代码大概率是在这个状态

public class Test {
    public static void main(String[] args){
        //RUNNABLE
        System.out.println(Thread.currentThread().getState());
    }
}

TERMINATED:工作完成了

操作系统中的线程已经执行完毕,销毁了.但是Thread对象还在,获取到的状态.

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //TERMINATED,
        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        Thread.sleep(3000);
        System.out.println(t.getState());
    }
}

BLOCKED:表示排队等着其他事情(和下一个一起观察)

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态.(等待获取锁)

TIMED_WAITING

表示线程在等待等待其他线程发来通知.(下列代码中t1等待sleep唤醒)

public class Test {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override        
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

运行起来代码以后,使用 jconsole工具就可以观察到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

WAITING同样表示等待其他线程发来通知.

与TIMED_WAITING的区别在于,TIMED_WAITING线程在等待唤醒,但设置了时限;
而WAITING 线程在无限等待唤醒

public class Test {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override        
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            // [与上述代码相同,只是修改了这里]
                            // Thread.sleep(1000);                    
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

此时使用 jconsole 工具可以看到 t1 的状态是 WAITING

3.了解方法:yield():大公无私,让出 CPU

谁调用Thread.yield()方法谁就让出CPU,不会改变线程的状态,会重新进入就绪队列排队

public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("张三");
                    Thread.yield();
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("李四");
                }
            }
        }, "t2");
        t2.start();
    }
}

通过上述代码,我们可以发现,张三的数量远远少于李四,说明"张三"总在给"李四"让行.这就是yield的作用

4.多线程带来的的风险-线程安全问题

我们先来观察下面代码

class Counter{
    public int count = 0;
    
    public void increase(){
        count++;
    }
}
public class Test {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();//main线程等待t1和t2线程运行结束,输出最终结果.
        System.out.println(counter.count);
    }
}

多次运行后,我们会发现结果总会在50_000-100_000之间.

为什么会有这样的现象发生呢?为什么不是每次都是100_000呢?


类似于此类多线程安全问题的具体的原因有以下几点

  1. 线程是抢占式,进程间的调度充满随机性.
  2. 多个线程对同一个变量进行修改操作
  3. 针对变量的操作不是原子的
  4. 内存可见性(属于编译器优化)
  5. 指令重排序(属于编译器优化)

我们图示一个可能发生的状态来描述.

Java多线程(2)_第5张图片

我们发现,此时虽然执行了两次++操作,但是最后的结果却只是++了一次的结果.

类似于这种状态还有多种.

Java多线程(2)_第6张图片

认真想想,只要不是这种每次的操作是原子性的执行,就都会产生上述不安全的情况.

什么是原子性.

在我们这里,原子性就是保证一段代码是不可分割执行的.

synchronized关键字

对于上述安全问题的解决,我们就需要了解这个关键字.

synchronized的底层是使用操作系统的mutex lock实现的


synchronized 的特性

1.互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

2.刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性(简单理解就是保证每次得到的数据都是最新的).

3.可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题.(也就是在自己加锁后,在没有释放锁的状态下,自己是可以再次进入这段加锁的代码的.)

在下面的代码中,

increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)

对于synchronized来说,这个代码是完全没问题的. 因为 synchronized 是可重入锁

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
    synchronized void increase2() {
        increase();
    }
}

实现方式: 在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.

解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)


synchronized关键字 本质就是对代码加锁

加锁之后,可以保证指令的原子性,同时保证内存可见性

使得多个线程不可以同时对同一个变量进行修改操作,也就保证了安全.

synchronized 有三种使用方式

使用synchronized的时候,本质上是在针对某个"对象"进行加锁

1. 直接修饰普通方法

当修饰普通方法的时候,锁对象就是this:加锁操作就是在设置this的对象头的标志位

2. 修饰一个代码块

修饰一个代码块的时候,就需要显示指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)

3. 修饰一个静态方法

修饰一个静态方法的时候,就是针对当前类的类对象(xxx.class)加锁


当一个线程加锁成功的时候,其他线程如果尝试加锁,就会触发阻塞等待.等待到锁释放时对线程的唤醒.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态

那我们该怎么解决上述问题呢

当然是使用synchronized加锁啦(对获取变量并修改的操作进行加锁,把操作打包成原子的)

正确的加锁之后,多线程代码就变成安全的了.

class Counter{
    public int count = 0;
    synchronized public void sIncrease(){
        //加锁
        count++;
    }
}
public class Test2 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.sIncrease();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.sIncrease();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

我们对要操作变量的方法,加上synchronized关键字,就是对此方法加了锁,此时我们再去运行这段代码.无论运行几次,最终结果都会达到我们的预期.

5.内存可见性

我们看一个编译器优化的问题.

public class Test {
//    private static volatile int isQuit = 0;
    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(isQuit == 0){
                //不进行操作,使得程序不停地访问isQuit
            }
            System.out.println("循环结束 t线程退出");
        });
        t.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个isQuit的值");
        if(sc.hasNextInt()){
            isQuit = sc.nextInt();
        }
        System.out.println("main 执行完毕");
    }
}

上述代码,当输入1之后t线程可能是不会退出的.这就属于编译器优化了.

因为在不停高速地访问isQuit变量,编译器直接 (进行优化) 将isQuit变量的值一次读取并保存副本.后续读取副本的值.不会访问实际内存,大量节省了程序读的时间.(也因此产生了bug)

volatile关键字

volatile关键字就会保证了内存可见性,保证程序每次读取的值,都是内存中真实的值.

volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

我们给isQuit变量加上volatile关键字,在运行程序,输入1之后马上就会退出循环,bug就消失啦.

你可能感兴趣的:(鱼小飞代码之旅,java,jvm,面试)