Singerw's Repository Singerw's Repository
首页
  • 相关文章

    • HTML相关文章
    • CSS相关文章
    • JavaScript相关文章
  • 学习笔记

    • JavaScript笔记
    • ES6笔记
    • Vue笔记
  • 相关文章

    • Spring相关文章
    • SpringBoot相关文章
    • MyBatis相关文章
    • MySQL相关文章
  • 学习笔记

    • SpringBoot笔记
    • Spring笔记
    • MyBatis笔记
    • MySQL笔记
    • JavaWeb笔记
    • JavaCore笔记
  • 学习笔记

    • Linux笔记
    • Git笔记
    • 技术文档
  • 偏门技术

    • GitHub技巧
    • 博客搭建
    • 科学上网
  • 安装教程

    • JDK
    • MySQL
    • Node.js
    • Linux
  • 终身学习
  • 面试人生
  • 心情杂货
  • 生活随笔
  • 归档
  • 标签
GitHub (opens new window)

Singerw

谁能够凭爱意将富士山私有
首页
  • 相关文章

    • HTML相关文章
    • CSS相关文章
    • JavaScript相关文章
  • 学习笔记

    • JavaScript笔记
    • ES6笔记
    • Vue笔记
  • 相关文章

    • Spring相关文章
    • SpringBoot相关文章
    • MyBatis相关文章
    • MySQL相关文章
  • 学习笔记

    • SpringBoot笔记
    • Spring笔记
    • MyBatis笔记
    • MySQL笔记
    • JavaWeb笔记
    • JavaCore笔记
  • 学习笔记

    • Linux笔记
    • Git笔记
    • 技术文档
  • 偏门技术

    • GitHub技巧
    • 博客搭建
    • 科学上网
  • 安装教程

    • JDK
    • MySQL
    • Node.js
    • Linux
  • 终身学习
  • 面试人生
  • 心情杂货
  • 生活随笔
  • 归档
  • 标签
GitHub (opens new window)
  • Java 核心语法
  • Java 流程控制
  • Java 数组
  • Java 面向对象
  • Java 集合框架
  • Java 深入面向对象
  • Java 常用类(API)
  • Java 内部类
  • 深入理解 Java 异常
  • Java IO流
  • Java 多线程
    • 一、线程简介
      • 1. 1 什么是进程
      • 1.2 什么是线程
      • 1.3 进程和线程的区别
      • 1.4 多线程优点
    • 二、创建线程的方式
      • 2.1 使用Thread类
      • 2.2 使用Runnable实现线程
    • 三、线程常用方法
      • 3.1 currentThread
      • 3.2 线程休眠和线程礼让
      • 3.3 守护线程
      • 3.4 线程通信
      • 3.5 优先级
    • 四、线程安全问题
      • 4.1 模拟售票案例
    • 五、线程生命周期
    • 六、 线程常见问题
      • 6.1. sleep、yield、join 方法有什么区别
      • 6.2. 为什么 sleep 和 yield 方法是静态的
      • 6.3. Java 线程是否按照线程优先级严格执行
      • 6.4. 一个线程两次调用 start()方法会怎样
      • 6.5. start 和 run 方法有什么区别
      • 6.6. 可以直接调用 Thread 类的 run 方法么
      • 6.7 集合中支持线程同步的类有哪些?
  • Java 网络编程
  • Java 设计模式
  • 深入了解序列化
  • DBUtil 手写工具类
  • 《JavaCore》学习笔记
Singerw
2021-08-22

Java 多线程

# Java多线程

转载于: javacore(opens new window) (opens new window)

并加入本人总结,文章如下:

# 一、线程简介

# 1. 1 什么是进程

简言之,进程可视为一个正在运行的程序。它是系统运行程序的基本单位,因此进程是动态的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。

# 1.2 什么是线程

线程是操作系统进行调度的基本单位。线程也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

# 1.3 进程和线程的区别

线程与进程:

  • 进程
    • 系统运行程序的基本单位
    • 有独立的内存空间和系统资源
  • 线程
    • 进程中执行运算的最小单位
    • 处理器分配给线程真正在处理器上运行的是线程
  • 一个程序至少有一个进程,一个进程至少有一个线程。
  • 线程比进程划分更细,所以执行开销更小,并发性更高。
  • 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。

