进程和线程

进程,绝大部分使用过电脑的人都知道这东西。进程是一个正在运行的应用程序,Windows 上打开任务管理器就可以看到许许多多的进程,有什么系统进程和用户进程。

image-20210806100924445

线程,线程似乎绝大部分使用的过电脑的人都不是很熟悉,也许只在“性能”模块中的“CPU”中看到线程数。线程是进程的一个执行单元,这怎么解释呢?比如著名的 x 狗浏览器(非恰饭),有一个线程是用户加载浏览器的界面的,有一个线程是用于加载页面,有一个线程是用于下载图片的。每一个线程都在做着自己该做的工作,各司其职。

进程,好比一家公司。而线程,就好比公司里的员工,为公司创造价值。

线程和进程的关系

前面提到过,线程是进程的一个执行单元。一个进程中必须要有一个进程,可以有多个进程。

每一个进程都有一个独立的内存空间,每一个线程都可以使用进程所开辟的内存空间中的内容。

这就可以比喻成,你可以使用你所在的公司里的 wifi 来上网,使用公司里的空调来享受着带来的凉爽。但是不能使用其它进程中的内存空间。

比如你是拼夕夕的员工,你想直接去京东蹭 wifi 吗,这能行吗?这显然是不可以的。线程也是如此。

java 中的线程

在 java 中,我们平时写的控制台程序,运行时认为只有一个线程,也就是 main 方法所执行的线程。但是并不是,还有一个垃圾回收器的线程,总共有两个线程。垃圾回收器的目的就是将无用的资源释放掉,为 java 虚拟机腾出更多的内存空间,由于这里主要是讨论多线程,关于垃圾回收器,这里不过多赘述。

那,我们可以自己创建线程吗?

这不是 FIFA 嘛,写这篇日志的目的就是让我自己能够更加的了解 java 的线程,以及线程中的一些问题。

提个简单的问题吧

我最近在学习的时候,看到一个这样的代码,他问,程序在执行时共开启了多少个线程?

public class Question1 {
    public static void main(String[] a) {
        System.out.println("main执行--------");
        method1();
        System.out.println("main结束--------");
    }
    
    public static void method1() {
        System.out.println("method1执行--------");
        method2();
        System.out.println("method1结束--------");
    }
    
    public static void method2() {
        System.out.println("method2执行--------");
        method3();
        System.out.println("method2结束--------");
    }
    
    public static void method3() {
        System.out.println("method3执行--------");
    }
}

请不要立即展开,先思考 30 秒钟。

怎么样,肯定有人认为这是 4 个线程吧。其实,线程只有 1 个!

因为这很简单,只是方法之间的调用而已呀。

创建一个线程

线程的创建非常简单的,总共有三种方法。目前我也是刚刚学,所以目前只知道两种创建线程的方式,一个是通过继承 Thread 类来创建线程,一个是通过自己实现 Runnable 接口来创建线程。

然后再调用 start 方法去创建线程,这样,一个线程就这样成功的开起了。

继承 Thread 类

继承 Thread 类的相关代码如下所示。

public class MyThreadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程------" + i);
        }
    }
}

/**
 * 使用继承的方式
 */
class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程------" + i);
        }
    }
}

实现 Runnable 接口

实现 Runnable 接口创建线程的代码如下所示。这里使用到的方式是使用匿名内部类的方式,当然,你可以创建一个类来实现 Runnable 接口的方式,实例化你创建的类,传入到 Thread 类中的构造方法即可。

public class MyRunnableTest {

    public static void main(String[] args) {
        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("子线程------" + i);
                }
            }
        });
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程------" + i);
        }
    }
}

运行结果

由于运行结果实在是太长了,我将运行的结果进行折叠了。后续的运行结果也是如此

