多线程概念

线程指进程中的一个执行场景

  • 每个进程是一个应用程序,都有独立的内存空间
  • 同一个进程中的线程共享其进程中的内存和资源
    共享的内存是堆内存和方法区内存,栈内存不共享

实现线程的三种方式

Java 虚拟机的主线程入口是 main 方法,用户可以自己创建线程,创建方式有三种:

  • 继承 Thread 类
  • 实现 Runnable 接口(推荐使用 Runnable 接口)
  • 实现 Callable 接口

继承 Thread 类

编写一个类,直接继承java.lang.Thread,重写run方法

  • 优点: 编写简单, 可以在子类中增加新的成员变量, 使线程具有某种属性, 也可以在子类中增加方法, 使线程具有某种功能。
  • 缺点: 因为线程类已经继承了 Thread 类, 所以不能再继承其他的父类。
class MyThread extends Thread {
    @Override
    public void run() {
        // 编写程序,这段程序运行在分支线程中
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        // 新建一个分支线程对象
        MyThread t = new MyThread();
        // 再新建一个分支线程对象
        MyThread t1 = new MyThread();
        // 启动线程
        t.start(); 
        t1.start(); 
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}
  • start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间
  • start()方法就结束了。线程就启动成功了
  • 启动成功的线程会自动调用run方法

实现 Runnable 接口

  • 优点: 线程类只是实现了 Runable 接口, 还可以继承其他的类。 在这种方式下,可以多个线程共享同一个目标对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将 CPU 代码和数据分开, 形成清晰的模型, 较好地体现了面向对象的思想。
  • 缺点: 编程稍微复杂。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        // 创建一个可运行的对象
        MyRunnable r = new MyRunnable();
        // 将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);
        // 启动线程
        t.start();
        for(int i = 0; i < 100; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

【运行结果(部分)】

主线程--->97
分支线程--->86
主线程--->98
分支线程--->87
主线程--->99
分支线程--->88
分支线程--->89
分支线程--->90
分支线程--->91

或者使用匿名内部类

//匿名内部类
Thread t = new Thread(new Runnable(){
	@Override
	public void run() {
		for(int i = 0; i < 100; i++){
			System.out.println("t线程---> " + i);
		}
	}
});
//lambda表达式
Thread t1 = new Thread(()->{
	for(int i = 0; i < 100; i++){
		System.out.println("t线程---> " + i); 
	}
});

实现 Callable 接口

  • 这种方式的优点:可以获取到线程的执行结果
  • 这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
public class ThreadTest {
    public static void main(String[] args) throws Exception {

        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
        	// call()方法就相当于run方法。只不过这个有返回值
            @Override
            public Object call() throws Exception { 
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 3);//睡觉3秒
                System.out.println("call method end!");
                int a = 100,b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });
        // 创建线程对象
        Thread t = new Thread(task);
        // 启动线程
        t.start();
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        // main方法这里的程序要想执行必须等待get()方法的结束
        System.out.println("线程执行结果:" + obj);
    }
}

线程的生命周期

线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡
在这里插入图片描述
新建:采用 new语句创建完成
就绪:执行 start() 后
运行:占用 CPU 时间
阻塞:执行了 wait 语句、执行了 sleep 语句和等待某个对象锁、等待输入的场合
终止:退出 run()方法

线程控制问题

多线程中常用方法

返回值类型方法名描述
static ThreadcurrentThread()怎么获取当前线程对象
StringgetName()获取线程对象的名字
voidsetName(String name)修改线程对象的名字
static voidsleep(long millis)让当前线程进入休眠
voidinterrupt()终止线程的睡眠
voidsetPriority(int newPriority)设置线程的优先级
staticyield()让位给其它线程
voidjoin()合并线程
voidsetDaemon(boolean on)设置守护线程

线程的优先级

处于就绪状态的线程首先进入就绪队列排队等候 CPU 资源, 同一时刻在就绪队列中的线程可能有多个。 Java 虚拟机中的线程调度器负责管理线程, 调度器把线程的优先级分为10 个级别, 分别用 Thread 类中的常量表示。 每个 Java 线程的优先级都在常数 1 和 10 之间线程的优先级分

  • 最高优先级:Thread.MAX_PRIORITY (对应优先级参数为10)
  • 最低优先级:Thread.MIN_PRIORITY (对应优先级参数为1)
  • 默认优先级:Thread.NORM_PRIORITY (对应优先级参数为5)

