java 线程基础篇,看这一篇就够了。

时间:2022-05-23 09:33:31

前言:

Java三大基础框架:集合,线程,io基本是开发必用,面试必问的核心内容,今天我们讲讲线程。

想要把线程理解透彻,这需要具备很多方面的知识和经验,本篇主要是关于线程基础包括线程状态和常用方法。

本篇主要从线程常用方法来理解线程各个状态及状态的切换,之后再通过状态于状态之间的切换来加深对线程常用方法的应用于印象。

正题:

java中定义了线程的几种状态,在java.lang.Thread.State中,分别为以下6个:

NEW(初始化),RUNNABLE(就绪),BLOCKED(阻塞),WAITING(等待),TIMED_WAITING(超时等待),TERMINATED(终止)java 线程基础篇,看这一篇就够了。

1. 创建:通过继承Thread类,或者实现Runnable接口来创建一个线程。

  方式1:继承Java.lang.Thread类,并覆盖run() 方法

    优势:编写简单

    劣势:单继承的限制----无法继承其它父类,同时不能实现资源共享

  方式2:实现Java.lang.Runnable接口,并实现run()方法

    优势:可继承其它类,多线程可共享同一个Thread对象

    劣势:编程方式稍微复杂,如需访问当前线程,需调用Thread.currentThread()方法

public class Daqiu extends Thread{

        @Override
public void run() {
System.out.println("我打完球了");
}
} public class chifan implements Runnable{ @Override
public void run() {
System.out.println("我吃完了");
}
}

2.就绪:当线程调用start()方法,会进入准备就绪状态。

  start方法是Thread 类的方法,在这个方法中会调用native方法(start0())来让线程就绪,等待CPU为该线程分配资源(时间片)。

3.运行当线程获得cpu资源(时间片)会执行run()达到正真运行的效果,并可以调用yield()方法试探性的让出cpu执行器权。

  1) 上面说了,start()方法最终调用了一个native的方法,并非java实现的,所以这里的run()方法是如何被调用的我们就不研究了。

  2) yield()方法是对调度器的一个暗示表示愿意让出CPU执行器的当前使用权,但是调度器可以*忽略这个提示。此方法使用极少,不过多研究。

  3) 要注意,我们手动调用线程的run()方法也会执行run()方法的内容,但是这样就没有达到多线程的效果。这也是run()方法和start()方法的一个重要区别。通过下面的示例可以看出输出的线程名不一样。

public class TestThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
} public static void main(String[] args){
  TestThread test = new TestThread();
test.start();
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.run();
}
结果: 

Thread-0
main
Process finished with exit code 0

4.超时等待:又称为计时等待,让线程等待一定时间。

  1) sleep(long): 让当前线程睡眠一定时间后再继续执行,此时只释放资源,不释放锁。这是一个静态的native方法,不过多研究。

  2) join(): 该方法有三个重载join(),join(long millis),join(long millis,int nanoseconds),主要看第二个,就是等待一个线程指定毫秒数后再执行。无参数的join方法其实就是调用了join(0),即永远等待下去。不过通过源码我们可以看到,在while循环中有一个条件判断,即isAlive()方法,意思是如果当前线程还活着,就会一直等待下去。

public static native void sleep(long millis) throws InterruptedException;

public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)  throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0; if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
} if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
 

对于join()方法的理解来看一个示例:

public class Chifan extends Thread{

    private Daqiu daqiu;

    public Chifan(String name,Daqiu daqiu){
super(name);
this.daqiu = daqiu;
} @Override
public void run() {
try {
daqiu.join();
System.out.println(getName()+"我开始吃饭。。。");
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"我吃完了");
}
}
public class Daqiu extends Thread {

    private int playTime;