点击查看运行结果
主线程------0
子线程------0
子线程------1
子线程------2
子线程------3
子线程------4
子线程------5
子线程------6
子线程------7
子线程------8
子线程------9
子线程------10
子线程------11
子线程------12
子线程------13
子线程------14
子线程------15
子线程------16
子线程------17
子线程------18
子线程------19
子线程------20
子线程------21
子线程------22
子线程------23
子线程------24
子线程------25
子线程------26
子线程------27
子线程------28
子线程------29
子线程------30
子线程------31
子线程------32
子线程------33
子线程------34
子线程------35
子线程------36
子线程------37
子线程------38
子线程------39
子线程------40
子线程------41
子线程------42
子线程------43
子线程------44
子线程------45
子线程------46
子线程------47
子线程------48
子线程------49
子线程------50
子线程------51
子线程------52
子线程------53
子线程------54
子线程------55
子线程------56
子线程------57
子线程------58
子线程------59
子线程------60
子线程------61
子线程------62
子线程------63
子线程------64
子线程------65
子线程------66
子线程------67
子线程------68
子线程------69
子线程------70
子线程------71
子线程------72
子线程------73
子线程------74
子线程------75
子线程------76
子线程------77
子线程------78
子线程------79
子线程------80
子线程------81
主线程------1
主线程------2
主线程------3
子线程------82
子线程------83
子线程------84
子线程------85
子线程------86
子线程------87
子线程------88
子线程------89
子线程------90
子线程------91
子线程------92
子线程------93
子线程------94
子线程------95
子线程------96
子线程------97
子线程------98
子线程------99
主线程------4
主线程------5
主线程------6
主线程------7
主线程------8
主线程------9
主线程------10
主线程------11
主线程------12
主线程------13
主线程------14
主线程------15
主线程------16
主线程------17
主线程------18
主线程------19
主线程------20
主线程------21
主线程------22
主线程------23
主线程------24
主线程------25
主线程------26
主线程------27
主线程------28
主线程------29
主线程------30
主线程------31
主线程------32
主线程------33
主线程------34
主线程------35
主线程------36
主线程------37
主线程------38
主线程------39
主线程------40
主线程------41
主线程------42
主线程------43
主线程------44
主线程------45
主线程------46
主线程------47
主线程------48
主线程------49
主线程------50
主线程------51
主线程------52
主线程------53
主线程------54
主线程------55
主线程------56
主线程------57
主线程------58
主线程------59
主线程------60
主线程------61
主线程------62
主线程------63
主线程------64
主线程------65
主线程------66
主线程------67
主线程------68
主线程------69
主线程------70
主线程------71
主线程------72
主线程------73
主线程------74
主线程------75
主线程------76
主线程------77
主线程------78
主线程------79
主线程------80
主线程------81
主线程------82
主线程------83
主线程------84
主线程------85
主线程------86
主线程------87
主线程------88
主线程------89
主线程------90
主线程------91
主线程------92
主线程------93
主线程------94
主线程------95
主线程------96
主线程------97
主线程------98
主线程------99

建议创建线程的方式

强烈建议使用实现 Runnable 接口的方式,因为在 java 中,继承只能是单一的继承,不能多继承,如果使用继承 Thread 的方式,就会出现这样的问题。而在 java 中,可以这样子写。

class Example1 extends AbstractModule implements Runnable {
    //......
}

质疑

  1. 为什么我运行的结果和视频上运行的结果是完全不一样的呀?
  2. 为什么我运行的时候,总是主线程现执行而不是子线程先执行?
  3. 主线程一定是最先运行的吗,子线程就没有先输出的权利吗?
  4. 你看,我的运行结果完了之后,它就是主线程先执行完再执行子线程,这我觉得和单线程没什么区别呀?
  5. 调用 start 方法和调用的 run 的方法好像没什么区别吧?
  6. ……

以上是我在多线程学习时,我目前所遇到的疑问,可能还有其它的问题。显然,出现 1~4 点这样的疑问的原因绝大多是不了解的线程的生命周期所造成的。当然,第 5 点是自身 java 基础的问题了。

这也是我大专时期刚学习 java 的时候学习多线程部分所碰到的问题。

解答

先从最简单的第 5 点开始说起吧。

调用 run 方法和调用 start 方法的都是一样的,都是入栈和弹栈,仅仅只是调用了类中的方法而已。但是仔细思考,run 方法里面的内容是什么?比如上面那两个例子,run 方法是需要自己实现的,实现的里面是一个 for 循环。而 start 方法里面是啥,我把内容放出来如下。

