【java线程】

线程

1.进程和线程

进程:进程就是在系统中,运行一个应用程序的基本单位,每一个应用程序都是一个基本单位;

线程:程序执行的最小单位,线程是进程中的一个代码执行单元,负责当前进程中代码程序的执行,一个进程中有一个或多个线程。 当一个进程中启动了多个线程去分别执行代码的时候,这个程序就是多线程程序。

2.并发与并行

线程的并发执行:在一段时间内,两个或多个以上线程,共用一个CPU,进行交替使用;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3LhvtNb-1638150182661)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622103859721.png)]

线程的并行:两个或者多个线程各占一个CPU,在同一段时间上运行;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EENqCG0i-1638150182664)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622103956170.png)]

如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来执行代码

如果计算机是多核CPU的话,那么同一时刻有可能是俩个线程同时使用不同的CPU执行代码

3.时间片

1.概述

时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间 就叫做时间片,也就是该线程允许使CPU运行的时间,在这个期间,线程拥有CPU的使用权。

如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用 权,然后等待下一个CPU时间片的分配。

所以,在宏观上,一段时间内,我们感觉俩个线程在同时运行代码,其实在微观中,这俩个线程在使用 一个CPU的时候,它们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使 用权,只是它们俩个交替运行的速度太快了,给我们的感觉,好像是它们俩个线程在同时运行。

2.调度

调度算法:

  • 时间片轮转调度算法;

    所有线程轮流使用CPU的使用权,平均分配每个线程称占用CPU的时间;

  • 抢占式调度算法:

    系统会让优先级高的线程先使用CPU(提高抢占的概率),但是如果优先级相同,那么会随机选择一个线程获取当前CPU。(具体还是看谁抢到了,谁就有使用权)

JVM中的线程,使用的是抢占式调度

例子:

public static void main(String[] args) {
    //创建线程对象t1
    Thread t1 = new Thread(){
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
            System.out.println("hello");
            }
        }
    };
    //创建线程对象t2
    Thread t2 = new Thread(){
    @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
            System.out.println("world");
            }
        }
    };
    //启动线程t1 t2
    t1.start();
    t2.start();
}

t1.和t2线程是公平竞争(优先级相同),抢到即使用CPU

所以hello 和world是交替出现的

而且每次运行打印的结果都会有所不同,因为是随机的;

4 main线程

使用 java 命令来运行一个类的时候,

  1. 首先会启动JVM(进程),JVM会在创建一个名字叫做main的线 程,来执行类中的程序入口(main方法);
public class Test {
    public static void main(String[] args) {
        //获取执行当前方法的线程对象
        Thread currentThread = Thread.currentThread();
        System.out.println("执行当前方法的线程名字为:"+currentThread.getName());
    }
}
//运行结果:
执行当前方法的线程名字为:main

所以,我们在写main方法中的代码,其实都是由名字叫做main的线程去执行的

Thread.currentThread():可以写在任意方法中,返回的就是执行该方法的线程对象

上面代码使用java命令运行的过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X14fC52L-1638150182667)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622140022296.png)]

详解:

  1. 使用java命令运行Test类,会先启动JVM;

  2. 应用类加载器通过CLASSPATH环境变量配置的路径,找到Test.class文件,并加载到方法区;

    注意,这里同时会生成一个Class类型对象,来代表这个Test类型,并且会优先处理类中的静态代码块(静态属性、静态方法、静态代码块);

  3. JVM创建并启动一个名字叫做main的线程;

  4. main线程将Test中的main方法加载到栈区中;

  5. 在栈里面,main线程就可以一行一行的执行方法中的代码了;

  6. 如果在执行代码中,遇到了方法调用,那么线程继续吧被调用的方法,加载到栈中(压栈操作),然后执行栈顶这个最新添加进来的方法,栈顶方法执行完,就释放(出栈操作),然后再进行执行当前最新的栈顶方法;

  7. 代码执行过程输出执行结果;

  8. 当前是单线程程序,main线程结束了,JVM就停止了,如果是多线程程序,那么JVM要等所有线程 都结束了才会停止

5 线程的创建和启动

java.lang.Thread 是java中的线程类,所有的线程对象都必须是Thread类或其子类的实例。

每个线程的作用,就是完成我们给他的指定任务,实际上就是执行一段我们指定的代码。我们只需要在Thread类的子类中重写run()方法即可,这就是线程的执行任务;

Java中通过继承Thread类来创建启动一个新的线程

步骤如下:

  1. 定义Thread类的子类(可以是匿名内部类),并重写Thread类中的run方法,run方法中的代码就是线程的执行任务;
  2. 创建 Thread 子类的对象,这个对象就代表了一个要独立运行的新线程
  3. 调用线程对象的 start 方法来启动该线程
public class Threa1 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i<10; i++){
                    System.out.println("hello world");
                    try {
                        Thread.sleep(1000*2);//2秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }

}
运行结果:
hello world//十次输出,每次输出间隔2s
hello world
hello world
hello world
hello world
hello world
hello world
hello world

在此过程中main线程和t线程之间的关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G7kV3HUg-1638150182671)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622185847481.png)]

可以看出,main线程在执行main方法的过程中,创建并启动了t线程,并且t线程启动之后,和main线程就没有关系了,这时候main线程和t线程是独立的,并且他们会抢占cpu的使用权;