    public Daqiu(String name,int playTime) {
super(name);
this.playTime = playTime;
} @Override
public void run() {
System.out.println(getName()+"我开始打球了");
try {
sleep(playTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"我打完球了");
}
}
public static void main(String[] args){
Daqiu daqiu = new Daqiu("打球线程:",1000);
Chifan chifan = new Chifan("吃饭线程:",daqiu);
System.out.println("打完球才能吃饭");
daqiu.start();
chifan.start();
}
打完球才能吃饭
打球线程:我开始打球了
打球线程:我打完球了
吃饭线程:我开始吃饭。。。
吃饭线程:我吃完了 Process finished with exit code 0

可以看到:daqiu.join();一定是等打完球了才会执行后面的吃饭。

5.等待:wait()方法只能在synchronized中调用,因为前提是已经拥有某对象锁,但是选择暂时交出去,此时线程将进入等待队列。

  Object类中有三个不同参数的wait()方法,如果传入时间参数,也可以理解为计时等待,但于sleep()不同的是wait()方法会释放拥有的锁,当被其他持有该锁的线程调用notify()或notifyAll()唤醒时将进入同步队列去竞争锁。wait()实际也是调用了wait(long)方法,参数为0,表示一直等待下去。

6.阻塞:当synchronized(Obj)去竞争一个对象锁时,如果对象锁被其他线程占用,那么线程将进入等待队列(阻塞)  

  在java中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA中简单的同步、互斥操作。当出现阻塞锁定需要等其他持有该对象锁的线程调用wait(),notify()或notifyAll()释放对象锁后才有机会获取锁进入运行状态,所以wait(),notify()或notifyAll()只能在synchronized获取到对象锁内使用,这于第五点是相呼应的。(注意:调用notify()方法后,当前线程并不会马上释放该对象锁,要等到执行notify()方法的线程执行完才会释放对象锁)

7.终止:当线程run()方法正常执行完毕,或者出现未捕获的异常,线程就已经完成了他的使命进入终止状态了。


了解了线程的6个状态以及常用方法后,再来通过一个示例看看线程运行切换的一个流程。
思路就是:创建一个Student对象,三个线程A,B,C,且各自获取Student对象锁,其中A,B调用wait()方法释放锁进入等待队列。C线程调用notify()方法唤醒等待队列中的线程,
public class TestThread extends Thread {

    private Student student;

    public TestThread(String name,Student student){
super(name);
this.student = student;
}
@Override
public void run() {
synchronized (student){
try {
System.out.println("我是线程:"+Thread.currentThread().getName());
student.wait();
System.out.println("我是线程:"+Thread.currentThread().getName()+",我拿到了对象锁");
System.out.println("我是线程:"+Thread.currentThread().getName()+",我执行完了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class NotifyThread extends Thread {

    private Student student;

    public NotifyThread(String name,Student student){
super(name);
this.student = student;
} @Override
public void run() {
synchronized (student){
System.out.println("我是唤醒线程"+Thread.currentThread().getName());
student.notifyAll();
System.out.println("我是唤醒线程"+Thread.currentThread().getName()+",我已经执行唤醒。");
}
}
}
public static void main(String[] args) throws InterruptedException {
Student student = new Student("小明","12");
TestThread testA = new TestThread("A",student);
TestThread testB = new TestThread("B",student);
NotifyThread testC = new NotifyThread("C",student);
testA.start();
testB.start();
sleep(10);
testC.start();
}
我是线程:A
我是线程:B
我是唤醒线程C
我是唤醒线程C,我已经执行唤醒。
我是线程:B,我拿到了对象锁
我是线程:B,我执行完了
我是线程:A,我拿到了对象锁
我是线程:A,我执行完了 Process finished with exit code 0

延申一个线程面试中的经典问题:创建3个线程,顺序答应‘A’,‘B’,‘C’各10次

其实思路也是利用wait()和notify()来控制线程的执行顺序,具体实现这里就不说了。

总结:

java中线程还有非常多的延申,包括线程底层实现,多线程同步,并发,安全,唤醒机制,以及线程池,要学习于沉淀的知识非常多。希望本文能让自己对线程的基础有个清晰的理解。