public synchronized void start() {
    /**
      * This method is not invoked for the main method thread or "system"
      * group threads created/set up by the VM. Any new functionality added
      * to this method in the future may have to also be added to the VM.
      *
      * A zero status value corresponds to state "NEW".
      */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

先不看这里面复杂的内容,但是没有看到 start 方法里有在调用 run 方法。这就意味着,start 方法和 run 方法是两个不同的东西。创建出来的线程,这个 run 方法其实就相当于主线程中的 main 方法一样的。

也就是说 start 方法就是创建线程用的。

以上的四个问题,就单独用一个内容来说。

线程的生命周期

线程主要有 5 种状态,分别是 “创建”、“就绪”、“运行”、“阻塞” 和 “死亡” 这 5 种状态。可以用这张图来描述一下。

什么是时间片?如果你学过痛苦的操作系统,你就知道。

时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该线程允许运行的时间。

image-20210806154504981

在 java 中,通过创建出来的 Thread 对象,要想真正的创建出线程,则必须调用其 start 方法。调用它的 start 方法,此时线程创建完成。

创建的线程能够直接运行吗?当然不能,要先进入就绪状态,等待 CPU 的调度才能够执行。假如这个程序中共有 2 个线程,当第一个程序的时间片到了,他就需要进入就绪状态,等待 CPU 的调度。但是第二个线程一定会运行状态么?显然这是不一定的,有可能线程 1 会继续执行,也有可能线程 2 会继续执行。这就有了上面的示例所输出的结果,即主线程执行到 0 ,子线程就突然从 0 一次性执行到 99,也就有了每次输出都是不同的结果的原因了。

由于 CPU 的切换的频率非常之快。这就看起来像是,宏观上是并行(同时运行),微观上是串行(一个一个运行)。

那阻塞状态什么时候会出现呐?比如,线程 2 中突然遇到了键盘输入的操作,需要等待用户输入完成才能执行的下一步操作,在输入完成之前,处于运行状态的线程 2 就进入了阻塞状态,此时线程 1 受到 CPU 的调度继续执行着。用户输入完成之后,线程 2 只好进入就绪的状态了,等待 CPU 的调度了。

那死亡状态又是怎么一回事?在 java 中,死亡状态就是线程中的 run 方法,或者主线程的 main 方法中的代码运行结束了。线程就处于死亡状态了。

但是,主线程运行结束了,子线程就提前挂了吗?这是绝对不可能的。

其实,主线程和子线程并不是“父子”关系,而是“兄弟”关系。

java 多线程的一些简单操作

比如,获取、修改线程的名字,获取当前线程的对象,让当前线程休眠(进入阻塞状态),唤醒正在休眠的线程,强行正在运行的线程,合理的关闭正在运行的线程。

获取、修改线程的名字

使用 setName 方法来修改线程的名字,使用 getName 方法来获取线程的名字

Thread t1 = new Thread(new Runnable(){
    public void run() {
        // do something......
    }
});
// 设置线程名字
t1.setName("小赫赫");
System.out.println(t1.getName());

获取当前线程的对象

获取当前线程对象的方式也很简单,使用 currentThread 方法来调用当前的线程对象。

Thread currentThread = Thread.currentThread();

如果是在主线程下,得到的是主线程的对象。如果是自己创建的线程,就获取自己创建线程的对象。

Thread t1 = new Thread(new Runnable(){
    public void run() {
        // 新线程对象
	    Thread mainThread = Thread.currentThread();
        // do something......
    }
});
// 主线程对象
Thread mainThread = Thread.currentThread();

线程休眠与唤醒

使用 sleep 方法来休眠一个线程。单位是毫秒。

Thread.sleep(1000);

唤醒一个线程,唤醒一个线程是需要调用线程对象的 interrupt 方法,通过 java 异常机制来唤醒线程

Thread t1 = new Thread(new Runnable(){
    public void run() {
        try {
            Thread.sleep(1000 * 60 * 60 * 24);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // do something......
    }
});
// 唤醒 t1 线程
t1.interrupt();

终止一个线程

使用 stop 方法来终止强行终止一个线程。

Thread t1 = new Thread(){
    public void run() {
        // do something......
    }
});
t1.stop();

这样做其实是非常不安全的,因为这样很容易造成数据的丢失,那我们如何正常的关闭一个线程呢?

当然是可以的。

这个目前只能够通过代码的方式来终止,而不是使用线程类中的方法。

一个例子

Thread t1 = new Thread(){
    // 线程停止的状态
    isEnd = false;
 
    public void run() {
        while(isEnd) {
            // do something......
        }
        // stop to do another......
        // 如果是数据库的连接,则可以做关闭数据库连接的操作
    }
});
// 停止线程
t1.isEnd = true;

关于线程休眠的疑问

这里有一段代码,我调用 t 线程的 sleep 方法。那么是 t 线程会不会休眠,还是主线程会不会休眠?

public class Question2 {
    public static void main(String[] args) {
        Thread t = new MyThread3();
        t.setName("t");
        t.start();
        //调用 t 线程的 sleep 方法
        t.sleep(1000 * 5);
    } catch (InterruptedException e) {
    	e.printStackTrace();
    }
    system.out.println("he11o world!");
}

class MyThread3 extends Thread {
    public void run() {
		for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread() . getName() + "--->" + i);
    	}
    }
}

请不要立即展开,先思考 30 秒

正确答案是,主线程休眠!

怎么样,有和我一样被误认为是 t 线程休眠的吗,哈哈哈哈哈哈哈哈。

因为 sleep 方法它是一个静态的方法,静态方法和静态成员变量都是随着类的加载而加载呀。所以 t.sleep()Thread.sleep() 是一样的效果,只要写在哪个线程里面,就是在哪个线程中休眠。

java 多线程的一些调度

线程优先级

在 java 的多线程中,可以给线程设置优先级。设置线程的优先级需要获取当前的线程对象,才能够设置线程的优先级。首先,在 Thread 类中,共有三个常量。它们分别是最低优先级,正常优先级和最高优先级。

/**
  * The minimum priority that a thread can have.
  */
public final static int MIN_PRIORITY = 1;

/**
  * The default priority that is assigned to a thread.
  */
public final static int NORM_PRIORITY = 5;

/**
  * The maximum priority that a thread can have.
  */
public final static int MAX_PRIORITY = 10;

当然,线程的优先级也可以通过数字(1-10)的方式来设置。

以下是设置优先级的方法。

Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(8);
Thread.currentThread().setPriority(3);

设置优先级的目的在于让该线程处于运行态的时间会更久一些。

线程让位

就是当前的线程的运行时间片,让给其它线程。在 java 中,使用静态 yield 方法来将线程进行让位

Thread.yield();

以下是一个例子,每运行到 10 的时候,就让位一次。