线程的优先级可以通过 setPriority(int grade)方法调整。getPriority ()方法返回线程的优先级

线程休眠与中断

Thread 的 sleep( )方法能使当前线程暂停运行一段时间 (单位: 毫秒) 需要注意的是, sleep( )方法的参数不能为负, 否则会抛出 IllegalArgumentException 异常。 而 Thread 的 interrupt( )方法经常用来 “吵醒” 休眠的线程。

休眠 sheep()

static void sleep(long millis)
1、静态方法:Thread.sleep(1000);
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用
这行代码出现在A线程中,A线程就会进入休眠
这行代码出现在B线程中,B线程就会进入休眠
4、Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.setName("t");
        t.start();
        try {
            // 问题:这行代码会让线程t进入休眠状态吗?
            // 这样代码出现在main方法中,main线程睡眠
            t.sleep(1000 * 5); 
            // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后这里才会执行。
        System.out.println("hello World!");
    }
}

class MyThread extends Thread {
    public void run(){
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

中断 interrupt()

这个不是终断线程的执行,是终止线程的睡眠

【案例】

模拟一个火车站的售票窗口, 有两个线程售票员 ticketSeller 和乘客 passenger, 因为没人买票, 售票员决定休息 30 分钟, 这时有个乘客过来买票,吵醒休眠的售票员

public class Main {
    public static void main(String[] args) throws InterruptedException {
        WaitingRoom waitRoom = new WaitingRoom();
        waitRoom.ticketSeller.start();//售票员决定休息
        Thread.sleep(5*1000);//过了5秒钟来了个乘客
        waitRoom.passenger.start();//乘客买票
    }
}

class WaitingRoom implements Runnable {
    Thread ticketSeller;//售票员线程
    Thread passenger;//乘客线程

    public WaitingRoom() {
        ticketSeller = new Thread(this);
        passenger = new Thread(this);
        ticketSeller.setName("售票员");
        passenger.setName("乘客");
    }

    public void run() {
        if (Thread.currentThread() == ticketSeller) {
            try {
                System.out.println(ticketSeller.getName() + "决定休息 30 分钟");
                Thread.sleep(1000 * 60 * 30);
            } catch (InterruptedException e) {
                System.out.println(ticketSeller.getName() + "被叫醒了!");
            }
            System.out.println(ticketSeller.getName() + "开始卖票");
        } else if (Thread.currentThread() ==passenger){
            System.out.println("乘客说:买票");
            ticketSeller.interrupt();//吵醒 ticketSeller
        }
    }
}

线程的让步与插队

让步 yield()

线程的让步就是让正在执行的任务暂停, 使其他任务继续执行。当前线程暂停,回到就绪状态。yield( )方法不会阻塞该线程, 之后该线程与其他线程是相对公平的。 这就好比篮球赛两队同学互相抢篮球, 当某个同学抢到篮球后就可以拍一会, 之后他会把篮球让出来, 大家重新开始抢篮球。

public class Main {
  public static void main(String[ ] args){
    Basketball basketball = new Basketball( );
    basketball.playerOne.start( );
    basketball.playerTwo.start( );
  }
}

class Basketball implements Runnable {
  Thread playerOne, playerTwo;
  public Basketball( ){
    playerOne = new Thread(this);
    playerTwo = new Thread(this);
    playerOne.setName("红方");
    playerTwo.setName("蓝方");
  }
  public void run( ){
    for(int i = 1;i<= 5;i++){
      System.out.println(Thread.currentThread( ).getName( )+"拍了第"+i+"下");
      Thread.yield( );  //线程让步
    }
  }
}

【运行结果】

蓝方拍了第1下
蓝方拍了第2下
红方拍了第1下
蓝方拍了第3下
红方拍了第2下
蓝方拍了第4下
红方拍了第3下
蓝方拍了第5下
红方拍了第4下
红方拍了第5Process finished with exit code 0

插队 join()

线程插队是通过 join( )方法阻塞当前线程, 先完成被 join( )方法加入的线程, 之后再完成其他线程。 使用线程插队 join( )方法时, 需要抛出 InterruptedException 异常。

【案例】

在火车站买票的时候, 有的乘客着急赶火车, 会插到队伍前面先买车票, 其他乘客再买票。 那么在多线程程序中, 也可以通过线程插队, 让插队的线程先执行完, 然后本线程才开始执行。

public class Main {
    public static void main(String[] args) {
        ThreadJoin join = new ThreadJoin();
        join.passenger.start();
    }
}

class ThreadJoin implements Runnable {
    Thread passenger;//正常排队的线程
    Thread joinPassenger; //插队的线程

    public ThreadJoin() {
        passenger = new Thread(this);
        joinPassenger = new Thread(this);
        passenger.setName("排队线程");
        joinPassenger.setName("插队线程");
    }

    public void run() {
        if (Thread.currentThread() == passenger) {
            System.out.println(passenger.getName() + "想买票");
            joinPassenger.start();
            try {
                joinPassenger.join(); //当前排队线程等待插队线程完成买票
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(passenger.getName() + "开始买票");
        } else if (Thread.currentThread() == joinPassenger) {
            System.out.println(joinPassenger.getName() + "说:我着急,请让我先买票。");
            System.out.println(joinPassenger.getName() + "买票中…");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(joinPassenger.getName() + "买票完毕");
        }
    }
}

【运行结果】

排队线程想买票
插队线程说:我着急,请让我先买票。
插队线程买票中…
插队线程买票完毕
排队线程开始买票

Process finished with exit code 0

如何正确的停止一个线程

public class ThreadTest10 {
    public static void main(String[] args) {
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);
        t.start();
        // 模拟5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //t.stop();已过时(不建议使用)
        // 终止线程(正确方式)
        r.run = false;
    }
}

class MyRunable4 implements Runnable {
    boolean run = true;

    public void run() {
        for (int i = 0; i < 10; i++){
            if(run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                //save....
                //终止当前线程
                return;
            }
        }
    }
}

线程同步与死锁

在这里插入图片描述

线程同步

线程同步,指某一个时刻,指允许一个线程来访问共享资源,线程同步其实是对对象加锁,如果对象中的方法都是同步方法,那么某一时刻只能执行一个方法,采用线程同步解决以上的问题,我们只要保证线程一操作 s 时,线程 2 不允许操作即可,只有线程一使用完成 s 后,再让线程二来使用 s 变量

【案例】

假设去买火车票, 一趟列车的车票数量是固定的, 不管有多少个地方可以买火车票, 买的一定是这些固定数量的火车票。 如果把各个售票点理解为线程的话, 则所有线程应该共同拥有同一份票数。

不加锁前

public class Main {
    public static void main(String[] args) {
        MyThread mt = new MyThread();//定义线程对象
        Thread t1 = new Thread(mt);//定义 Thread 对象
        Thread t2 = new Thread(mt);//定义 Thread 对象
        Thread t3 = new Thread(mt);//定义 Thread 对象
        t1.start();
        t2.start();
        t3.start();
    }
}

class MyThread implements Runnable {
    private int ticket = 5;  //假设一共有 5 张票

    public void run() {
        while (ticket > 0) {  //还有票
            try {
                Thread.sleep(100);  //加入延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("卖票:ticket = " + ticket--);
        }
    }
}

【运行结果】

卖票:ticket = 5
卖票:ticket = 5
卖票:ticket = 4
卖票:ticket = 3
卖票:ticket = 3
卖票:ticket = 3
卖票:ticket = 2
卖票:ticket = 0
卖票:ticket = 1

Process finished with exit code 0

很明显卖出去的票大于5张,如何解决以上的问题 ?

  • 1、使用同步代码块实现同步
public void run() {
	synchronized (this){//要对当前对象进行同步
		while (ticket > 0) {
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("卖票:ticket = " + ticket--);
		}
	}
}
  • 2、使用同步方法实现同步
public synchronized void run() {
	while (ticket > 0) {  //还有票
		try {
			Thread.sleep(300);  //加入延迟
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("卖票:ticket = " + ticket--);
	}
}

【修改后的运行结果】

卖票:ticket = 5
卖票:ticket = 4
卖票:ticket = 3
卖票:ticket = 2
卖票:ticket = 1

Process finished with exit code 0

对象锁与类锁

在实例方法上使用synchronized表示共享对象一定是this
在静态方法上使用synchronized表示找类锁。类锁永远只有1把。

  • 对象锁:1个对象1把锁,100个对象100把锁。
  • 类锁:100个对象,也可能只是1把类锁。

怎么解决线程安全问题

  • 第一种方案:尽量使用局部变量代替实例变量和静态变量
  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了
  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制

线程死锁

资源共享时需要进行同步操作, 程序中过多的同步会产生死锁。 比如, 张三想要李四的画,李四想要张三的书, 张三对李四说: “先把你的画给我, 我就给你书。” 李四也对张三说: “先把你的书给我, 我就给你画。” 此时, 张三等李四答复, 而李四也等张三答复, 那么这样下去最终结果就是张三得不到李四的画, 李四也得不到张三的书, 这实际上就是死锁。

public class DeadLock {
    public static void main(String[] args) {
        Object picture = new Object();
        Object book = new Object();
        // t1和t2两个线程共享picture,book
        Thread zs = new MyThread1(picture,book);
        Thread ls = new MyThread2(picture,book);
        zs.start();
        ls.start();
    }
}

class MyThread1 extends Thread{
    Object picture;
    Object book;
    public MyThread1(Object picture,Object book){
        this.picture = picture;
        this.book = book;
    }
    public void run(){
        synchronized (picture){
            try {
                System.out.println("张三对李四说:“你给我画,我就把书给你”");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (book){
                System.out.println("张三得到画");
            }
        }
    }
}

class MyThread2 extends Thread {
    Object picture;
    Object book;
    public MyThread2(Object picture,Object book){
        this.picture = picture;
        this.book = book;
    }
    public void run(){
        synchronized (book){
            try {
                System.out.println("李四对张三说:“你给我书,我就把画给你”");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (picture){
                System.out.println("李四得到书了");
            }
        }
    }
}

【运行结果】

张三对李四说:“你给我画,我就把书给你”
李四对张三说:“你给我书,我就把画给你”

Process finished with exit code -1

张三永远得不到画,李四永远得不到画

守护线程

所有的用户线程结束生命周期,守护线程才会结束生命周期,只要有一个用户线程存在,那么守护线程就不会结束,例如 java 中著名的垃圾回收器就是一个守护线程

守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");
        // 启动线程之前,将线程设置为守护线程
        t.setDaemon(true);
        t.start();
        // 主线程:主线程是用户线程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定时器

定时器的作用:间隔特定的时间,执行特定的程序。

public class TimerTest {
    public static void main(String[] args) throws Exception {
        // 创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); 守护线程的方式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-03-14 09:34:30");
        //timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
        timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);// 指定定时任务
    }
}

// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {
    public void run() {
        // 编写你需要执行的任务就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":成功完成了一次数据备份!");
    }
}

wait()和notify()

  • wait和notify方法不是线程对象的方法,是普通java对象都有的方法
  • wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题
  • wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁
  • notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁

【案例】

模拟仓库我们采用List集合。List集合中假设只能存储1个元素。1个元素就表示仓库满了。如果List集合中元素个数是0,就表示仓库空了。保证List集合中永远都是最多存储1个元素。

public class ThreadTest {
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));
        t1.setName("生产者线程");
        t2.setName("消费者线程");
        t1.start();
        t2.start();
    }
}

// 生产线程
class Producer implements Runnable {
    private List list;
    public Producer(List list) {
        this.list = list;
    }
    public void run() {
        while(true){
            synchronized (list){
                if(list.size() > 0){ // 说明仓库中已经有1个元素了
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}

// 消费线程
class Consumer implements Runnable {
    private List list;
    public Consumer(List list) {
        this.list = list;
    }
    public void run() {
        while(true){
            synchronized (list) {
                if(list.size() == 0){// 仓库已经空了
                    try {
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}

【运行结果(部分)】

生产者线程--->java.lang.Object@2bf1d4e6
消费者线程--->java.lang.Object@2bf1d4e6
生产者线程--->java.lang.Object@7f05c69
消费者线程--->java.lang.Object@7f05c69
生产者线程--->java.lang.Object@4a569b99
消费者线程--->java.lang.Object@4a569b99
生产者线程--->java.lang.Object@1174068f
消费者线程--->java.lang.Object@1174068f
生产者线程--->java.lang.Object@3407b6a3
消费者线程--->java.lang.Object@3407b6a3
生产者线程--->java.lang.Object@3111b75e
消费者线程--->java.lang.Object@3111b75e
生产者线程--->java.lang.Object@4f6568b6
消费者线程--->java.lang.Object@4f6568b6
生产者线程--->java.lang.Object@7e944fab
消费者线程--->java.lang.Object@7e944fab
生产者线程--->java.lang.Object@772f5c74
消费者线程--->java.lang.Object@772f5c74
生产者线程--->java.lang.Object@4b38be88
消费者线程--->java.lang.Object@4b38be88
生产者线程--->java.lang.Object@2ffc5f81

Process finished with exit code -1

notifyAll()方法:o.notifyAll()这个方法是唤醒o对象上处于等待的所有线程

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