深度分析面试90被问到的多线程创建线程线程状态线程安全一次性帮你全搞定

32次阅读

共计 6035 个字符,预计需要花费 16 分钟才能阅读完成。

一、多线程

1. 概述

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。

就是在单个程序中同时运行多个线程来完成不同的工作。

2. 并发与并行

并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
3. 程序、进程与多线程
1)程序
Java 源程序和字节码文件被称为“程序”(Program),是一个静态的概念。

2)进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

3)线程

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

二、创建线程类

1. 继承 Thread 类

Java 使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

Java 中通过继承 Thread 类来创建并启动多线程的步骤如下:

定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程需要完成的任务, 因此把 run()方法称为线程执行体
创建 Thread 子类的实例,即创建了线程对象
调用线程对象的 start()方法来启动该线程
自定义线程类:

public class MyThread extends Thread {
    // 定义指定线程名称的构造方法
    public MyThread(String name) {
        // 调用父类的 String 参数的构造方法,指定线程的名称
        super(name);
    }
    /**
     * 重写 run 方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);
        }
    }
}

测试类:

public class Demo01 {public static void main(String[] args) {
        // 创建自定义线程对象
        MyThread mt = new MyThread("新的线程!");
        // 开启新线程
        mt.start();
        // 在主方法中执行 for 循环
        for (int i = 0; i < 10; i++) {System.out.println("main 线程!"+i);
        }
    }
}

1)Thread 类介绍

构造方法:

public Thread(): 分配一个新的线程对象。

public Thread(String name): 分配一个指定名字的新的线程对象。

public Thread(Runnable target): 分配一个带有指定目标新的线程对象。

public Thread(Runnable target,String name): 分配一个带有指定目标新的线程对象并指定名字。

常用方法:

public String getName(): 获取当前线程名称。

public void start(): 导致此线程开始执行;Java 虚拟机调用此线程的 run 方法。

public void run(): 此线程要执行的任务在此处定义代码。

public static void sleep(long millis): 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

public static Thread currentThread(): 返回对当前正在执行的线程对象的引用

2. 实现 Runnable 接口

采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写 run 方法即可。

步骤如下:

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

public class MyRunnable implements Runnable {
    @Override
    public void run(){for(inti=0;i<20;i++){System.out.println(Thread.currentThread().getName()+""+i);
        }
    }
}

测试类:

public class Demo{public static void main(String[]args){
        // 创建自定义类对象线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建线程对象
        Thread t = new Thread(mr,"小强");
        t.start();
        for(inti=0;i<20;i++){System.out.println("旺财"+i);
        }
    }
}

3.Thread 和 Runnable 的区别

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

适合多个相同的程序代码的线程去共享同一个资源。

可以避免 java 中的单继承的局限性。

增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类。

4. 用 Lambda 表达式创建多线程

/**
 * 用 Lambda 表达式来创建多线程
 */
public class Thread004Lambda {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("我是一个线程对象");
        });

        t.start();// 启动线程
        System.out.println("我是主线程");
    }
}

三、线程状态

1. 线程的五种状态
新生状态: new
就绪状态: runnable
运行状态: running
阻塞状态: blocked
执行完毕: dead

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过执行 stop 或 destroy 方法来终止一个线程。

2. 终止线程

不要调用 stop destory 方法 , 太暴力,一盆冷水让其停止。

自定义类:

/**
 *
 * 终止线程的套路
 * 1、加入标识
 * 2、线程体的执行需要依赖标识
 * 3、对外提供一个可以修改标识的方法
 * 4、外部适当的时机调用方法修改标识
 */
class MyThread03 implements Runnable{
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (true){
            try {Thread.sleep(1000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("在和小美聊天,在说第"+(i++)+"句话");
            if(!flag){break;}
        }
    }
    public void stopThread(){flag = false;}
}

测试类:

public class Thread005 {public static void main(String[] args) throws Exception{MyThread03 mt = new MyThread03();
        Thread t = new Thread(mt);

        t.start();// 开启线程