public class MyRunnableTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                for (int i = 1; i <= 100; i++) {
                    System.out.println("线程" + thread.getName() + "------" + i);
                }
            }
        });
        t1.setName("t1");
        t1.setPriority(7);
        t1.start();
        for (int i = 1; i <= 100; i++) {
            if(i % 10 == 0) {
                Thread.yield(); // 每10次让给 t1 线程
            }
            System.out.println("线程" + Thread.currentThread().getName() + "------" + i);
        }
    }

}
运行结果如下,请展开
线程main------1
线程main------2
线程main------3
线程main------4
线程main------5
线程main------6
线程main------7
线程main------8
线程main------9
线程t1------1
线程t1------2
线程t1------3
线程t1------4
线程t1------5
线程t1------6
线程t1------7
线程t1------8
线程t1------9
线程t1------10
线程t1------11
线程t1------12
线程t1------13
线程t1------14
线程t1------15
线程t1------16
线程t1------17
线程t1------18
线程t1------19
线程t1------20
线程t1------21
线程t1------22
线程t1------23
线程t1------24
线程t1------25
线程t1------26
线程t1------27
线程t1------28
线程t1------29
线程t1------30
线程t1------31
线程t1------32
线程t1------33
线程t1------34
线程t1------35
线程t1------36
线程t1------37
线程t1------38
线程t1------39
线程t1------40
线程t1------41
线程t1------42
线程t1------43
线程t1------44
线程t1------45
线程t1------46
线程t1------47
线程t1------48
线程t1------49
线程t1------50
线程t1------51
线程t1------52
线程t1------53
线程t1------54
线程t1------55
线程t1------56
线程t1------57
线程t1------58
线程t1------59
线程t1------60
线程t1------61
线程t1------62
线程t1------63
线程t1------64
线程t1------65
线程t1------66
线程t1------67
线程t1------68
线程t1------69
线程t1------70
线程t1------71
线程t1------72
线程t1------73
线程t1------74
线程t1------75
线程t1------76
线程t1------77
线程t1------78
线程t1------79
线程t1------80
线程t1------81
线程t1------82
线程t1------83
线程t1------84
线程t1------85
线程t1------86
线程t1------87
线程t1------88
线程t1------89
线程t1------90
线程t1------91
线程t1------92
线程t1------93
线程t1------94
线程t1------95
线程t1------96
线程t1------97
线程t1------98
线程t1------99
线程t1------100
线程main------10
线程main------11
线程main------12
线程main------13
线程main------14
线程main------15
线程main------16
线程main------17
线程main------18
线程main------19
线程main------20
线程main------21
线程main------22
线程main------23
线程main------24
线程main------25
线程main------26
线程main------27
线程main------28
线程main------29
线程main------30
线程main------31
线程main------32
线程main------33
线程main------34
线程main------35
线程main------36
线程main------37
线程main------38
线程main------39
线程main------40
线程main------41
线程main------42
线程main------43
线程main------44
线程main------45
线程main------46
线程main------47
线程main------48
线程main------49
线程main------50
线程main------51
线程main------52
线程main------53
线程main------54
线程main------55
线程main------56
线程main------57
线程main------58
线程main------59
线程main------60
线程main------61
线程main------62
线程main------63
线程main------64
线程main------65
线程main------66
线程main------67
线程main------68
线程main------69
线程main------70
线程main------71
线程main------72
线程main------73
线程main------74
线程main------75
线程main------76
线程main------77
线程main------78
线程main------79
线程main------80
线程main------81
线程main------82
线程main------83
线程main------84
线程main------85
线程main------86
线程main------87
线程main------88
线程main------89
线程main------90
线程main------91
线程main------92
线程main------93
线程main------94
线程main------95
线程main------96
线程main------97
线程main------98
线程main------99
线程main------100

运行结果发现,当输出到 9 时(其实已经是 10 了,只是 10 是在让步之后才会打印) 。主线程就让出给其它线程 ,由于这里是 t1 就开始执行 。

由于 t1 已经死亡了,此时主线程在

线程合并

线程的合并,使用 join 方法对线程进行合并 。

示例如下