# 1.4 多线程优点

  • 可以更好的实现并行
  • 恰当地使用线程时,可以降低开发和维护的开销,并且能够提高复杂应用的性能。
  • CPU在线程之间开关时的开销远比进程要少得多。因开关线程都在同一地址空间内,只需要修改线程控制表或队列,不涉及地址空间和其他工作。
  • 创建和撤销线程的开销较之进程要少。

Java中线程存在以下几种状态(后续将详细讲解对应代码):

新线程:

当利用new关键字创建线程对象实例后,它仅仅作为一个对象实例存在,JVM没有为其分配CPU时间片和其他线程运行资源

就绪状态:

在处于创建状态的线程中调用start方法将线程的状态转换为就绪状态。这时,线程已经得到除CPU时间之外的其它系统资源,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会

运行状态:

就绪态的线程获得CPU就进入运行态

等待/阻塞:

线程运行过程中被剥夺资源或者,等待某些事件就进入等待/阻塞状态, suspend()方法被调用 , sleep()方法被调用,线程使用wait()来等待条件变量;线程处于I/O等待等,调用suspend方法将线程的状态转换为挂起状态。这时,线程将释放占用的所有资源,但是并不释放锁,所以容易引发死锁,直至应用程序调用resume方法恢复线程运行。等待事件结束或者得到足够的资源就进入就绪态

死亡状态:

当线程体运行结束或者调用线程对象的stop方法后线程将终止运行,由JVM收回线程占用的资源

# 二、创建线程的方式

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口

# 2.1 使用Thread类

  1. 定义 Thread 类的子类,并覆写该类的 run 方法。run 方法的方法体就代表了线程要完成的任务,因此把 run 方法称为执行体。
  2. 创建 Thread 子类的实例,即创建了线程对象。
  3. 调用线程对象的 start 方法来启动该线程。
构 造 方 法 说 明
Thread() 创建一个新的线程
Thread(String name) 创建一个指定名称的线程
Thread(Runnable target) 利用Runnable对象创建一个线程,启动时将执行该对象的run方法
Thread(Runnable target, String name) 利用Runnable对象创建一个线程,并指定该线程的名称

【步骤】: 继承Thread类,重写其run方法: 创建这个类对象,调用其start方法;

public class Thread
extends Object
implements Runnable
1
2
3

【示例】:

  1. 创建一个类继承Thread,重写run方法
  2. 创建线程对象
  3. 启动线程
package com.singerw.threaddemo;

public class TestThread01{

    public static void main(String[] args) {
        // 2.创建线程对象
        Thread01 t1 = new Thread01();
        Thread02 t2 = new Thread02();

        // 3.启动线程
        t1.start();
        t2.start();
    }
}

/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 11:04
 * @Description: //TODO 1、创建一个类继承Thread,重写run方法
 */
class Thread01 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Thread01");
        }
    }
}
/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 11:04
 * @Description: //TODO 1、创建一个类继承Thread,重写run方法
 * 重写run方法
 */
class Thread02 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Thread02");
        }
    }
}
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

问:启动一个线程调用的为什么是start而不是run方法?

答:Start方法是线程的方法,这个方法会调用start0方法,start0是一个native方法,JVM会自动执行,此时才是一个多线程的执行方法;而run方法直接调用,就是一个普通方法的执行而已.

问:是否一个线程的start执行后,线程就处于运行状态呢?

答:Start()的方法执行之后,我们认为这个线程以及处于就绪状态了,具体什么时候执行取决于CPU(JVM)合时分配资源给他(线程什么时候能抢到了资源就执行)

# 2.2 使用Runnable实现线程

直接继承 Thread 类实现线程的方法存在局限性:由于Java是典型的单亲继承体系,因此一旦类继承Thread之后就不能再继承其他父类,对于一些必须通过继承关系来传播的特性这种方式显然会造成困扰