        Thread.sleep(2000);
        mt.stopThread();}
}

3. 阻塞状态(sleep/yield/join 方法)

sleep 方法:Sleep 时别的线程也不可以访问锁定对象。
yield 方法: 让出 CPU 的使用权,从运行态直接进入就绪态。让 CPU 重新挑选哪一个线程进入运行状态。
join 方法: 等待当前线程死亡,当某个线程等待另一个线程执行结束后,才继续执行时,使用另一个线程的 join 方法。使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行

4. 线程基本信息

一个线程的默认优先级是 5

Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程。

自定义类:

class FatherThread  implements  Runnable {
    @Override
    public void run() {System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买包红塔山");
        Thread son = new Thread(new SonThread());
        son.start();  // 启动新的线程
        System.out.println("爸爸等儿子买烟回来");
        try {son.join();  // 一直傻傻地等
        } catch (InterruptedException e) {e.printStackTrace();
        }

        System.out.println("可以抽烟了");
    }
}
class SonThread implements Runnable {public void run() {System.out.println("儿子出门去买烟");
        System.out.println("儿子买烟需要 10 分钟");
        try {for (int i = 1; i <=10;i++) {System.out.println(Thread.currentThread().getName() + "第" + i + "分钟");
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "儿子买烟回来了");
    }
}

测试类:

public class Demo008Thread {public static void main(String[] args)throws Exception {Thread t1 = new Thread(new SonThread(),"t1");
        Thread t2 = new Thread(new SonThread(),"t2");
//        int p = t1.getPriority(); 获取优先级
        t1.setPriority(10);// 设置线程优先级
        t2.setPriority(1);
        
        t1.start();
        t2.start();
        Thread.sleep(1000);
        //System.out.println(t.isAlive());  // 查看线程是否还活着
    }
}

四、线程安全

1. 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题: 也就是解决重复票与不存在票问题,Java 中提供了同步机制 (synchronized) 来解决。

2. 同步代码块

同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:

synchronized(同步锁){需要同步操作的代码}

同步锁:

对象的同步锁只是一个概念, 可以想象为在对象上标记了一个锁.

锁对象可以是任意类型。
多个线程对象要使用同一把锁。

3. 同步方法

同步方法: 使用 synchronized 修饰的方法, 就叫做同步方法, 保证 A 线程执行该方法的时候, 其他线程只能在方法外等着。
格式:


public synchronized void method(){可能会产生线程安全问题的代码}
1
2

同步锁是谁?

对于非 static 方法, 同步锁就是 this。
对于 static 方法, 我们使用当前方法所在类的字节码对象 (类名.class)。
综合案例:

/**
 * 账户类
 */
class Account {
    String aname; // 账户名
    int money; // 账户余额

    public Account(String aname, int money) {
        this.aname = aname;
        this.money = money;
    }
}
class Drawing implements Runnable{
    Account account = null;
    int money = 0;  // 准备取钱的金额
    int acturalMoney = 0; // 实际取走的钱

    public Drawing(Account account, int money) {
        this.account = account;
        this.money = money;
    }

    @Override
    public  void run() {synchronized (account){// 同步代码块

            // 如果余额不够,取不了钱
            if (account.money < money) {System.out.println("余额不足,没去走钱");
                return;
            }
            try {Thread.sleep(500);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            account.money -= money;   // 取了钱,余额变少
            acturalMoney = money;    // 取出来的钱
            System.out.println(Thread.currentThread().getName() + "取了" + acturalMoney + "还剩" + account.money);
        }
    }
}

测试类:

/**
 * 两个人操作同一个账户
 */
public class Demo010Thread {public static void main(String[] args) throws Exception{
        // 开户
        Account account = new Account("高富帅",100);
        System.out.println(account.money);
        Drawing d =  new Drawing(account,90);
        // 你去取钱
        Thread t = new Thread(d,"你");
        Thread t1 = new Thread(d,"女朋友");
        t.start();
        t1.start();

        Thread.sleep(1500);
        System.out.println(account.money);
    }
}

4. 死锁及解决方案

1)死锁的概念
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

如何解决死锁问题:

往往是程序逻辑的问题。需要修改程序逻辑。
尽量不要同时持有两个对象锁。

正文完
 0