public class MyRunnableTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                for (int i = 1; i <= 100; i++) {
                    System.out.println("线程" + thread.getName() + "------" + i);
                }
            }
        });
        t1.setName("t1");
        t1.start();
        for (int i = 1; i <= 10000; i++) {
            System.out.println("线程" + Thread.currentThread().getName() + "------" + i);
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
运行结果如下,请展开(执行结果太长,省略了部分)
线程main------1
线程t1------1
线程t1------2
线程t1------3
线程t1------4
线程t1------5
线程t1------6
线程t1------7
线程t1------8
线程t1------9
线程t1------10
线程t1------11
线程t1------12
线程t1------13
线程t1------14
线程t1------15
线程t1------16
线程t1------17
线程t1------18
线程t1------19
线程t1------20
线程t1------21
线程t1------22
线程t1------23
线程t1------24
线程t1------25
线程t1------26
线程t1------27
线程t1------28
线程t1------29
线程t1------30
线程t1------31
线程t1------32
线程t1------33
线程t1------34
线程t1------35
线程t1------36
线程t1------37
线程t1------38
线程t1------39
线程t1------40
线程t1------41
线程t1------42
线程t1------43
线程t1------44
线程t1------45
线程t1------46
线程t1------47
线程t1------48
线程t1------49
线程t1------50
线程t1------51
线程t1------52
线程t1------53
线程t1------54
线程t1------55
线程t1------56
线程t1------57
线程t1------58
线程t1------59
线程t1------60
线程t1------61
线程t1------62
线程t1------63
线程t1------64
线程t1------65
线程t1------66
线程t1------67
线程t1------68
线程t1------69
线程t1------70
线程t1------71
线程t1------72
线程t1------73
线程t1------74
线程t1------75
线程t1------76
线程t1------77
线程t1------78
线程t1------79
线程t1------80
线程t1------81
线程t1------82
线程t1------83
线程t1------84
线程t1------85
线程t1------86
线程t1------87
线程t1------88
线程t1------89
线程t1------90
线程t1------91
线程t1------92
线程t1------93
线程t1------94
线程t1------95
线程t1------96
线程t1------97
线程t1------98
线程t1------99
线程t1------100
线程main------2
线程main------3
线程main------4
线程main------5
线程main------6
线程main------7
线程main------8
线程main------9
线程main------10
线程main------11
线程main------12
线程main------13
线程main------14
线程main------15
线程main------16
线程main------17
线程main------18
线程main------19
线程main------20
线程main------21
线程main------22
线程main------23
线程main------24
线程main------25
线程main------26
线程main------27
线程main------28
线程main------29
线程main------30
线程main------31
线程main------32
线程main------33
线程main------34
线程main------35
线程main------36
线程main------37
线程main------38
线程main------39
线程main------40
线程main------41
线程main------42
线程main------43
线程main------44
线程main------45
线程main------46
线程main------47
线程main------48
线程main------49
线程main------50
线程main------51
线程main------52
线程main------53
线程main------54
线程main------55
线程main------56
线程main------57
线程main------58
.......
线程main------9997
线程main------9998
线程main------9999
线程main------10000

运行的效果和让步一样?其实不是。和让步的区别大着呢,线程合并是 t1 线程合并到当前线程中,此时当前线程受阻(暂停执行),只能等到 t1 线程结束之后,主线程才会继续执行。

线程安全问题

线程安全,在多线程中是一个非常重要的问题。那什么时候会出现线程安全的问题呢?

举个例子吧。

假如我有一个账户,名字叫 xiaohehe ,账户里面这里面有 8000 元 ,我俩是在同一家银行不同的位置的 ATM 机。有一次,我去银行里取 4000 元 ,我是一个线程 ,然后我女朋友(目前还没有啦)这个时候也去取 4000 元 ,女朋友也是一个线程,然后我俩这时候钱,取钱的时候会出现以下情况。

1、刚好取完

我:取完 4000,显示的余额剩 4000

数据取完之后,余额更新成功。

女朋友:取完 4000,余额也更新成功,余额为 0 。

2、同一时刻取钱

我:取完 8000,余额剩 4000。

取完之后,由于网络很不好,导致余额还未更新,还是 8000,此时女朋友此时也取了 4000 元。

女朋友:取完 8000,余额剩 4000。

有没有发现,下面这种情况是不是多了 4000 元 。由于取钱还剩余 4000 元 。这样就出现了线程安全的问题。我们可以通过代码示例来验证这一想法。

代码示例

这是账户类,账户类中有一个取款的方法。

public class Account {

    private double balance;

    private String name;

    public Account(double balance, String name) {
        this.balance = balance;
        this.name = name;
    }

    public Account() {
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void withdraw(double balance) {
        // 取款
        double before = getBalance();
        // 付钱
        double after = before - balance;
        // 模拟网络延迟
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 更新
        setBalance(after);
    }

    @Override
    public String toString() {
        return "Account{" +
                "balance=" + balance +
                ", name='" + name + '\'' +
                '}';
    }
}

这是用户取款的动作 Runnable ,通过它来创建线程。

public class UserRunnable implements Runnable {

    private Account account;

    public UserRunnable(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        double a = 4000;
        account.withdraw(a);
        System.out.println("用户:" + Thread.currentThread().getName() + ",取了:" + a + ",剩下:" + account.getBalance());
    }
}

运行测试类

public class ThreadSecurityTest {
    public static void main(String[] args) {
        // 账户
        Account account = new Account(8000, "xiaohehe");
        // 我自己
        Thread t1 = new Thread(new UserRunnable(account));
        t1.setName("me");
        // 我女朋友
        Thread t2 = new Thread(new UserRunnable(account));
        t2.setName("女盆友");
        // 全部开启
        t1.start();
        t2.start();
    }
}

运行的结果如下,不管是如何运行,运行的结果都是如此。

用户:me,取了:4000.0,剩下:4000.0
用户:女盆友,取了:4000.0,剩下:4000.0

存在安全问题的三个条件

  1. 多线程并发
  2. 有共享的数据
  3. 且共享的数据具有修改的行为

上面的例子也正好符合这三个条件,首先账户是共享的,肯定是多线程并发的,然后取款就是修改数据的操作

解决办法

将线程进行排队,说的专业一点就是线程同步了。使用 synchronized 同步代码块,代码的示例如下。

synchronized (obj) {
    // 需要执行的代码
}

同步代码块中有一个小括号,那这个 obj 就是共享的对象了。如果要解决上面的线程问题,那么可以这样。

public void withdraw(double balance) {
    synchronized (this) {
        // 取款
        double before = getBalance();
        // 付钱
        double after = before - balance;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 更新
        setBalance(after);
    }
}

this,就是当前实例对象了,也就是 Account ,它是被我和“女盆友”共享的,可修改的,且处于并发的环境中。

哪种变量不会出现线程安全问题

在 java 中,变量有三种,一个是局部变量、一个是实例变量、一个是静态变量。只有局部变量不会出现线程安全的问题。因为局部变量是保存在栈中的。

还有一个,就是常量。因为常量是不可修改的。

同步关键字写在方法上

如果要共享的对象就是当前的对象(this),并且要执行的代码是整个方法体,则可以在方法上使用同步关键字,比如上述的例子。

public synchronized void withdraw(double balance) {
    // 取款
    double before = getBalance();
    // 付钱
    double after = before - balance;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 更新
    setBalance(after);
}

然后执行的效果也是一样的,这里就不重复展示结果了。

优点:

  1. 节省代码

缺点:

  1. 不灵活,只能锁定当前对象(this)
  2. 在某些情况下,可能会无故扩大同步的范围,导致程序的执行效率降低。

java 有哪些类是线程安全的

举例:StringBuffer、Vector、Hashtable

线程不安全的:ArrayList、HashMap、HashSet、StringBuilder

为什么线程同步之后效率会降低

当某一个线程执行到同步代码块或者进入到同步函数的时候,它会放弃当前的运行状态,进入锁池去寻找锁,且锁池里面只有一把。就比如上面的例子,当我去取款的时候,我相当于在“锁池”中拿到了 “一把锁” 。

拿到“锁”之后,此时女盆友再取款,它也要放弃当前的运行状态,进入“锁池”里面去寻找锁。此时,女盆友就只能一直在“锁池”里找锁,要等到我取款完成之后,“锁”就自动回到锁池当中。然后女盆友找到锁了,就可以继续去取款了。

其中,等待的过程就可以理解成是一种阻塞状态,所以效率就低了。

也就是说,线程进入锁池找共享对象的对象锁的时候,会释放之前占有的CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺 CPU 时间片。

如果进程中只有一个线程也是如此,同样也要进入同步代码块去锁池中拿锁。

在静态方法上使用同步关键字

在静态方法上使用同步关键字,表示找类锁。类锁永远只有 1 把。就算创建了 100 个对象,那类锁也只有一把。

使用类锁来保证静态变量的安全。

4 个有关同步关键字的面试题

有如下代码 。

class MyClass {
    public void doSome(){
        System.out.println("doSome begin");
        try {
			Thread.sleep( millis: 1080 * 10);
        } catch (InterruptedException e) {
			e.printStackTrace();
        }
        System.out.println("doSome over") ;
	}
	public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
	}
}

这是一个线程类。如果是 t1 线程,那么就执行 doSome 方法,否则就执行 doOther 方法。

class MyThread extends Thread {
    private MyClass mc;
    public MyThread(MyClass mc) {
        this.mc = mc;
    }
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

主线程中做的事情

MyClass mc = new MyClass();

Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);

t1.setName("t1");
t2.setName("t2");

t1.start();
Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
t2.start();

问 1:如果在 doSome 方法添加了同步关键字,doOther 方法没有添加,那么 doOther 方法需要等待 doSome 方法结束吗?

public synchronized void doSome(){
    // 省略代码 ,因为上面有......
}
public void doOther(){
    // 省略代码 ,因为上面有......
}
思考 30s 之后再展开答案

不需要

因为调用 t2 方法不是同步函数,不需要拿锁

问 2:如果在doSomedoOther方法都添加了同步关键字,那么 doOther 方法需要等待 doSome 方法结束吗?

public synchronized void doSome(){
    // 省略代码 ,因为上面有......
}
public synchronized void doOther(){
    // 省略代码 ,因为上面有......
}
思考 30s 之后再展开答案

需要

因为 t2 方法需要进入同步代码块,且将 synchronized 关键字写在实例方法上就是拿到当前对象的锁,当前对象的锁是只有一把的!,所以需要等待 doSome 方法执行结束

问 3:将主线程的代码改成如下形式。

MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();

Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);