内存情况:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hqYWjrz-1638150182674)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622190312570.png)]

  • 注意1,之前所提到的栈区,又被称为方法调用栈,是线程专门执行方法中代码的地方,并且每一个线 程,都有自己独立的栈空间,和别的线程相互不影响(线程独享)
  • 最先启动的是主线程(main线程),因为他要执行程序的入口main方法,在主线程中,创建并启动了t线程,启动之后main线程和t线程各自独立运行,并且相互争夺CPU时间片;
  • ,线程启动之后(调用start方法),会开始争夺CPU的时间片,然后自动执行run方法,如果子类对象重写了,那么就调用到重写后的run方法
  • ,堆区是对所有线程共享的,每个线程中如果创建了对象,那么对象就会存放到堆区中
  • 线程对象t被创建出来的时候,它还只是一个普通的对象,但是当调用了t.start()方法之后,线程对象t可以说才真正的“现出原形”:开辟了单独的栈空间,供线程t调用方法使用

线程可以共享内存:

  • 线程对象t被创建出来的时候,它还只是一个普通的对象,但是当调用了t.start()方法之后,线程对象t可以说才真正的“现出原形”:开辟了单独的栈空间,供线程t调用方法使用

  • t.start()启动线程,就会在内存中为自己分配一个栈

  • 即每一个线程都会又自己的线程栈,里面存放着自己的方法,如果创建了对象,每个对象会在堆区申请一块内存空间;

所以共享内存实际上就是线程共享堆区;

6 Runnable接口

给一个线程对象指定要执行的任务,除了继承Thread类后重写run方法之外,还可以利于Runnable 接口来完成线程任务的指定

java.lang.Runnable ,该接口中只有一个抽象方法 run

public interface Runnable {
	public abstract void run();
}

其实 Thread 类也是 Runnable 接口的实现类,其代码结构大致为:

public class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;
    public Thread() {
    	//...
    }
    public Thread(Runnable target) {
        this.target = target;
        //..
    }
    @Override
    public void run() {
        if (target != null) {
        	target.run();
        }
    }
}

可以看出,子类重写Thread中的run方法,这个run方法其实也来自于Runnable接口

通过以上的代码结构,可以知道,我们还可以直接创建 Thread 对象,在调用构造器的时候,传一个 Runnable 接口的实现类对象进来,然后调用线程对象的 run 方法,那么默认就会调用到Runnable接口 实现类重写的run方法!

例如,使用 Runnable 接口的匿名内部类,来指定线程的执行任务(重写接口中的run方法)

public class RunnableTest {
    public static void main(String[] args) {
        Runnable run=new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("hello world");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace(run);
                    }
                }
            }
        };
        Thread t=new Thread(run);
        t.start();
    }
}

可以看出,这种方式也可以完成之前相同的功能

实现Runnable接口比继承Thread类所具有的优势:

  • 可以避免单继承的局限性;
  • 可以把相同的一个执行任务,交给不同的进程对象去做;
  • 线程和执行代码各自独立,实现代码解耦;

7 线程的名字

通过Thread类中的currentThread方法,可以获取当前线程的对象,然后调用线程对象的getName方法, 可以获取当前线程的名字。 String name = Thread.currentThread().getName();

注意,这里说的当前线程,指的是执行当前方法的线程,因为获取线程名字的代码肯定是写在某个方法 中的,并且这个方法一定是由某个线程调用执行的,它们的关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8EyQIlxF-1638150182676)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210622192246888.png)]

public static void main(String[] args) {

    Runnable run=new Runnable() {
        @Override
        public void run() {
            String name=Thread.currentThread().getName();
            System.out.println("执行当前main方法的线程是:"+name);
        }
    };
    Thread t=new Thread(run);
    t.start();
    String name=Thread.currentThread().getName();
    System.out.println("执行当前run方法的线程是:"+name);
}
//运行结果为:
执行当前main方法的线程是:main
执行当前run方法的线程是:Thread-0

注意,一定要记得,start方法启动线程后,线程会自动执行run方法

千万不要直接调用run方法,这样就不是启动线程执行任务,而是普通的方法调用,和调用 sayHello没区别

默认情况下,主线程中,创建出的线程,它们的都会有一个默认的名字:

public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

其中, "Thread-" + nextThreadNum() 就是在拼接出这个线程默认的名字,Thread-0 Thread-1 Thread-2等等

我们也可以创建线程对象的时候,给它设置一个指定的名字:

Thread t = new Thread("t线程");

//或者
Thread t = new Thread(new Runnable(){
    public void run(){
    	//执行任务
    }
},"t线程");

//或者
Thread t = new Thread();
t.setName("t线程");

8 线程的分类

java中,线程分为:

  • 前台线程,又叫做执行线程,用户线程;
  • 后台线程,又叫做守护线程,精灵线程;

前台线程:这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前 台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!

例如,执行程序入口的主线程(main),就是一个前台线程,在单线程程序中,main方法执行完,就代 表main线程执行完了,这时候JVM就停止了

例如,我们在主线程创建并启动的新线程,默认情况下就是一个前台线程,用来执行用户编写的代码任 务。

后台线程:这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止 运行,根本不关心后台线程的运行情况和状态。

例如,垃圾回收器,其实就一个后台线程,它一直在背后默默的执行着负责垃圾回收的代码,为我们前 台线程在执行用户代码的时候,提供一个良好的内存环境。

9 线程优先级

线程类Thread中,有一个属性,表示线程的优先级,代码结果大致为:

