进程和线程
进程,绝大部分使用过电脑的人都知道这东西。进程是一个正在运行的应用程序,Windows 上打开任务管理器就可以看到许许多多的进程,有什么系统进程和用户进程。
线程,线程似乎绝大部分使用的过电脑的人都不是很熟悉,也许只在“性能”模块中的“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 {
//......
}
质疑
- 为什么我运行的结果和视频上运行的结果是完全不一样的呀?
- 为什么我运行的时候,总是主线程现执行而不是子线程先执行?
- 主线程一定是最先运行的吗,子线程就没有先输出的权利吗?
- 你看,我的运行结果完了之后,它就是主线程先执行完再执行子线程,这我觉得和单线程没什么区别呀?
- 调用 start 方法和调用的 run 的方法好像没什么区别吧?
- ……
以上是我在多线程学习时,我目前所遇到的疑问,可能还有其它的问题。显然,出现 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 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该线程允许运行的时间。
在 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
存在安全问题的三个条件
- 多线程并发
- 有共享的数据
- 且共享的数据具有修改的行为
上面的例子也正好符合这三个条件,首先账户是共享的,肯定是多线程并发的,然后取款就是修改数据的操作
解决办法
将线程进行排队,说的专业一点就是线程同步了。使用 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);
}
然后执行的效果也是一样的,这里就不重复展示结果了。
优点:
- 节省代码
缺点:
- 不灵活,只能锁定当前对象(this)
- 在某些情况下,可能会无故扩大同步的范围,导致程序的执行效率降低。
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:如果在doSome
、doOther
方法都添加了同步关键字,那么 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:给doSome
、doOther
方法都添加了静态关键字,主线程中和第三问一样
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 内存模型如下图所示。
其中:
- 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
请勿发布违反中国大陆地区法律的言论,请勿人身攻击、谩骂、侮辱和煽动式的语言。