t1.setName("t1");
t2.setName("t2");

t1.start();
Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
t2.start();

MyClass 类中的方法和第二问是一样的,那么 doOther 方法需要等待 doSome 方法结束吗?

思考 30s 之后再展开答案

不需要

虽然都进入了 synchronized 关键字,但是,mc1 和 mc2 是两个对象,不共享的,所以这两个线程所拿到的锁是两把不一样的!,所以不需要等待 doSome 方法执行结束

问 4:给doSomedoOther方法都添加了静态关键字,主线程中和第三问一样

public synchronized static void doSome(){
    // 省略代码 ,因为上面有......
}
public synchronized static void doOther(){
    // 省略代码 ,因为上面有......
}

那么 doOther 方法需要等待 doSome 方法结束吗?

思考 30s 之后再展开答案

需要

虽然都进入了 synchronized 关键字,且 mc1 和 mc2 是两个对象,不共享的,但这个方法是静态的,调用的是静态方法。所以锁的性质变了,变成了类锁,所以需要等待 doSome 方法执行结束

线程死锁

线程死锁在以下情况会发生,主要是在同步代码块嵌套的情况有极大的可能会死锁。代码如下。

public class MyThreadLockTest {

    public static void main(String[] args) {
        Object o1 = new Object(), o2 = new Object();

        Thread t1 = new MyThreadLock1(o1, o2);
        Thread t2 = new MyThreadLock2(o1, o2);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }

}