public class Thread implements Runnable {
private int priority;
/**
* 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;
public final int getPriority() {
return priority;
}
public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}
    private native void setPriority0(int newPriority);
}

可以看出,最终设置线程优先级的方法,是一个native方法,并不是java语言实现的

线程的优先级使用int类型数字表示,最大值使10,最小值是1,默认的优先级是5;

当两个线程争夺CPU时间片的时候:

  • 优先级相同,获得CPU使用权的概率相同;
  • 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权
public class Test {
	public static void main(String[] args) {
        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10000; i++) {
                }
                System.out.println(name+"线程执行完毕");
            }
        };
        Thread t2 = new Thread("t2线程"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10000; i++) {
                }
                System.out.println(name+"线程执行完毕");
            }
        };
        // t1.setPriority(Thread.MAX_PRIORITY);
        // t2.setPriority(Thread.MIN_PRIORITY);
        System.out.println("t1线程的优先级:"+t1.getPriority());
        System.out.println("t2线程的优先级:"+t2.getPriority());
        t1.start();
        t2.start();
    }
}

注意1,默认情况下,俩个线程的优先级都是5,那个俩个线程争夺到CPU的使用权的概率一样,那么基 本上俩个线程都有相同的概率先执行完10000次循环

注意2,其实t1先稍微占了那么一点点的优势,因为毕竟在主线程的代码中,先启动了t1先,然后又启动 了t2线程

注意3,设置t1和t2优先级之后,在运行查看结果,会明显看到优先级高的线程,有更高的概率先执行完 代码

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

  • 设置程序的优先级只能在线程启动之前

10 线程组

Java中使用 java.lang.ThreadGroup 类来表示线程组,它可以对一批线程进行管理,对线程组进行操 作,同时也会对线程组里面的这一批线程操作。

java.lang.ThreadGroup :

public class ThreadGroup{
    public ThreadGroup(String name){
    	//..
    }
    public ThreadGroup(ThreadGroup parent, String name){
    	//..
    }
}

创建线程组的时候,需要指定该线程的名字;

也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。

例如

public static void main(String[] args) {
			//获取当前线程对象
            Thread currentThread=Thread.currentThread();
    		//主线程中,创建一个线程对象,它的线程组默认就是当前线程的线程组
            System.out.println(currentThread.getThreadGroup().getName());
			//获取当先线程所属的线程组
            ThreadGroup currThreadGroup=currentThread.getThreadGroup();

            ThreadGroup g1=new ThreadGroup("myfirst");//创建名为myfirst的线程组
            System.out.println(currThreadGroup.getName());
            System.out.println(currThreadGroup);
            System.out.println(g1.getName());
            System.out.println(g1);
        }
//运行结果:
main
main
java.lang.ThreadGroup[name=main,maxpri=10]
myfirst
java.lang.ThreadGroup[name=myfirst,maxpri=10]

可以看出,当前线程组的名字为main,并且线程组中的线程最大优先级可以设置为10

可以看出,主线程中,创建一个线程对象,它的线程组默认就是当前线程的线程组

public static void main(String[] args) {
    ThreadGroup group=new ThreadGroup("my threadgroup");

    Runnable run=new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    Thread t1=new Thread(group,run,"t1 thread");
    Thread t2=new Thread(group,run,"t2 thread");
    Thread t3=new Thread(group,"t3 thread");
    //注意,启动后,t1.t2两个线程都会进行休眠,等run方法运行完就“死亡”了
    t1.start();
    t2.start();
    t3.start();
    //返回当前线程组中还没有“死亡”的线程个数
    System.out.println(group.activeCount());
    //准备好数组,保存线程组中还存活的线程
    Thread[] threads=new Thread[group.activeCount()];//现在只是定义了长度,还没有填数据

    System.out.println("threads数组中存放的线程个数为:"+group.enumerate(threads));//将group线程组及其子组中的每个活动线程复制到指定的数组中。

    System.out.println("threads数组中的内容为:"+Arrays.toString(threads));
}
//运行结果:
3//t3线程还没有执行完
arr数组中存放的线程个数为:2//t3线程没有休眠,执行完就死亡了
arr数组中的内容为:[Thread[t1 thread,5,my threadgroup], Thread[t2 thread,5,my threadgroup]]

注意,只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变它所属的线 程组

11 线程状态

线程状态名称描述
NEW新建状态线程刚刚创建,到调用start方法之前,这段时间就是新建状态
RUNNABLE就绪状态 运行状态就绪:调用start方法后线程启用,这是就从NEW到RUNNABLE,这个时候线程是alive的,只有线程抢占到了CPU资源,才可以进入运行状态,这两种状态都属于RUNNABLE的;
BLOCKED锁阻塞线程A和线程B 都要执行test方法,而且test方法被加了锁,线程A先拿了锁去执行test方法,这是B需要等带线程A把锁释放,这时线程B就处于BLOCKED状态;
WAITING无限等待(阻塞)一个线程在等待另一个线程的唤醒动作,该线程就进入了无限等待状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify方法或者notifyAll方法才可以唤醒
TIMED_WAITING有限等待(阻塞)和WAITING状态类似,但是有一个时间期限,时间到了,自己也会主 动醒来
TERMINATED终止run方法执行结束线程就终止了

注意,其实BLOCKED,WAITING,TIME_WAITING这三种都属于线程阻塞,只是触发条件和从阻塞状态中恢复过来的条件不同而已

相同的特点:

  • 线程不参与CPU时间片的争夺;
  • 线程不执行代码;

线程的变化情况如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e435RlZ8-1638150182678)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623154842545.png)]

一个线程从创建到启动、到运行、到死亡,以及期间可能出现的情况都在上图中进行了描述。 在后面的学习中,会陆续认识到这里面的每一种情况!

public class state1 {
    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                }
            }
        };
        System.out.println(t1.getState());

        t1.start();
        for (int i=0;i<10;i++) {
            System.out.println(t1.getState());
        }
    }
}
//运行结果:注意需要多运行几次,因为可能每次运行的情况不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED

注意1,刚创建好的线程对象,就是出于NEW的状态

注意2,线程启动后,会出于RUNNABLE状态

注意3,其实这个RUNNABLE状态包含俩种情况: 就绪状态,此时这个线程没有运行,因为没有抢到CPU的执行权 运行状态,此时这个线程正在运行中,因为抢到CPU的执行权

注意4,JavaAPI中并没有定义就绪状态和运行状态,而是把这俩情况统一叫做RUNNABLE(可运行状态),但是一般我们为了能更加清楚的描述问题,会用上就绪状态和运行状态

注意5,在线程多次抢到CPU执行权,“断断续续”把run方法执行完之后,就变成了TERMINATED状态(死 亡),之所以是“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢夺下一个时间片,并且中间还有可能抢不到的情况

注意6,死亡后的线程,不能重新启动

和上面描述对应的状态图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wp29FDi9-1638150182680)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623155016515.png)]

从就绪状态到运行状态,之间会经过多次反复的CPU执行权的争夺(线程调度)

这就是一个线程经历的最基本的状态变化

其他的状态都是线程在Running的时候,线程中调用了某些方法,或者触发了某些条件,导致这个 线程进入到了阻塞状态(上面介绍的三种阻塞情况)

12 sleep()方法

线程类Thread中的 sleep 方法:

public static native void sleep(long millis) throws InterruptedException;
public static void main(String[] args) {
    Thread t1=new Thread(){
        @Override
        public void run() {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    System.out.println(t1.getState());

    t1.start();
    for (int i=0;i<500;i++) {
        System.out.println(t1.getState());
    }
}
//运行结果: 可以多运行几次,每次结果可能不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
.....
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED
TERMINATED
TERMINATED
.....

可以看出调用sleep方法后,会从RUNNABLE状态转换成TIMED_WAITING;

这时候线程处于阻塞状态;

有限等待阻塞:阻塞状态结束,会自动进入RUNNABLE状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GkcSONVC-1638150182682)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623161651386.png)]

2 join方法

线程类Thread中的 join 方法:

public final synchronized void join(long millis)throws InterruptedException{
	//...
}
public final void join() throws InterruptedException{
	//...
}

使用join方法,可以让当前线程阻塞,等待另一个指定的线程(调用者)运行结束后,当前线程才可以继续运行:

例如,使用无参的join方法

package com.chapter10.ThreadTest;

public class joinTest {
    public static void main(String[] args) {
        Thread t1=new Thread("t1线程"){
            @Override
            public void run() {
                System.out.println("t1线程开始");
                try {
                    //t1线程睡眠1秒钟
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };
        Thread t2=new Thread("t2线程"){
            @Override
            public void run() {
                System.out.println("t2线程开始");
                try {
                    //t2线程进入阻塞状态
                    //t2线程要等带t1线程结束后才能恢复到RUNNABLE状态
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程结束");
            }
        };
        t1.start();
        t2.start();

        //让主线程休眠1000毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t2.getName()+"状态:"+t2.getState());

    }
}
//运行结果:
t2线程开始
t1线程开始
t1线程结束
t2线程结束
t2线程状态:WAITING//main线程打印输出的

t2线程中,调用了t1对象的join方法,那么t2线程就会阻塞,等待t1线程的运行结束,t2线程才能恢 复

可以看出,线程执行了join()方法后,会从RUNNABLE状态进入到WAITING状态

package com.chapter10.ThreadTest;

public class joinTest {
    public static void main(String[] args) {
        System.out.println("main start");
        Thread t1=new Thread("t1线程"){
            @Override
            public void run() {
                System.out.println("t1线程开始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };
        Thread t2=new Thread("t2线程"){
            @Override
            public void run() {
                System.out.println("t2线程开始");
                try {
                    //t2线程只等t1线程500ms,之后t2线程就会开始争夺资源
                    t1.join(500);
                    System.out.println("t2 is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程结束");
            }
        };
        t1.start();
        t2.start();

        for (int i=0;i<5;i++){
            System.out.println(t2.getName()+"状态:"+t2.getState());
        }
        System.out.println("main end");

    }
}
//运行结果
main start
t1线程开始
t2线程开始
t2线程状态:RUNNABLE//状态都是main线程打印输出
t2线程状态:TIMED_WAITING
t2线程状态:TIMED_WAITING
t2线程状态:TIMED_WAITING
t2线程状态:TIMED_WAITING
main end
t2 is running
t2线程结束
t1线程结束

可以看出,线程执行了join(long million)方法后,会从RUNNABLE状态进入到TIMED_WAITING状态(期间也有可能出现BLOCKED),

之前join()方法是的t2线程只有等t1线程结束之后才可以执行,所以总是在t1结束之后结束

此时的状态图为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIRWL2e7-1638150182685)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623164332963.png)]

线程A中,调用了线程B对象的join方法,那么线程A就会进入到阻塞状态,这种阻塞有俩种情况,一种是 调用了无参join方法,那么此时线程A的状态为WAITING(无限期等待),另一种是调用了有参join方 法,那么此时线程A的状态为TIMED_WAITING(有限期等待)

线程A如果调用了sleep方法,那么线程A也会进入阻塞状态,此时线程A的状态为TIMED_WAITING

总结:

  • 如果指定了时间,线程阻塞一定的时间后,会自动恢复到RUNNABLE状态,这种情况下,线程的状 态为TIMED_WAITING(有限期等待)
  • 如果没有指定时间,线程会一直阻塞着,直到某个条件满足时,才会自动恢复,这种情况下,线程 的状态为WAITING(无限期等待)

在这种情况下,其实还有另一种方式,可以让线程从阻塞状态恢复到RUNNABLE状态,那就是调用线程的 interrupt 方法

3 interrput方法

interrupt()方法

线程类Thread中的 interrupt 方法:

//Interrupts this thread
public void interrupt(){
//...
}

根据上面介绍 sleep 方法和 join 方法可知,这俩个方法都会抛出 InterruptedException 类型的异 常,说明调用 sleep 和 join 使线程进入阻塞状态的情况下,是有可能抛出 InterruptedException 类型的异常的。

InterruptedException 异常类型指的是:

​ 线程A中,调用了线程B的 interrupt 方法,而此时线程B 处于阻塞状态,那么此时 sleep 方法或者 join 方法就会抛出被打断的异常;

public static void main(String[] args) {
        Thread t1 = new Thread("t1线程") {
            @Override
            public void run() {
                try {
//t1线程休眠100秒
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };
        t1.start();
//让主线程休眠500毫秒,目的是为了给t1时间,让它调用sleep方法而进入阻塞状态
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//打断t1由于调用sleep方法而进入的阻塞状态
        t1.interrupt();
    }
//运行结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.briup.sync.Test$1.run(Test.java:11)
t1线程结束

可以看出,本来t1线程调用了sleep方法进入了阻塞状态,需要100后才会恢复的,但是我们在主线 程中调用了t1线程对象的打断方法 interrupt ,那么此时 Thread.sleep``(100000); 这句代码就 会抛出被打断的异常,同时t1线程从阻塞状态恢复到RUNNABLE状态,继续执行代码,输出了t1线 程结束

此时对于的流程图为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKTrEMI1-1638150182688)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623184053252.png)]

interrupt方法的工作原理:

interrupt方法是通过改变线程对象中的的一个标识的值(flag:true|false),来达到打断阻塞状态的效果的;

一个线程在阻塞状态下,会时刻检测这个标识是不是true,如果一旦发现这个值变成了true,那么就抛出异常结束阻塞状态,并再把这个值改成false

从Thread类的源码中可以看到:

interrupt 方法中其实是调用了 interrupt0 这个本地方法,而interrupt0的注释为:Just to set the interrupt flag

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

可以看出,interrupt方法只是改变了线程对象中的一个flag的值

查看线程对象中“打断标识”值的俩个方法:

isInterrupted ()方法:

public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* 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)

注意,这个非静态方法,只是返回这个“打断标识”值,并且不会对这个值进行清除(true- >false),因为所传参数ClearInterrupted的值为false

例如,默认情况下,一个线程对象中的“打断标识”值为false

public static void main(String[] args) {
        Thread t=new Thread("t线程"){
            @Override
            public void run() {
                System.out.println("t线程开始");
                for (int i=0;i<10;i++){
                    System.out.println(this.isInterrupted());
                }
                System.out.println("t线程结束");
            }
        };
        t.start();
    }
//运行结果
t线程开始
false
false
false
false
false
false
false
false
false
false
t线程结束

无论线程是否处于阻塞状态,其他线程都可以调用这个线程的 interrupt 方法,因为该方法只是 改变线程对象中“打断标识”值而已

public static void main(String[] args) {
    Thread t=new Thread("t线程"){
        @Override
        public void run() {
            System.out.println("t线程开始");
            for (int i=0;i<10;i++){
                //判断是否有其他线程调用了自己的interrupt方法
//调用类中的非静态方法:isInterrupted
                System.out.println(this.isInterrupted());
            }
            System.out.println("t线程结束");
        }
    };
    t.start();
    t.interrupt();
}
//运行结果:
t线程开始
true
true
true
true
true
true
true
true
true
true
t线程结束

可以看出,调用了 t.interrupt(); 后,t1线程中的“打断标识”值设置为了true,可以通过线程对 象中的 isInterrupted 方法返回这个标识的值,并且不会修改这个值,所以输出显示的一直是 ture

interrupted() 方法:

线程类Thread中的 interrupted 方法:

public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* 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);

注意,这个静态方法,返回这个“打断标识”值,并且会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为true

public static void main(String[] args) {
    Thread t=new Thread("t线程"){
        @Override
        public void run() {
            System.out.println("t线程开始");
            for (int i=0;i<10;i++){
                System.out.println(this.interrupted());
            }
            System.out.println("t线程结束");
        }
    };
    t.start();
    t.interrupt();
}
//运行结果
t线程开始
true
false
false
false
false
false
false
false
false
false
t线程结束

小结

Thread类中的三个方法: interrupt()、isInterrupted()、interrupted() 的结构关系大致如下:

public class Thread{
public void interrupt() {
//...
interrupt0(); // Just to set the interrupt flag
}
private native void interrupt0();
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* 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);
public static native Thread currentThread();
}
  • interrupt():中断方法;

线程处于阻塞状态(sleepjoin

默认false改成true,后面又会改回false;

线程没有阻塞,调用该方法

默认false改成true,不会改回false;

  • interrupted()静态方法:会改变flag参数(改回默认false),只输出一次true,后面的多次调用输出的都属false;

  • isInterrupted()非静态方法:不会改变flag参数;

4 线程安全

JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。

JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区 的数据。

我们之前编写的代码只有一个main线程,只有它自己去访问堆区中对象的数据,自然不会有什么问题。

但是在多线程环境中,如果有俩个线程并发访问堆区中一个对象中的数据,那么这个数据可能会出现和 预期结果不符的情况,例如

package com.chapter10.ThreadTest;

public class SafetyThread {
    public static void main(String[] args) {
        MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
//先给num赋值
                    myData.num = i;
//然后再输出num的值
                    System.out.println(name + ": " + myData.num);
                }
            }
        };
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
              
                for (int i = 100; i < 2000; i++) {
//先给num赋值
                    myData.num = i;

                }
            }
        };
        t1.start();
        t2.start();
    }
}
class MyData{
     int num;
}
//运行结果:
t1: 1999
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
t1: 6
t1: 7
t1: 8
t1: 9

可以看到,每次运行的结果中,t1线程输出的num的值可能和预期都不一样

此时的内存图为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTgl15Yl-1638150182692)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210623194214450.png)]

  • 注意,t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一次的时间片,也就是说t1和t2都是“断断续续”运行的;
  • 这里t2先拿到了时间片,并且线程先执行完了,此时num为1999,然后t1 又拿到了时间片,又将num赋值成了i;进行循环遍历,本来输出0的,但在打印之前,但是由于t2抢占了时间片,并且完成了赋值,在回到t1 的时候num已经是1999,此时下一步就是输出num;

引用类型:栈里面放引用,数据放在堆区,通过引用修改堆区的数据;

基本类型:数据存放在堆区,会复制一份放在栈区,直接在栈区修改;

核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num, 等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况

如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可 能会出数据的结果和预期不符的情况,这种情况就是线程安全问题。

我们经常会进行这样的描述:这段代码是线程安全的,那段代码是非线程安全的。 其实就是在说,这段代码在多线程并发访问的环境中,是否会出现上述情况,也就是结果和预期不符的情况。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
        @Override
            public void run() { 
                String name = Thread.currentThread().getName();
                //每次循环改变变量i的值
                for (int i = 0; i < 10; i++) {
                //输出变量i的值
                System.out.println(name + ": " + i);
            }
    	}
    };
    Thread t2 = new Thread("t2"){
        @Override
        public void run() {
            //每次循环改变变量i的值
            for (int i = 100; i < 20000; i++) {
            }
        }
    };
    t1.start();
    t2.start();
    }
}

每次执行结果都是一样的,和预期的相同,因为t1和t2俩个线程根本就没访问同一个共享变量!相 当于t1和t2都是各自操作各自的变量i

5 线程同步

当使用多个线程访问同一个共享变量的时候,并且线程中对变量有写的操作,这时就容易出现线程安全 问题。

Java中提供了线程同步的机制,来解决上述的线程安全问题。

Java中实现线程同步的方式,是给需要同步的代码进行 synchronized 关键字加锁。

例如,改造之前有线程安全问题的代码,给需要同步的代码使用 synchronized 加锁

package com.chapter10.ThreadTest;

public class SafetyThread {
    public static void main(String[] args) {
        MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (myData) {
                    for (int i = 0; i < 10; i++) {
                        //先给num赋值
                        myData.num = i;
                        //然后再输出num的值
                        System.out.println(name + ": " + myData.num);
                    }
                }
            }
        };
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                synchronized (myData) {
                    for (int i = 100; i < 2000; i++) {
                        //先给num赋值
                        myData.num = i;
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }
}
class MyData{
     int num;
}

现在运行这个程序,每次输出的结果都是和预期的一样:t1线程每次输出的值都是0~9

分析: 线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的 锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。

这样以来,本来这段代码是俩线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就 变成了先由一个拿到锁的线程去执行,执行完了,再由另一个线程拿到锁去执行。

相当于是大家每个线程不要抢,排好队一个一个去执行,那么这时候共享的变量的值,肯定不会出现线 程安全问题!

例如, synchronized 修饰代码块的使用格式为:

synchronized (锁对象){
    //操作共享变量的代码,这些代码需要线程同步,否则会有线程安全问题
    //...
}

对应这样的加锁代码,如果两个线程进行并发访问的话:

  • 假设t1线程是第一个这段代码的线程,那么他会率先拿到这把锁,其实就是在这个锁对象中写入自己线程的信息,相当于告诉其他线程,这把锁时我的,你们现在都不能用;
  • 这时候t1线程拿着锁,就可以进入到加锁的代码块中,去执行代码,执行很短的一个时间片,然后 退出,但是锁并不释放,也就意味着,即使下次是t2线程抢到CPU的使用权,它也无法运行代码, 因为t2没有拿到锁。
  • 就这样,t1线程开心的拿着锁,抢到CPU的执行权,抢到了就去执行,抢不到也不用担心,因为没 有其他线程可以“偷偷”的执行这段代码,因为其他线程拿不到锁。
  • 而对于t2线程来说,即使有一次抢到了CPU执行权,来到了代码面前,要执行的时候才发现,锁被 t1线程拿走了,自己无法进入代码块中执行,这时候t2线程就会从运行状态进入阻塞状态直到t1 运行完,把锁释放了,t2线程才会恢复到RUNNABLE状态,抢到CPU执行权,再拿到锁,然后进入代 码块中执行

注意,这时候t2线程的阻塞状态,和之前学习的调用sleep或join方法进入的阻塞不同,这种阻塞属于锁 阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对 象的 getState 方法返回的状态名称为:BLOCKED

BLOCKED:加了锁但是没有获取到锁就会锁阻塞

package com.chapter10.ThreadTest;

public class synTest {
    public static void main(String[] args) {
        Object object = new Object();

        Thread t1=new Thread("t1线程"){
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t2=new Thread("t2线程"){
            @Override
            public void run() {
                synchronized (object){

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

        for (int i = 0; i <10 ; i++) {
            System.out.println(t1.getName() + "" + t1.getState());
        }
        for (int i = 0; i <10 ; i++) {
            System.out.println(t2.getName() + "" + t2.getState());
        }

    }
}
//运行结果
t1线程RUNNABLE
t1线程TIMED_WAITING
.......
t1线程TIMED_WAITING
t1线程TIMED_WAITING
......
t2线程BLOCKED
t2线程BLOCKED

注意:

  • 这里t1线程要拿锁对象object,才能运行加锁代码块,同样的t2线程也要拿锁对象object,才能运行加锁代码块;
  • 这里t1线程先拿到了锁对象object,然后执行了sleep()方法,由RUNNABLE状态进入了TIMED_WAITING状态;在此期间,t1失去了cpu时时间片,t2争夺到了cpu时间片,但是由于t2加了锁,必须拿到锁对象才可以运行;
  • 锁对象object只有一个,所以t2进入BLOCKED阻塞状态,称为锁阻塞,只有等到t1线程执行完了,释放了锁对象,t2线程才可以运行;

此时的线程状态图为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0ITZAuj-1638150182694)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210624144438386.png)]

可以看出,假设t1线程拿到了锁,t2线程没拿到锁,那么t2线程就会因为锁不可用,进入到锁阻塞 状态,直到t1先把加锁的代码执行完,把锁释放了,锁变的可用了,这是t2线程会自动恢复到 RUNNABLE状态

注意,t1线程“拿到”锁,只是一种形象的说,就是我们之前说的 引用“指向”对象一样。其实是t1线 程把自己的信息写入到了锁对象中,用这种方式告诉其他线程,这个锁对象已经被我 "拿走"了

6 synchronized

synchronized作用位置:

  • 修饰一个代码块,并指定谁是锁对象;
  • 修一个方法,表示着方法中的代码都需要线程同步;
    1. synchronized 关键字修饰非静态方法,默认使用 this 当做锁对象,并且不能自己另外指定;
    2. synchronized 关键字修饰静态方法,默认使用 当前类的Class对象 当做锁对象,并且不能自己另外指 定
    3. 以上两种情况只是锁对象不同而已;

例子:

public class Test {
    public static void main(String[] args) {
    MyData myData = new MyData();
    Thread t1 = new Thread("t1"){
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
            	myData.add(i);
                //计算机运行10次运行太快了,让它执行慢一些,好观察效果
                try {
                	Thread.sleep(1);
                } catch (InterruptedException e) {
               	 	e.printStackTrace();
                }
            }
        }
    };
    //t2线程的名字前面加个制表符\t,打印的时候好观察
    Thread t2 = new Thread("\tt2"){
        @Override
        public void run() {
            for (int i = 10; i < 20; i++) {
                myData.add(i);
                //计算机运行10次运行太快了,让它执行慢一些,好观察效果
                try {
                	Thread.sleep(1);
                } catch (InterruptedException e) {
                	e.printStackTrace();
                }
            }
        }
    };
    t1.start();
    t2.start();
	}
}
//运行结果:
t1线程本次写入的值为0,写入后取出的值为0
t2线程本次写入的值为10,写入后取出的值为10
t2线程本次写入的值为11,写入后取出的值为1
t1线程本次写入的值为1,写入后取出的值为1
t2线程本次写入的值为12,写入后取出的值为2
t1线程本次写入的值为2,写入后取出的值为2
t2线程本次写入的值为13,写入后取出的值为3
t1线程本次写入的值为3,写入后取出的值为3
t2线程本次写入的值为14,写入后取出的值为4
t1线程本次写入的值为4,写入后取出的值为4
t1线程本次写入的值为5,写入后取出的值为5
t2线程本次写入的值为15,写入后取出的值为5
t2线程本次写入的值为16,写入后取出的值为16
t1线程本次写入的值为6,写入后取出的值为16
t1线程本次写入的值为7,写入后取出的值为17
t2线程本次写入的值为17,写入后取出的值为17
t1线程本次写入的值为8,写入后取出的值为18
t2线程本次写入的值为18,写入后取出的值为18
t1线程本次写入的值为9,写入后取出的值为19
t2线程本次写入的值为19,写入后取出的值为19

可以看出,在某一次add方法执行的时候,会出现写入的数据和当前的数据不一致的情况

此时,我们可以直接在add方法(非静态方法)上,添加修饰符 synchronized 关键字,表示给这个方 法中的所有代码进行线程同步,默认使用的锁对象是 this

public synchronized void add(int num){
    String name = Thread.currentThread().getName();
    arr[current] = num;
    System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);
    current++;
}

此时,再运行代码,就不会出现之前那种线程安全问题了 该代码表示,拿到锁对象this的线程,才可以进入到add方法中执行代码,代码执行完,会释放 锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状 态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去 执行

注意,此时t1线程中调用add方法,争夺的锁对象this就是myData对象,t2线程中调用的add方法,争夺 的锁对象this也是myData对象,所以t1和t2俩个线程争夺的是同一把锁对象,那么就能达到线程同步的效 果!

所以,线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象

思考,根据上面的例子,考虑为什么之前学习的ArrayList中的add方法是非线程安全的,而Vector中的 add方法是线程安全的?

说明加了锁啊

7 wait()和 notify()线程通信

Object类中有三个方法: wait()、notify()、notifyAll

当一个对象,在线程同步的代码中,充当锁对象的时候,在 synchronized 同步的代码块中,就可以调 用这个锁对象的这三个方法了。

三个核心点:

  • 任何对象中都一定有这三个方法(Object中有);
  • 只有对象作为锁对象的时候,才可以调用
  • 只有在同步的代码块中(加了锁),才可以调用

其他情况下,调用一个对象的这三个方法,都会报错!

synchronized 关键字,虽然可以达到线程同步的效果,但是太“霸道”了,只要一个线程拿到了锁对 象,那么这个线程无论是在运行状态,还是时间片用完,回到就绪状态,还是sleep休眠,这个线程都是 死死的拿着这个锁对象不释放,只有这个线程把线程同步的代码执行完,才会释放锁对象让别的线程使 用。

wait()方法被调用后进入等待阻塞,同时会释放锁资源,好让别的线程获取锁资源;

案例

package com.chapter10.ThreadTest;

public class wait {
    public static void main(String[] args) {
        final Object obj = new Object();
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        System.out.println(name+"线程: i = "+i);
                    }
                }
            }
        };
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        System.out.println(name+"线程: j = "+j);
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }
}

t1和t2俩个线程,争夺同一把锁对象obj,所以程序的运行结果是:要么t1先拿到锁输处0~9,然后 t2再拿到锁输出1019,要么就是t2先拿到锁输入1019,然后t1再拿到锁输出0~9

现在,我们希望的是t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释 放锁,让t1拿到锁去运行:

在代码中加入条件判断和wait方法的调用

package com.chapter10.ThreadTest;

public class wait {
    public static void main(String[] args) {
        final Object obj = new Object();
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        if (i==5){
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(name+"线程: i = "+i);
                    }
                }
            }
        };
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        if(j==15){
                            try {
                                //obj是锁对象,在同步代码块中,可以调用wait方法
                                //让当前拿到锁的线程,立即释放锁
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                        System.out.println(name+"线程: j = "+j);
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }
}
//运行结果
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14

可以看到,t1线程和t2线程都没有运行完,但是代码不运行了,JVM也没停住

这是因为,当前调用锁对象的wait方法后,当前线程释放锁,然后进入到阻塞状态,并且等待其他线程 先唤醒自己,如果没有其他线程唤醒自己,那么就一直等着。所以现在的情况是,俩个线程t1和t2都是 在处于阻塞状态,等待别人唤醒自己,所以程序不运行了,但是也没结束!

此时线程的状态图为:

**[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ws78Cvy5-1638150182696)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210624154701169.png)]

可以看出,此时线程调用了wait方法,释放了锁,变为阻塞状态(WAITING),并进入了等待池, 等待其他线程唤醒自己或者打断自己,如果有线程调用了notify方法进行了唤醒,或者interrupt方 法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象 是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE状态。

由图可知,TIMED_WAITING、WAITING、BLOCKED都属于线程阻塞,他们共同的特点是就是线程不执行代 码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要)

  • 阻塞1,线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE 状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了
  • 阻塞2,线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条 件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
  • 阻塞3,线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复 到BLOCKED状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify 方法)

理解上述的状态变化过程后,我们修改代码,加入notify方法的调用:

package com.chapter10.ThreadTest;

public class wait {
    public static void main(String[] args) {
        final Object obj = new Object();
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        if (i==5){
                            try {
                                obj.notify();
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(name+"线程: i = "+i);
                    }
                    obj.notify();
                }
            }
        };
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        if(j==15){
                            try {
                                //obj是锁对象,在同步代码块中,可以调用wait方法
                                //让当前拿到锁的线程,立即释放锁
                                obj.notify();
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                        System.out.println(name+"线程: j = "+j);
                    }
                }
            }
        };
        t1.start();
        t2.start();

        for (int i = 0; i <5 ; i++) {
            System.out.println(t1.getName() + ":" + t1.getState());
        }
        for (int i = 0; i < 5; i++) {
            System.out.println(t2.getName() + ":" + t2.getState());
        }
    }
}
//运行结果
t1线程: i = 0
t1:RUNNABLE
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1:RUNNABLE
t1:WAITING
t1:WAITING
t1:WAITING
t2:BLOCKED
t2:BLOCKED
t2:BLOCKED
t2:BLOCKED
t2:BLOCKED
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t1线程: i = 5
t1线程: i = 6
t1线程: i = 7
t1线程: i = 8
t1线程: i = 9
t2线程: j = 15
t2线程: j = 16
t2线程: j = 17
t2线程: j = 18
t2线程: j = 19

可以看到,此时t1和t2俩个线程都执行完了,打印输出的结果也符合我们的预期

锁对象.notify(),该方法可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进 入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了

锁对象.notifyAll(),该方法可以在等待池中,唤醒所有等待指定锁对象的线程,使得这个线程进入 到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了

8 死锁

在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生 死锁的问题代码

简单的描述死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释 放,俩个线程就这样一直僵持下去。

public class ThreadDeadLock extends Thread{
    private Object obj1;
    private Object obj2;
    public ThreadDeadLock(Object obj1,Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }
    public void run() {
        String name = Thread.currentThread().getName();
        if("Thread-0".equals(name)){
            while(true){
                synchronized (obj1) {
                    synchronized (obj2) {
                    	System.out.println(name+" 运行了..");
                	}
            	}
        	}
    	}
        else{
            while(true){
                synchronized (obj2) {
                    synchronized (obj1) {
                        System.out.println(name+" 运行了..");
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Thread t1 = new ThreadDeadLock(obj1,obj2);
        Thread t2 = new ThreadDeadLock(obj1,obj2);
        t1.start();
        t2.start();
    }
}

注意,可以通过jconsole查看到线程死锁的情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值