在上述情况下,可以通过实现java.lang.Runnable接口的方式来实现线程:

  • Runnable中只有一个签名和Thread中一致的run()方法,满足函数式接口的要求,可以使用Lambda表达式
  • Runnable接口的子类并不是线程类,我们只是通过这种形式向线程类提供run()方法指令代码,最后还需借助Thread类和Runnable接口的依赖关系和Thread类的Thread(Runnable runnable)构造方法构建线程对象

实现 Runnable 接口优于继承 Thread 类,因为:

  • Java 不支持多重继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 Thread 类就无法继承其它类,这不利于扩展。
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

通过实现 Runnable 接口创建线程的步骤:

  1. 定义 Runnable 接口的实现类,并覆写该接口的 run 方法。该 run 方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  3. 调用线程对象的 start 方法来启动该线程。

# 1、 独立类:

  • 定义 Runnable 接口的实现类,并覆写该接口的 run 方法。该 run 方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  • 调用线程对象的 start 方法来启动该线程。
package com.singerw.runable;

public class RunnableTest01 {
    public static void main(String[] args) {
        //2-  使用Runnable接口实现线程的时候,如果启动线程,需要借助Thread
        //Runnable01 类对象
        Runnable01 r1 = new Runnable01();
        //3->创建Thread类对象
        Thread th = new Thread(r1);
        //4->启动线程 处于就绪状态
        th.start();
        //简单写法
        new Thread(new Runnable02()).start();
    }
}

/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 14:40
 * @Description: //TODO 1->创建实现类实现Runnable ,重写run方法
 */
class Runnable01 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Runnable01 ");
        }
    }
}

class Runnable02 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Runnable02 ");
        }
    }
}
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

# 2、匿名内部类:

package com.singerw.runable;

/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 14:10
 * @Description: //TODO 类描述
 */
public class RunableTest02 {

    public static void main(String[] args) {

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("run....");
            }
        };
        new Thread(r).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3、拉姆达:

package com.singerw.runable;

/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 14:10
 * @Description: //TODO 类描述
 */
