10、多线程
1、程序、线程、进程等概念
- 程序:静态代码,软件执行的蓝本
- 进程:程序的一次动态执行过程,包括进程的产生、发展、消亡
- 线程:比进程更小的执行单位
一个进程可以有产生多个线程,每个进程有一段「专用」的内存区域,线程间可以共享 「相同」的内存单元
每个 Java 程序都有一个默认的「主线程」 main()
当 JVM 加载代码,发现 main()
后,会启动一个主线程,在 main 方法中可以创建其他线程。
如果 main 方法没有创建其他线程,那么 main 方法执行结束后,JVM 就结束了 java 程序
如果创建了其他线程,JVM 就要在主线程和其他线程之间轮流切换,即使 main 方法结束,JVM 也不会结束 java 程序,JVM 一直到程序中的所有线程都结束才结束 java 程序
在 Java 中,至少有三个线程
- main() 主线程
- gc() 垃圾回收线程
- 异常处理线程
并行与并发:
- 并行:在一个时间点,多个 CPU 同时执行多个任务
- 并发:在一段时间内,一个 CPU 执行多个任务
多线程程序优点:
- 提高应用程序的响应,增强用户体验。
- 提高计算机资源利用率
进程可细化为多个线程
- 每个线程,拥有自己独立的栈,程序计数器(PC)
- 多个线程,共享一个进程中的结构:方法区,堆
线程生命周期
Java 使用 Thread 类及其子类的对象来表示线程
Tread 提供 getStatue() 方法返回枚举类型,Thread.Statue 的值:
- 新建状态(NEW)
- 当一个 Thread 类或其子类的对象被创建时,新生的线程处于 「NEW」 状态。此时它已经占有了相应的内存空间和其他资源,但并没有被 JVM 管理,即尚未启动(没有调用
start()
方法)
- 当一个 Thread 类或其子类的对象被创建时,新生的线程处于 「NEW」 状态。此时它已经占有了相应的内存空间和其他资源,但并没有被 JVM 管理,即尚未启动(没有调用
- 可运行状态(RUNNABLE)
- 线程调用
start()
方法后进入「RUNNABLE」状态,称为可运行状态,JVM 管理该线程,当 JVM 将 CPU 使用权切换给该状态的线程时,如果线程是 Thread 子类创建的,则调用run()
方法
- 线程调用
- 中断状态(BLOCKED、TIMED_WAITING、WAITING)
- 当中断的线程重新进入「RUNNABLE」状态后,且 JVM 分配 CPU 使用权给该线程,
run()
方法将从中断出继续执行- BLOCKED:JVM 将 CPU 资源从当前「RUNNABLE」切换给其他线程时,本线程进入「BOLCKED」状态,「BOLCKED」状态必须等待 JVM 解除其「BOLCKED」再次进入「RUNNABLE」
- TIMED_WAITING:当线程执行
sleep(int millsecond)
方法后,该线程立刻让出 CPU 使用权,经过至多 millsecond 毫秒后再次进入「RUNNABLE」状态 - WAITING:当先线程执行
wait()
方法后,进入「WAITING」状态「WAITING」状态不会主动进入「RUNNABLE」状态,必须由其他线程调用notify()
方法通知它,使其进入「RUNNABLE」状态
- 当中断的线程重新进入「RUNNABLE」状态后,且 JVM 分配 CPU 使用权给该线程,
- 死亡状态(TERMINATED)
- 一个线程执行完了 run() 方法,该线程进入 TERMINATED 状态
相关信息
只有处于 NEW 状态的线程可以调用 start() 方法,处于其他状态的线程都不可调用 start() 方法,否则触发 IllegalThreadStateException
方法
线程的调度与优先级
处于就绪状态的线程首先就绪队列排队等待 CPU 资源
线程在获得 CPU 使用权后,即使没有完成自己的全部操作,java 调度器也会中断当前线程的执行,把 CPU 的使用权切换给下一个排队等待的线程,当前线程等待 CPU 资源的下一次轮回,然后从中断出继续执行
Java 调度器使高优先级的线程始终运行,一旦有空闲时间片,则使有相同优先级的线程顺序使用时间片。
如 A 和 B 线程优先级高于 C 和 D,则先执行 A 和 B,等 A 和 B 进入死亡转台,执行 C 和 D(高优先级的线程要抢占低优先级线程 CPU 的执行权。 但从概率上讲,高优先级的线程高概率情况下被执行,并不意味着强制的先后顺序)
Java 虚拟机的线程调度器负责管理线程,调度器把线程的优先级分为 10 个级别,分别用 Thread 类中的类常量表示,每个 Java 线程优先级为常数 1 - 10,即 Thread.MIN_PRIORITY
和 Thread.MAX_PRIORITY
之间。
线程优先级别默认为 5,即 Thread.NORM_PRIORITY
线程优先级可以通过 setPriority(int grade) 方法调整,grade 参数在 1 - 10 之间
2、线程的创建
Thread 子类
Java 中,用 Thread 类或子类创建线程对象
不同的线程可以操作共享的数据
- 创建子类
- 重写
Thread
的run()
,此线程执行的操作声明在run()
中 - 创建
子类对象
- 通过此对象调用
start()
,启动当前线程并调用当前线程的run方法
使用 Thread 子类创建线程的优点是可以在子类中增加新的成员变量和方法
但 Java 不支持多继承,Thread 类的子类不能再继承其他的类
//创建子类
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
相关信息
子类对象调用 start() 方法,不能直接调用 run() 方法
Runnable 接口
- 创建一个实现了
Runnable 接口
的类 - 实现类实现 Runnable 中的抽象方法:
run()
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器
Thread(Runnable target)
中,创建 Thread 对象 - 通过 Thread 类的对象调用
start()
通过 Thread 类的对象调用 start():
- 启动线程
- 调用当前线程的 run() --> 调用了 Runnable 类型的 target 对象的 run 方法
class MThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MThread mThread = new MThread();
Thread t1 = new Thread(mThread);
Thread t2 = new Thread(mThread); // t1 与 t2 目标对象相同
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
相关信息
比较创建线程的两种方式
开发中,优先选择实现 Runnable
接口的方式,原因如下:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合处理多个线程有共享数据的情况
联系:public class Thread implements Runnable
相同点:两种方式都需要重写 run(),将线程要执行的逻辑声明在 run() 中
相关信息
在线程中启动其他线程
在主线程中调用 start 方法后,一旦该线程获取到 CPU 资源,就可以脱离创建它的主线程开始自己的生命周期
实际上也可以在任何一个线程中启动另外一个线程(在 run 方法中调用不同 Thread 的对象的 start 方法)
3、Thread 有关方法
相关信息
关于 run() 方法中的局部变量:
当线程被创建多个时,run() 方法中的局部变量也被分配不同的内存空间。不同线程的 run() 方法中的局部变量互不干扰
thread 的常用方法:
方法名称 | 描述 |
---|---|
void start() | 启动当前线程,调用 run() 方法 |
run() | 需要重写,方法体为线程所执行的操作 |
static Thread currentThread() | 静态方法,返回执行当前代码的线程 |
String getName() | 获取当前线程名 |
void setName(String name) | 设置当前线程名 |
static void yield() | 线程让步,暂停当前正在执行线程, 把执行机会让给优先级相同或更高的线程,若等待队列中没有,则忽略此方法 |
join() | 在线程 A 中调用线程 B 的 join() 方法,线程 A 进入阻塞状态直到线程 B 完全执行完才结束阻塞状态,进入就绪状态(该方法与优先级无关) |
static void sleep(long millitime) | 让当前线程放弃 CPU 资源,让其他线程获得执行机会,millitime 毫秒后重新计入就绪队列,如果休眠时被打断,抛出 InterruptedException |
boolean isAlive() | 判断当前进程是否存活(NEW 和 TERMINATED 状态返回 fasle,其他返回 true) |
stop() | 已过时,强制结束当前进程 |
interrupt() | 用于唤醒正在休眠(sleep)的线程,使休眠线程发生 InterruptedException,是线程进入 RUNNABLE 状态,等待 CPU 资源 |
void setDaemon(boolean on) | 线程默认是非守护线程(用户线程),通过 thread.setDaemon(true) 设置为守护线程,当程序中的所有用户线程都结束运行,即使守护线程的 run() 方法中还有要执行的语句也立刻结束运行 |
相关信息
一个正在运行的线程在没有进入死亡状态时,不要再给线程分配实体,线程只能引用最后分配的实体,先前的实体就会成为垃圾,且不会被垃圾收集器回收,因此会继续运行,如:
Thread thread = new Thread(target);
target.start;
thread = new Thread(target) // thread 指向新实体,之前的实体不被回收,先前线程继续执行
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread thread = new HelloThread("thread-1"); // 创建 Thread 实例并命名
thread.start();
Thread.currentThread().setName("main thread"); //给主线程命名
for (int i = 0; i < 100; i++) {
System.out.println(i);
if(i == 20){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(thread.isAlive());
}
}
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 20 == 0){
this.yield();
}
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public HelloThread(String name){
super(name);
}
}
4、线程的同步(线程的安全问题)
线程安全问题:不同线程在同一时间操作同一变量时或出现混乱
在 java 中,通过 同步机制
,解决线程安全问题
解决线程安全问题的方法:
- 同步代码块
- 同步方法
相关信息
在使用 wait()
、notify()
、notifyAll()
方法时,通过同步监视器调用
同步代码块
同步代码块编写方式:
synchronized(同步监视器){
//需要被同步的代码
}
- 需要被同步的代码:操作共享数据的代码(不含包含过多或过少代码)
- 同步监视器(锁):任何一个类的对象,都可以充当锁
- 多个线程必须要公用同一把锁
相关信息
在实现 Runnable 接口创建多线程的方式中,可以考虑使用 this
充当同步监视器
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100; // 多个线程操作的变量
Object object = new Object();
@Override
public void run() {
while (true){
synchronized (this) {// 此时的 this: 唯一的 Window1 对象
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 window1 = new Window2();
Window2 window2 = new Window2();
Window2 window3 = new Window2();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
//synchronized (obj)
synchronized (Window2.class) {//window2.class只会加载一次
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":票号" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
同步方法
如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的
权限修饰符 synchronized 返回值类型 方法名(参数列表){
}
相关信息
关于同步方法:
- 同步方法仍然涉及到同步监视器
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身
同步的方式,解决了线程的安全问题
操作同步代码时只能有一个线程参与,其他线程等待
相当于是一个单线程的过程
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
private synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 window1 = new Window4();
Window4 window2 = new Window4();
Window4 window3 = new Window4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window4 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){// 同步监视器:window4.class
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket);
ticket--;
}
}
}
线程死锁
- 死锁:不同的线程分别占用对方需要的同步资源不放弃
- 说明:出现死锁后,不会出现异常与提示,只是所有的线程都处于阻塞状态,无法继续
- 要避免出现死锁
5、线程安全的懒汉式单例模式
public class BankTest {
}
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance() {
//方式一:效率稍差
/*synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
return instance;
}*/
//方式二:效率稍高
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
6、lock 锁
解决线程安全问题方式三:Lock 锁 -- JDK5.0 新增
相关信息
synchronized 与 lock 的异同:
- 相同点:二者都可以解决线程安全问题
- 不同点:
- synchronized 机制在执行相应的同步代码后,自动的释放同步监视器
- lock 需要手动的启动同步(lock()),同时也需要手动的结束同步(unlock())
相关信息
解决线程安全问题的方式(优先顺序)
- Lock
- 同步代码块
- 同步方法
public class MyThread extends Thread{
int ticket = 100;
static Lock lock = new ReentrantLock(); // 每个线程对象共享一个锁
@Override
public void run() {
while (true) {
lock.lock(); // 加锁
{
try{
if (ticket > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + ":票号" + ticket);
ticket--;
} else {
break;
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock(); // 最后一定要释放锁,否则程序会继续运行
}
}
}
}
}
7、线程的通信(wait、notify、notifyAll)
对于同步方法(synchronized)有时涉及一些特殊情况,如当你在一个售票窗口排队购票时,售票员没有零钱找你,你就必须等待,允许后面的人购票,获得零钱
wait()
:中断同步方法/代码块执行,当前线程进入阻塞状态,释放同步监视器(锁),允许其他线程使用该同步方法/代码块notify()
:唤醒一个 wait 的线程,如果有多个线程被 wait,就唤醒优先级高的那个notifyAll
:一旦执行此方法,就会唤醒所有被 wait 的线程
一般来说,先中断的线程先被唤醒
相关信息
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的监视器
否则会出现IllegalMonitorStateException
异常wait()
,notify()
,notifyAll()
都是 Object 类中的 final 方法,不允许重写
class Number implements Runnable{
private int num = 1;
@Override
public void run() {
while (true){
synchronized (this) {
this.notify();
if (num <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+num);
num++;
try {
//使得调用 wait() 方法的线程进入阻塞状态
this.wait();// wait() 会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();
}
}
相关信息
sleep()
和 wait()
的异同:
- 相同点:都可以让当前线程进入阻塞状态
- 不同点:
- 两个方法声明的位置不同:Thread 类中声明
sleep()
,Object 类中声明wait()
- 调用要求:
sleep()
可以在任何需求的场景下调用。wait()
必须使用在同步代码块或同步方法中 - 如果两个方法都使用在同步代码块或同步方法中,
sleep()
不会释放锁,wait()
会释放锁
- 两个方法声明的位置不同:Thread 类中声明
8、多线程的经典问题
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个");
notify();
}else{
//等待
try {
wait();// wait() 会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个");
productCount--;
notify();
}else {
//等待
try {
wait();// wait() 会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"生产产品");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Customer extends Thread{//消费者
private Clerk clerk;
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"消费产品");
while(true){
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
public Customer(Clerk clerk) {
this.clerk = clerk;
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Customer c1 = new Customer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
9、JDK5.0 新增线程创建方式
1、实现 Callable 接口
- 创建一个实现 Callable 的实现类
- 实现 call 方法,将此线程需要执行的操作声明在 call() 中
- 创建 Callable 接口实现类的对象
- 将此 Callable 接口实现类的对象作为传递到 FutureTask 的构造器中,创建 FutureTask 的对象
- 将 FutureTask 的对象作为参数传递到Thread类的构造器中,创建 Thread 对象,并调用 start()
- 获取 Callable 中 call 方法中的返回值
class NumberThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i%2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
NumberThread numberThread = new NumberThread();
FutureTask futureTask = new FutureTask(numberThread);
new Thread(futureTask).start();
try {
//get() 方法返回值即为 FutureTask 构造器参数 callable 实现类重写的 call() 的返回值
Object sum = futureTask.get();
System.out.println("sum:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
相关信息
Callable 比 Runnable 接口创建多线程方式的优点:
- call() 方法可以有返回值
- call() 方法可以抛出异常,被外面的操作捕获,获取异常信息
- Callable 支持泛型
2、线程池
线程池的好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止...
class NumThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//设置线程池的属性
//System.out.println(service.getClass());
service1.setCorePoolSize(15);
//service1.setKeepAliveTime();
//2、执行指定的线程操作。需要提供实现Runnable接口或Call接口实现类的对象
service.execute(new NumThread());//适合使用于Runnable
//service.submit(Callable callable);//适合使用于Callable
//3、关闭连接池
service.shutdown();
}
}
10、多线程练习
100 张票,两个窗口领取,每次领取 30 ms
public class SellTickets {
public static void main(String[] args) {
Thread t1 = new MyThread("窗口1");
Thread t2 = new MyThread("窗口2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
static final Object obj = new Object();
static int tickets = 100;
@Override
public void run() {
while (tickets > 0){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(obj){
if (tickets > 0){
System.out.println(Thread.currentThread().getName() + ":sold " + tickets--);
}
}
}
}
}
100 张票,两个窗口领取,当剩下 10 张时,停止领取
// 1000 张票,两个窗口领取,当剩下 10 张时,停止领取
public class SellTickets {
public static void main(String[] args) {
Thread t1 = new MyThread("窗口1");
Thread t2 = new MyThread("窗口2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
static final Object obj = new Object();
static int tickets = 100;
@Override
public void run() {
while (tickets > 10){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(obj){
if (tickets > 10){
System.out.println(Thread.currentThread().getName() + ":sold " + tickets--);
}else {
System.out.println("关门了");
}
}
}
}
}
使用两个线程交替打印 1-100 中所有奇数
public class SellTickets {
public static void main(String[] args) {
Thread t1 = new MyThread("线程 1");
Thread t2 = new MyThread("线程 2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
static final Object obj = new Object();
static int nums = 1;
@Override
public void run() {
while (nums < 100){
synchronized(obj){
if (nums % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ":" + nums);
nums += 2;
}
obj.notifyAll(); // 唤醒 wait 的线程
try {
if (nums < 100){
obj.wait(); // 释放锁
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}