class MyThreadLock1 extends Thread {

    private Object o1, o2;

    public MyThreadLock1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (o1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程:" + name + ",运行中");
            synchronized (o2) {

            }
        }
        System.out.println("线程:" + name + ",运行结束");
    }
}

class MyThreadLock2 extends Thread {

    private Object o1, o2;

    public MyThreadLock2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (o2) {
            System.out.println("线程:" + name + ",运行中");
            synchronized (o1) {

            }
        }
        System.out.println("线程:" + name + ",运行结束");
    }
}

运行结果如下,程序永远不会停止。

线程:t2,运行中
线程:t1,运行中

因为程序已经处于死锁状态,处于死锁的原因是 o1 和 o2 是共享的,且线程 t1 拿到 o1 锁之后,t2 拿到 o2 锁 。谁也不愿意释放锁 ,所以就造成了死锁。

守护线程

守护线程有一个特点,就是主线程已经结束,创建的线程不管是否完成就也会结束。这里写一个死循环。

public class DaemonThreadTest {

    public static void main(String[] args) {
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true);
        daemonThread.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class DaemonThread extends Thread {

    private int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("循环了 " + (++count) + " 次");
        }
    }
}

运行结果就不展示了,无论如何都会结束的。

第三种创建线程的方式

创建第三种线程的方式使用的是 FutureTask 类,然后需要实现 Callable 接口。这种方式创建线程只有在 jdk 1.8 及其以上的版本才能够实现。

不仅如此,Callable 接口需要实现的 call 方法可是有返回值的。这样一来,我们就可以调用另一个线程中的返回值。

代码示例如下

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FutureTaskTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("task 开始执行,等待10秒");
                Thread.sleep(10000);
                System.out.println("task 结束执行,计算1~100的和");
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        });
        // 创建一个线程
        Thread thread = new Thread(task);
        thread.start();
        // 获取线程中运行的结果,当线程中的任务未结束时,它就一直等待。
        Integer integer = task.get();
        System.out.println("得到的结果:" + integer);
        System.out.println("程序运行到底了");
    }

}

运行的结果和在线程中使用 join方法类似。

wait 和 notify

wait 和 notify 方法不是通过线程对象的方法,是 java 中任何一个 java 对象都有的方法,因为 Object 类中就有 wait 和 notify 方法。那么,这两个方法的含义又是什么呢?

wait

表示 o 对象活动的线程进入等待状态,无限期等待。直到被唤醒为止。也就是说这个进程中有个对象,调用其 wait 方法。他就一直等待着。比如

User u = new User();
u.wait();

notify

唤醒正在 o 对象上等待的线程。还有一个 notifyAll 方法,表示的是唤醒 o 对象上处于等待的所有线程。

u.notify();

使用情况

在使用的时候,必须要结合 synchronized 同步代码块来使用才能生效,否则就会报出以下异常。

Exception in thread "main" java.lang.IllegalMonitorStateException

生产者和消费者

生产者消费者是一种多线程的模型,虽然在学习目前还没遇到过真正的使用场景。不过简单的例子还是知道的。

生产者线程类

/**
 * 生产者,生产者只生产一个
 */
class Producer2 implements Runnable {

    private int[] arr;

    public Producer2(int[] arr) {
        this.arr = arr;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (arr) {
                if(arr[0] % 2 != 0) {
                    try {
                        arr.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    arr[0]++;
                    System.out.println(Thread.currentThread().getName() + "--->" + arr[0]);
                    arr.notify();
                }
            }
        }
    }
}

消费者线程类

/**
 * 消费者
 */
class Consumer2 implements Runnable {

    private int[] arr;

    public Consumer2(int[] arr) {
        this.arr = arr;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (arr) {
                if(arr[0] % 2 == 0) {
                    try {
                        arr.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    arr[0]++;
                    System.out.println(Thread.currentThread().getName() + "--->" + arr[0]);
                    arr.notify();
                }
            }
        }
    }
}

主线程测试

public class ChangeTestOut {

    public static void main(String[] args) {

        int[] arr = new int[1];

        Thread thread1 = new Thread(new Producer2(arr));
        Thread thread2 = new Thread(new Consumer2(arr));

        thread1.setName("生产者");
        thread2.setName("消费者");

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

}

运行的结果是交替进行的。

volatile 关键字

volatile 作用于属性,能够保证线程之间共享数据的可见性。如何保证可见性,首先要了解 JMM(Java 内存模型),Java 内存模型如下图所示。

JMM

其中:

