哈喽,大家好,线程是Java中很重要的一个知识点,我相信大家都知道如何运用多线程来处理任务,但是其中有很多细节可能不是特别的明白,我打算做一系列有关线程的文章,就当是个记录,顺便和大家分享一下有关线程的知识。
这篇文章我们先来讲一讲线程的基础知识,那么下面直接开始。
进程
一说到线程,那就不得不提进程。这两个概念很多人最开始容易混淆,而且面试的时候,有的面试官也会问到。那么什么是进程呢,进程是程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
这样说可能还是有点懵逼,举个简单的栗子,你在手机上启动一个软件,那么这个软件就是一个进程。或者说你在电脑上打开QQ,那么这个QQ就是一个进程。
线程
进程说完了,来说说线程,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程可以在进程中独立运行子任务,并且一个进程至少有一个线程。
来举个栗子,假如你在手机上启动了QQ,在QQ中你可以和好友聊天,下载文件,传输数据,其中每一项工作我们都可以理解为一个线程在执行。这些工作也可以同时执行,当它们同时进行的时候我们可以理解为多个线程同时执行,这也是线程的好处之一,同时处理多个任务,以节约时间。
多线程同时工作的时候其实是CPU在各个线程之间快速切换,速度很快,使我们感觉是在同时进行。
线程运用
线程的调用大家肯定都很熟悉了,有两种方法来调用线程执行任务,下面我们来分别讲一讲。
新建一个类并继承Thread类
下面我们看下代码
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("TestThread is run");
}
}
}
代码大家肯定都很熟悉,需要注意的是start方法重复调用会报错。
当我们继承Thread类的时候有一个不好的地方是Java并不能多继承,这样可能会影响代码的灵活性,所以一般来说实现Runnable接口是一个更好的选择。
新建一个类实现Runnable接口
我们在Thread源码中看到,Thread的构造函数可以传入Runnable。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
所以我们也可以新建一个类并实现Runnable接口传入Thread中,并执行该线程。
下面我们先看看代码
public class TestMain {
public static void main(String[] args) {
Thread thread = new Thread(new TestThread());
thread.start();
}
public static class TestThread implements Runnable{
@Override
public void run() {
System.out.println("test is run");
}
}
}
这个相信大家也是写了很多遍了,没什么好说的。
线程执行不确定性
线程在执行的过程中有不确定性,这里我们先来看个例子。
public class TestMain {
public static void main(String[] args) {
Thread thread = new Thread(new TestThread());
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("main");
}
}
public static class TestThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("thread");
}
}
}
}
运行结果
thread
thread
thread
main
main
main
main
main
thread
thread
thread
我们可以看到在运行的结果中thread和main是交叉打印出来的,并不是先执行完thread或者main。当我们调用start方法的时候,会告诉"线程规划器"这个线程已经准备好了,等待调用线程对象的run方法。这个过程就是让系统安排一个时间来调用该线程中的run方法,使线程得到运行,具有异步执行的效果。所以我们会看到thread和main会交叉打印出来。
线程安全
线程安全是线程知识里面一个重要的知识点,简单来说就是当多个线程同时访问同一个变量时,可能会造成变量的不同步。我们先来举例,加入有5张门票,5个售票员,每个售票员卖出一张门票,门票数量就少1。下面先看看代码。
public class TestMain {
private static int count = 5;
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread a = new Thread(testThread, "A");
Thread b = new Thread(testThread, "B");
Thread c = new Thread(testThread, "C");
Thread d = new Thread(testThread, "D");
Thread e = new Thread(testThread, "E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
count--;
System.out.println(currentThread().getName() + "卖出一张票,还剩余:" + count);
}
}
}
代码很简单,就是依照上面的栗子写的,那么我们来看看运行结果
A卖出一张票,还剩余:3
B卖出一张票,还剩余:3
C卖出一张票,还剩余:2
D卖出一张票,还剩余:1
E卖出一张票,还剩余:0
我们可以看到结果中出现了两个3,这个是因为A,B同时访问了这个变量造成的。这就是线程安全问题,那么我们如何解决这个问题呢。Java给我们提供了synchronized字符,我们先来修改一下代码。
public static class TestThread extends Thread {
@Override
synchronized public void run() {
count--;
System.out.println(currentThread().getName() + "卖出一张票,还剩余:" + count);
}
}
我们在run方法前面加入synchronized。下面我们来看看运行结果。
B卖出一张票,还剩余:4
C卖出一张票,还剩余:3
A卖出一张票,还剩余:2
D卖出一张票,还剩余:1
E卖出一张票,还剩余:0
结果中并没有重复的数字出现。当在run方法前面加入synchronized的时,运行到run方法,会先去判断run方法是否有加锁,如果加锁了,证明别的线程在调用这个方法,就先等待其他线程调用完毕后再执行这个方法。这样run方法就是排队执行完成的,所以结果正常,没有同时访问同一个变量。当运行run方法的时候,如果没有加锁,那么线程会去拿这个锁,注意这里是所有线程同时抢这把锁,谁抢到了就先执行谁的run方法。
isAlive
isAlive方法是判断线程是否处于激活状态。我们先来看看代码
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
System.out.println("start testThread isAlive = " + testThread.isAlive());
testThread.start();
System.out.println("end testThread isAlive = " + testThread.isAlive());
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("testThread isAlive = " + currentThread().isAlive());
}
}
}
运行结果为
start testThread isAlive = false
end testThread isAlive = true
testThread isAlive = true
我们可以发现当调用start方法过后,线程就处于激活状态了。因为这里end在线程执行完成之前就打印了,所以也是true,如果我们修改下代码,那么end就可能为false了。
public static void main(String[] args) {
try {
TestThread testThread = new TestThread();
System.out.println("start testThread isAlive = " + testThread.isAlive());
testThread.start();
Thread.sleep(1000);
System.out.println("end testThread isAlive = " + testThread.isAlive());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
结果为:
start testThread isAlive = false
testThread isAlive = true
end testThread isAlive = false
下面我们再看一个有趣的栗子:
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
thread.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("currentThread isAlive = " + currentThread().isAlive());
System.out.println("this isAlive = " + this.isAlive());
}
}
}
这个运行结果为:
currentThread isAlive = true
this isAlive = false
这里第一个为true我相信大家都可以理解,那么为什么第二个为false呢。这个就要说说currentThread这个方法了,这个方法获取的是在哪个线程中运行,而this获取的是当前线程。因为testThread 是以参数传入到了Thread中,在Thread中并不是像线程调用start方法那样来运行run方法的。而是直接调用run方法,所以this.isAlive()获取的当前线程并没有调用start方法,所以为false。而currentThread获取的是运行的线程,所以结果为true。
线程停止
线程的停止我们主要来讲一讲interrupt方法。我们先来看一段代码:
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
testThread.interrupt();
}
public static class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
System.out.println(i);
}
}
}
}
我们在线程调用start方法过后马上又调用了interrupt方法,按理来说线程应该立马停止,那么我们看看结果:
.......
49997
49998
49999
最后我们可以看到线程是完完整整执行完成了的。难道interrupt方法没有作用吗?我们先来看看另外两个方法
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
这两个方法都是判断线程是否已经中断。第一个方法是一个静态方法,并且在方法中调用了currentThread方法,所以它判断的是当前运行的线程是否已经中断。第二个方法是判断线程对象是否已经中断。我们发现他们最终都是调用了同一个方法,我们先来看看这个方法:
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
这是一个native方法,传入的参数是指是否清除线程的中断状态。true为清除,false为不清除。我们在代码中加入判断试试
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
testThread.interrupt();
System.out.println("线程是否中断:" + testThread.isInterrupted());
}
结果为:
线程是否中断:true
我们可以发现在调用interrupt方法过后其实是给线程加了一个中断的标识,我们调用isInterrupted方法就可以看出。那么我们就可以运用这个特性,让线程实现真正的中断。下面来看看修改的代码:
public class TestMain {
public static void main(String[] args) {
try {
TestThread testThread = new TestThread();
testThread.start();
Thread.sleep(200);
testThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
if (isInterrupted()) {
System.out.println("线程已经终止");
break;
}
System.out.println(i);
}
}
}
}
结果为:
......
35289
35290
线程已经终止
我们可以发现线程进入了中断判断并跳出了for循环。这样虽然可以终止for循环,但是for循环以下的代码依然会执行,有的人肯定会想到用return,这样也是可以的,并且不会执行for循环下面的代码,但是return太多会造成代码污染,这里我们推荐另一个方法。先来看看代码:
public static class TestThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 50000; i++) {
if (isInterrupted()) {
System.out.println("线程已经终止");
throw new InterruptedException();
}
System.out.println(i);
}
System.out.println("for循环后面的代码");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("进入catch:" + e.toString());
}
}
}
我们利用try catch来停止线程,并可以在catch中做一些释放等操作。
结果为:
......
39160
线程已经终止
java.lang.InterruptedException
at test.TestMain$TestThread.run(TestMain.java:24)
进入catch:java.lang.InterruptedException
yield()
yield()方法的作用是先放弃当前的CPU资源,让其他线程去占用CPU执行时间。但是放弃的时间不确定,可能刚刚放弃马上又占有CPU资源了。下面我们举个栗子:
public class TestMain {
public static void main(String[] args) {
TestThread1 testThread1 = new TestThread1();
testThread1.start();
TestThread2 testThread2 = new TestThread2();
testThread2.start();
}
public static class TestThread1 extends Thread {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
int a = i;
}
long end = System.currentTimeMillis();
System.out.println("1使用时间为:" + (end - start));
}
}
public static class TestThread2 extends Thread {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
yield();
int a = i;
}
long end = System.currentTimeMillis();
System.out.println("2使用时间为:" + (end - start));
}
}
}
结果为:
1使用时间为:1
2使用时间为:8
我们可以明显的看到2使用的时间长于1使用的时间。
线程优先级
在线程中有一个方法可以设置线程的优先级
public final void setPriority(int newPriority)
Java线程中线程分为1-10个等级,等级越高,线程被执行的几率也就越大,这里要注意是执行的几率,而不是优先级高的就比优先级低的先执行。
另外线程的优先级是有传递效果的,举个栗子,A线程启动B线程,如果A线程优先级为5,那么B线程的优先级也为5。
守护线程
守护线程可能大家平时都没有怎么用,我们平时经常使用的是用户线程,守护线程是一个特殊的线程,当我们进程中没有用户线程的时候,守护线程就会自动销毁。Java中典型的守护线程就是垃圾回收线程。下面我们来举个栗子:
public class TestMain {
public static void main(String[] args) {
try {
TestThread1 testThread1 = new TestThread1();
testThread1.setDaemon(true);
testThread1.start();
Thread.sleep(1000);
System.out.println("end");
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestThread1 extends Thread {
@Override
public void run() {
try {
int i = 0;
while (true) {
i++;
System.out.println(i);
Thread.sleep(200);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
结果为:
1
2
3
4
5
end
而当我们去掉testThread1.setDaemon(true);这句代码,结果为:
......
3
4
5
end
6
7
......
这样我们就发现当线程为守护线程的时候,main结束了,守护线程也就结束了,如果不是守护线程,则会一直执行。
到这里线程的基础就讲完了,上文中有错误的地方欢迎大家指出。