public class RunableTest03 {
    public static void main(String[] args) {
        //一个接口有一个方法
        Runnable r = () -> {
            System.out.println("run....");
        };
        new Thread(r).start();
        //Thread.sleep(5000);
        //只有一行 代码
        Runnable r1 = () -> System.out.println("run....1111");
        new Thread(r1).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

【示例】使用多线程完成三个jar或者图片的下载:

所需jar包:Commons-io:https://mvnrepository.com/artifact/commons-io/commons-io (opens new window)

package com.singerw.runable;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
 * @Author: CodeSleep
 * @Date: 2021-06-16 15:22
 * @Description: //TODO 多线程下载图片
 */
public class RunnableDownImage {
    public static void main(String[] args) {
        DownLoadRunnable dr1 = new DownLoadRunnable("https://singerwimg-1300001977.cos.accelerate.myqcloud.com/2021/05/24/4efee1b8f2004.jpg", "1.jpg");
        DownLoadRunnable dr2 = new DownLoadRunnable("https://singerwimg-1300001977.cos.accelerate.myqcloud.com/2021/05/24/3d64343daa866.jpg", "2.jpg");
        DownLoadRunnable dr3 = new DownLoadRunnable("https://singerwimg-1300001977.cos.accelerate.myqcloud.com/2021/05/24/16202dccad090.jpg", "3.jpg");
        new Thread(dr1).start();
        new Thread(dr2).start();
        new Thread(dr3).start();
    }
}

class DownLoadRunnable implements Runnable {
    private String url;
    private String destination;

    public DownLoadRunnable(String url, String destination) {
        this.url = url;
        this.destination = destination;
    }

    @Override
    public void run() {
        // 第一个参数
        try {
            URL source = new URL(url);
            // 第二个参数
            FileUtils.copyURLToFile(source, new File(destination));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(destination + "下载成功!");

    }
}
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

# 三、线程常用方法

线程(Thread)基本方法清单:

方法 描述
run 线程的执行实体。
start 线程的启动方法。
currentThread 返回对当前正在执行的线程对象的引用。
setName 设置线程名称。
getName 获取线程名称。
setPriority 设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过 thread.setPriority(Thread.MAX_PRIORITY) 的方式设置,默认优先级为 5。
getPriority 获取线程优先级。
setDaemon 设置线程为守护线程。
isDaemon 判断线程是否为守护线程。
isAlive 判断线程是否启动。
interrupt 中断另一个线程的运行状态。
interrupted 测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后且在第二次调用检查其状态之前再次中断)。
join 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
Thread.sleep 静态方法。将当前正在执行的线程休眠。
Thread.yield 静态方法。将当前正在执行的线程暂停,让其他线程执行。

# 3.1 currentThread

返回对当前正在执行的线程对象的引用。

package com.etc.demothread;

public class TestThread05 {

	public static void main(String[] args) throws InterruptedException {

		// main入口执行,是否启动了一个线程

		// 1. 静态方法 得到当前线程对象
		Thread thread = Thread.currentThread();
		thread.setPriority(1);
		// 2. 实例方法 getId getName id和name
		System.out.println(thread.getId());
		System.out.println(thread.getName());
		// 3. 优先级
		System.out.println("getPriority :" + thread.getPriority());

		T t = new T();
		//守护线程
		t.setDaemon(true);
		t.setPriority(1);
		System.out.println(t.getId());
		System.out.println(t.getName());
		// 3. 优先级
		System.out.println("t getPriority " + t.getPriority());

		t.start();

		System.out.println("****************************");

		T1 t1 = new T1();
		t1.setPriority(10);
		System.out.println(t1.getId());
		System.out.println(t1.getName());
		// 3. 优先级
		System.out.println("t1 getPriority " + t1.getPriority());

		t1.start();

	}

}

class T extends Thread {
	@Override
	public void run() {
		// 当前线程休眠1秒
		try {
			Thread.sleep(30000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("t....");
	}
}

class T1 extends Thread {
	@Override
	public void run() {
		// 当前线程休眠1秒
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("t1....");
	}
}
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

# 3.2 线程休眠和线程礼让

sleep() yield()
使当前线程进入被阻塞的状态 将当前线程转入暂停执行的状态
即使没有其他等待运行的线程,当前线程也会等待指定的时间 如果没有其他等待执行的线程,当前线程会马上恢复执行
其他等待执行的线程的机会是均等的 会将优先级相同或更高的线程运行

# 1、sleep()

使用 Thread.sleep 方法可以使得当前正在执行的线程进入休眠状态。

使用 Thread.sleep 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。

Thread.sleep 方法可能会抛出 InterruptedException,因为异常不能跨线程传播回 main 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

sleep(long millis)
//使当前正在执行的线程停留(暂停执行)指定的毫秒数,这取决于系统定时器和调度程序的精度和准确性。  
sleep(long millis, int nanos)
//导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。 
1
2
3
4
  • sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会
  • Java并不保证线程在阻塞给定的时间后能够马上执行(事实上这几乎是不可能的事情),在阻塞时间到了之后,线程进入就绪状态,继续执行的时机取决于Java虚拟机的线程调度机制,唯一能够确定的是,线程中断执行的时间是大于等于给定的阻塞时长的,因此不要将sleep用作精确度要求非常高的定时任务调度
public class ThreadSleepDemo {

    public static void main(String[] args) {
        new Thread(new MyThread("线程A", 500)).start();
        new Thread(new MyThread("线程B", 1000)).start();
        new Thread(new MyThread("线程C", 1500)).start();
    }

    static class MyThread implements Runnable {

        /** 线程名称 */
        private String name;

        /** 休眠时间 */
        private int time;

        private MyThread(String name, int time) {
            this.name = name;
            this.time = time;
        }

        @Override
        public void run() {
            try {
                // 休眠指定的时间
                Thread.sleep(this.time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name + "休眠" + this.time + "毫秒。");
        }
    }
}
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

# 2、yield()

Thread.yield 方法的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行 。

该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

  • yield() 方法只是使当前线程重新回到就绪可执行状态,所以执行yield()线程有可能在进入到就绪状态后马上又被执行,只能使相同或更高优先级的线程有执行的机会
  • 同样,yield()也不会释放锁资源
public class ThreadYieldDemo {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        new Thread(t, "线程A").start();
        new Thread(t, "线程B").start();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
                if (i == 2) {
                    System.out.print("线程礼让:");
                    Thread.yield();
                }
            }
        }
    }
}
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

# 3、sleep()和yield()的区别

sleep和yield的区别在于, sleep需要提供阻塞时长,可以使优先级低的线程得到执行的机会, 而yield由于使线程直接进入就绪状态,没有阻塞时长,而且只能使相同或更高优先级的线程有执行的机会,甚至于某些时候JVM认为不符合最优资源调度的情况下会忽略该方法的调用(类似于System.gc())

# 3.3 守护线程

Java中将线程划分为了两类:

用户线程 (User Thread)

守护线程 (Daemon Thread)

  • 所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止
  • 用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了

如何使用守护线程?

  • 可以使用 isDaemon 方法判断线程是否为守护线程。
  • 可以使用

在使用守护线程时需要注意一下几点:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常,不能把正在运行的常规线程设置为守护线程

  • 在Daemon线程中产生的新线程也是Daemon的

  • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了

  • 可以通过线程对象的isDaemon()方法判定该线程是否守护线程

public class ThreadDaemonDemo {

    public static void main(String[] args) {
        Thread t = new Thread(new MyThread(), "线程");
        t.setDaemon(true); // 此线程在后台运行
        System.out.println("线程 t 是否是守护进程:" + t.isDaemon());
        t.start(); // 启动线程
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "在运行。");
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3.4 线程通信

# 1、Join方法

Thread类的join()方法用于等待其它线程结束,当前运行的线程可以调用另一线程的join方法,当前运行线程将转到阻塞状态,直至另一线程执行结束,它才会恢复运行

在线程操作中,可以使用 join 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。

package com.etc.th.syn;

public class TestThread {

	public static void main(String[] args) {
		JoinThread jt = new JoinThread();
		jt.start();

		for (int i = 1; i <= 20; i++) {
			if (i == 15) {
				try {
					jt.join(); //JoinThread ,使jt加入主线程;当前主线程让位给对方;理解为"插队"
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + ": " + i);
		}
	}
}

class JoinThread extends Thread {
	@Override
	public void run() {
		for (int i = 1; i <= 20; i++) {
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + ": " + i);
		}
	}
}
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

【 面试题】: 定义三个线程对象,每隔一定的时间来执行线程对象1(5),线程对象2(7),线程对象3(9)

# 3.5 优先级

可以通过Thread类的setPriority()方法(该方法被final修饰,不能被子类重载)更改优先级。

优先级不能超出1-10的取值范围,否则抛出IllegalArgumentException,建议使用Thread提供的三个优先级常量来确定线程优先级的高、常规和低取值

另外如果该线程已经属于一个线程组(ThreadGroup),该线程的优先级不能超过该线程组的优先级

# 四、线程安全问题

Synchronized:锁,就是非公平锁,不能设置是否是公平锁.关键字。放在方法前,代码段。

Lock:可以公平,也可以非公平。明确的创建锁对象。锁动作Lock() ,解锁unLock()

# 4.1 模拟售票案例

很多个售票点; 用的是同一份数据; 数据的一致性,也就是每个售票点理解为一个线程操作的话,这些售票点对票的数量应该是”共享”的;

package com.etc.demothread;

import java.util.Random;

public class TicketRunnable implements Runnable {

	// 定义一个变量,票的数量
	private int ticketCount = 100;

	public synchronized void saleTicket() {
		if (ticketCount > 0) {
			try {
				// 模拟卖票的时间
				Thread.sleep(new Random().nextInt(1000));
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "==> 正在卖票: " + ticketCount--);
		}
	}

	@Override
	public void run() {
		while (ticketCount > 0) {
			saleTicket();
		}
	}
}
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
package com.etc.demothread;

import java.util.ArrayList;
import java.util.Vector;

public class TestTicketMain {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TicketRunnable tr = new TicketRunnable();
		//一个线程,一个售票点
		new Thread(tr).start();
		//一个线程,一个售票点
		new Thread(tr).start();
		//一个线程,一个售票点
		new Thread(tr).start();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 五、线程生命周期

线程安全周期图

java.lang.Thread.State 中定义了 6 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。

以下是各状态的说明,以及状态间的联系:

  • 新建(New) - 尚未调用 start 方法的线程处于此状态。此状态意味着:创建的线程尚未启动。

  • 就绪(Runnable) - 已经调用了 start 方法的线程处于此状态。此状态意味着:线程已经在 JVM 中运行。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层操作系统的资源调度。

  • 阻塞(Blocked) - 此状态意味着:线程处于被阻塞状态。表示线程在等待 synchronized 的隐式锁(Monitor lock)。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 synchronized 隐式锁的线程释放锁,并且等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。

  • 等待(Waiting) - 此状态意味着:线程无限期等待,直到被其他线程显式地唤醒。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 synchronized 的隐式锁。而等待是主动的,通过调用 Object.wait 等方法进入。

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait 方法 Object.notify / Object.notifyAll
没有设置 Timeout 参数的 Thread.join 方法 被调用的线程执行完毕
LockSupport.park 方法(Java 并发包中的锁,都是基于它实现的) LockSupport.unpark
  • 定时等待(Timed waiting) - 此状态意味着:无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
进入方法 退出方法
Thread.sleep 方法 时间结束
获得 synchronized 隐式锁的线程,调用设置了 Timeout 参数的 Object.wait 方法 时间结束 / Object.notify / Object.notifyAll
设置了 Timeout 参数的 Thread.join 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos 方法 LockSupport.unpark
LockSupport.parkUntil 方法 LockSupport.unpark
  • 终止(Terminated) - 线程执行完 run 方法,或者因异常退出了 run 方法。此状态意味着:线程结束了生命周期。

# 六、 线程常见问题

# 6.1. sleep、yield、join 方法有什么区别

yield 方法

  • yield 方法会 让线程从 Running 状态转入 Runnable 状态。
  • 当调用了 yield 方法后,只有与当前线程相同或更高优先级的Runnable 状态线程才会获得执行的机会。

sleep 方法

  • sleep 方法会 让线程从 Running 状态转入 Waiting 状态。
  • sleep 方法需要指定等待的时间,超过等待时间后,JVM 会将线程从 Waiting 状态转入 Runnable 状态。
  • 当调用了 sleep 方法后,无论什么优先级的线程都可以得到执行机会。
  • sleep 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。

join方法

  • join 方法会 让线程从 Running 状态转入 Waiting 状态。
  • 当调用了 join 方法后,当前线程必须等待调用 join 方法的线程结束后才能继续执行。

# 6.2. 为什么 sleep 和 yield 方法是静态的

Thread 类的 sleep 和 yield 方法将处理 Running 状态的线程。

所以在其他处于非 Running 状态的线程上执行这两个方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

# 6.3. Java 线程是否按照线程优先级严格执行

即使设置了线程的优先级,也无法保证高优先级的线程一定先执行。

原因在于线程优先级依赖于操作系统的支持,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。

# 6.4. 一个线程两次调用 start()方法会怎样

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

# 6.5. start 和 run 方法有什么区别

  • run 方法是线程的执行体。
  • start 方法会启动线程,然后 JVM 会让这个线程去执行 run 方法。

# 6.6. 可以直接调用 Thread 类的 run 方法么

  • 可以。但是如果直接调用 Thread 的 run 方法,它的行为就会和普通的方法一样。
  • 为了在新的线程中执行我们的代码,必须使用 Thread 的 start 方法。

# 6.7 集合中支持线程同步的类有哪些?

  • Vector 基于方法的synchaonized
  • ConCurrentHashMap
  • HashTable
  • CopyOnWriteArrayList
编辑 (opens new window)
#JavaCore
Java IO流
Java 网络编程

← Java IO流 Java 网络编程→

最近更新
01
Maven资源导出问题终极版
10-12
02
《MyBatis-Plus》学习笔记
10-07
03
MyBatis-Plus—配置日志
10-07
更多文章>
Theme by Vdoing | Copyright © 2020-2021 版权所有 | repository.singerw.com
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×