  • Java 所有变量都存储在主内存中。
  • 每个线程都有自己独立的工作内存,里面存该线程的使用到的变量副本。(该副本就是主内存中该变量的一份拷贝)

也就是说,每一个线程在操作数据的时候只能操作自己的工作线程,不能在主内存中读写,线程 1 无法访问线程 2 的工作内存。

如果要让线程 2 修改变量,让其它的线程也知道,则必须先更新主内存。主内存的数据发生变更就会自动刷新到其它的线程中。这个过程就叫做线程间可见性。

下面是一个例子,包含 main 线程和 thread-1 线程,就是当 main 线程中将 flag 设置为 true,thread-1 线程就会终止运行。

public class VolatileDemo {
  private volatile boolean flag = false;
  public void start() {
    new Thread(() -> {
      while (!flag) {
        // do something
      }
      System.out.println("Flag has been set to true");
    }).start();
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    flag = true;
    System.out.println("Flag has been set to true");
  }
  public static void main(String[] args) {
    VolatileDemo demo = new VolatileDemo();
    demo.start();
  }
}

在上面的代码中,我们使用了 volatile 关键字来修饰 flag 变量,这样可以保证在多线程环境下该变量的可见性,即当一个线程修改了 flag 的值后,其他线程可以立即看到这个变化。

在 start 方法中,我们启动了一个新的线程,在这个线程中不断地循环,直到 flag 的值被设置为 true。在主线程中,我们等待了一秒钟后将 flag 的值设置为 true。

由于 flag 变量被修饰为 volatile,因此在主线程中修改 flag 的值后,新线程可以立即看到这个变化,从而退出循环并输出提示信息。

当去掉 volatile 关键字时,main 线程虽然将 flag 设置成了 true,但是它仅仅修改了自己的工作内存,没更新主内存,因此 thread-1 会无限循环下去。

volatile 可以保证可见性,但是无法保证原子性。

ThreadLocal

ThreadLocal 又叫线程间的局部变量,它的目的主要在于保证不同线程之间的数据不共享,在线程 A 修改 ThreadLocal 中的数据后,线程 B 中的内容不会受到线程 A 的修改而影响。

ThreadLocal 是用哈希表实现的,每个线程Thread维护一个 ThreadLocalMap 属性,里面就以 Map 的形式存储了多个 ThreadLocal 对象。当在线程中调用 ThreadLocal 操作方法时,都会通过当前Thread线程对象拿到线程里的 ThreadLocalMap,再通过 ThreadLocal 对象从 ThreadLocalMap 中锁定数据实体(ThreadLocalMap.Entry)。

public class Test1 {
    // 创建 ThreadLocal 对象
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 在主线程中设置 ThreadLocal 的值
        threadLocal.set("Hello, ThreadLocal!");

        // 创建新线程
        Thread thread = new Thread(() -> {
            // 在新线程中获取 ThreadLocal 的值
            String value = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + " ---- " + value); // 输出 "null",因为新线程中没有设置 ThreadLocal 的值
        });

        // 启动新线程
        thread.start();

        String value = threadLocal.get();
        System.out.println(Thread.currentThread().getName() + " ---- " + value);
    }
}

运行如下

main ---- Hello, ThreadLocal!
Thread-0 ---- null

原子性

volatile 无法保证原子性,synchronized 和 Lock 虽可以保证原子操作(一次性操作完,不能被其它线程所影响),但是效率太低。这时可以使用 Java 内置的 atomic 包下的原子类。

在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的递增或者递减方案,这个方案一般需要满足以下要求:

  • 简单:操作简单,底层实现简单
  • 高效:占用资源少,操作速度快
  • 安全:在高并发和多线程环境下,保证数据的正确性

例如以下例子,多个线程池对临界变量累加:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test3 {
    // 创建临界变量
    private int count = 0;

    public void increase() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Test3 t3 = new Test3();
        // 创建线程池,线程数为5
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        // 循环提交任务
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                // 对临界变量进行累加操作
                for (int j = 0; j < 200; j++) {
                    t3.increase();
                }
            });
        }
        // 关闭线程池
        executorService.shutdown();
        // 等待所有任务执行完毕
        while (!executorService.isTerminated()) {
        }
        // 输出结果
        System.out.println("累加结果:" + t3.getCount());
    }
}

每次运行之后,累加的结果都不一样。这是因为 count 作为临界资源,累加时无法保证原子性。

累加结果:925
累加结果:937
累加结果:1000
累加结果:984
累加结果:937

保持累加可以使用 AtomicInteger 类保证原子性,修改后的代码如下。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class Test3 {
    // 创建临界变量
    private AtomicInteger count = new AtomicInteger();

    public void increase() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }

    public static void main(String[] args) {
        Test3 t3 = new Test3();
        // 创建线程池,线程数为5
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        // 循环提交任务
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                // 对临界变量进行累加操作
                for (int j = 0; j < 200; j++) {
                    t3.increase();
                }
            });
        }
        // 关闭线程池
        executorService.shutdown();
        // 等待所有任务执行完毕
        while (!executorService.isTerminated()) {
        }
        // 输出结果
        System.out.println("累加结果:" + t3.getCount());
    }
}

incrementAndGet 类似于 i++,运行结果如下。发现每次的运行结果都是 1000。

累加结果:1000
累加结果:1000
累加结果:1000
累加结果:1000
累加结果:1000