上一篇11【泛型、Map、异常】

下一篇13【线程等待、状态、线程池、File类】

JavaSE系列教程目录【JavaSE零基础系列教程目录】




一、多线程概念

在实际应用中,多线程非常有用。例如,一个浏览器可以同时下载几幅图片,一个WEB浏览器需要同时服务来自客户端的请求,我们的电脑管家也可以一边杀毒一边清理垃圾再一边进行电脑体检等任务,这些都是多线程的应用场景。

1.1 程序的并发与并行

1.1.1 程序的并行

程序的并行指的是多个应用程序真正意义上的同时执行,CPU分配多个执行单元共同执行这些任务,效率高,但这依赖于CPU的硬件支持,需要CPU多核心的支持,单核处理器的CPU是不能并行的处理多个任务的。

12【多线程、锁机制、lock锁】_线程安全

1.1.2 程序的并发

程序的并发指的是多个应用程序交替执行,CPU分配给每个应用程序一些“执行时间片”用于执行该应用程序,由于CPU的处理速度极快,并且分配个每个线程的“执行时间片”极短,给人们造成视觉上的误感,让人们以为是“同时”执行,其实是交替执行

需要注意的是:虽然是交替执行,但是程序的并发解决了多个程序之间不能“同时”执行的问题,并且程序的并发利用了CPU的空余时间,能将CPU的性能较好的发挥,另外并发不受CPU硬件的限制,实际开发中,并发往往使我们考虑的重点。

Tips:程序并行执行需要依赖于CPU的硬件支持,而并发却不需要;

12【多线程、锁机制、lock锁】_lock锁_02

1.2 进程与线程

1.2.1 进程

  • 进程:是指一个内存中运行的应用程序,我们开启的应用如QQ、微信、google浏览器、idea开发工具等都是一个应用,一个应用最少具备一个进程,也有可能有多个进程,每个进程都有一个独立的内存空间,进程是系统运行程序的基本单位;

12【多线程、锁机制、lock锁】_线程安全_03

Tips:多个进程的执行可以是并行也可以是并发;

1.2.2 线程

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

12【多线程、锁机制、lock锁】_线程安全_04

关于进程和线程的概念我们理解即可,上图中电脑管家的“首页体检”、“病毒查杀”等功能也有可能是一个进程来完成,关于病毒查杀功能下面可能还有其他小功能,有可能是线程完成,也有可能还是一个独立的进程来完成;

1.2.3 多线程并发就一定快吗?

我们知道,并发本质上其实是多条线程交替执行,线程在交替过程中需要损耗一部分性能,由于CPU分配给这些线程执行的时间片非常短,线程交替也非常频繁,因此线程交替是一个比较消耗性能的步骤;

在大部分情况下,多线程的并发能够提升我们程序的执行速度,如:

  • 当应用程序需要同时处理多个任务时,每一个任务都需要花费大量的时间,这个时候我们可以开辟多条程序执行线路来并发的"同时"处理多个任务;
  • 但是当任务处理时间很短,这个时候根本不需要开启多个线程来"同时"处理多个任务,因为任务处理时间非常短暂,还没等CPU切换到其他线程任务就执行完毕了,这个时候多线程反而使得程序效率低;

这就好比如我们的任务是"烧水",我们需要烧开10壶水,每一壶水的烧开都是一个漫长的时间过程。

  • 在单线程环境中:在水烧开的过程中,CPU只能干等着,等第一壶水烧开了后,才可以烧第二壶水,以此类推...这样效率非常慢
  • 在多线程环境中:在水烧开的过程中,CPU去分配时间去其他的线程,让其他的线程也来烧水,这样可以让多个水壶同时烧水,效率快;

这样下来,多线程效率更高;

但是现在我们的任务如果变为了"拍蒜",我们需要拍10个蒜,拍一瓣蒜的速度非常快;

  • 在单线程环境中:拿起一把刀拍一个蒜,然后马上拍另一瓣蒜......拍10个蒜的时间花费8秒。
  • 在多线程环境中:拿起一把刀拍一个蒜,然后马上换另一把刀拍一个蒜......拍10个蒜的时间花费15秒。

这样下来,单线程效率更高;

Tips:在上述案例中,不管是"烧水"还是"拍蒜"都是一个人(CPU核心)在操作多个器具(调度多个线程),如果出现了多个人来同时操作多个器具那就不属于并发的范畴了,而是属于并行;

二、Java中的多线程

2.1 Java线程体验

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序代码。

继承Thread类都将变为线程类,调用Thread类中的start()方法即开启线程;当线程开启后,将会执行Thread类中的run方法,因此我们要做的就是重写Thread中的run方法,将线程要执行的任务由我们自己定义;

2.1.1 线程初体验

  • 定义线程类:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro: 继承Thread类称为线程类
 */
public class MyThread extends Thread {
    public MyThread() {
    }
    /**
     * 重写父类的构造方法,传递线程名称给父类
     *
     * @param name
     */
    public MyThread(String name) {
        super(name);
    }
    /*
            重写run方法,当线程开启后,将执行run方法中的程序代码
         */
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(getName() + "线程正在执行: " + i);
        }
    }
}
  • 测试类:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        MyThread thread = new MyThread("线程1");
        // 开启新的线程
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程正执行: " + i);
        }
    }
}

运行结果:

12【多线程、锁机制、lock锁】_多线程_05

运行测试代码,观察是否交替执行;如果没有,可能是因为执行任务太少,CPU分配的一点点时间片就足以将线程中的任务全部执行完毕,可以扩大循环次数;观察效果;

2.1.2 线程执行流程

首先程序运行开启main线程执行代码,执行start()方法时开启一条新的线程来执行任务,新的线程与main线程争夺CPU的执行权在交替执行;

12【多线程、锁机制、lock锁】_线程安全_06

2.2 线程类

2.2.1 常用方法

构造方法:

  • 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():返回对当前正在执行的线程对象的引用。

我们前面定义线程时说到过,run方法中规定了线程执行的任务,因此我们重写run方法即可;

现在我们翻开run方法的源码看看:

public class Thread implements Runnable {
    private volatile String name;
    private int            priority;
    private Thread         threadQ;
	....
    /* What will be run. */
    private Runnable target;
 
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    public Thread(String name) {
        init(null, null, name, 0);
    }
    ...
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    
    ...
}

发现执行的是Runnable对象的run方法,我们打开Runnable查看源码:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

发现Runnable是个接口,并且只有一个抽象方法run()

@FunctionalInterface:标注此注解的接口只有一个抽象方法,也被称为函数式接口;

2.2.2 使用Runnable创建线程

我们前面翻阅源码得知,Thread执行的run方法实质就是执行Runnable接口中的run方法,因此我们可以传递一个Runnable对象给Thread,此Runnable封装了我们要执行的任务;

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

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程;
  • 定义Runnable接口:
package com.dfbz.demo02;
/**
 * @author lscl
 * @version 1.0
 * @intro: 创建一个类实现Runnable接口
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            // 获取当前线程对象的引用
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "执行: " + i);
        }
    }
}
  • 测试类:
package com.dfbz.demo02;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        // 任务对象
        MyRunnable runnable = new MyRunnable();
        // 将任务对象传递给线程执行
        Thread thread = new Thread(runnable,"线程1");
        // 开启线程
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程执行: " + i);
        }
    }
}

运行结果:

12【多线程、锁机制、lock锁】_lock锁_07

2.2.3 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

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

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

扩充:在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

2.2.4 使用匿名内部类创建线程

1)回顾匿名内部类:
  • 定义吃辣接口:
package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public interface Chili {
    void chili();
}
  • 定义人类来实现接口并且重写方法:
package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Person implements Chili {
    @Override
    public void chili() {
        System.out.println("贵州煳辣椒~");
    }
}
  • 测试类(不适用匿名内部类):
package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:  不使用匿名内部类
 */
public class Demo01 {
    public static void main(String[] args) {
        // 需要自己创建一个真实的类(Person),然后重写抽象方法(chili)
        Chili chili=new Person();
        chili.chili();
    }
}
  • 使用匿名内部类:

格式如下:

接口名 xxx=new 父类名或者接口名(){
    // 方法重写
    @Override
    public void method() {
        // 执行语句
    }
};

测试代码:

package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        /*
        相当于:
        class Abc(匿名) implements Chili{
            @Override
            public void chili() {
                System.out.println("余干辣椒~");
            }
        }
        // 多态
        Chili abc=new Abc();
         */
        // 返回的一个Chili的子类(相当于定义了一个匿名的类,并且创建了这个匿名类的实例对象)
        Chili abc = new Chili() {         // abc是Chili接口的子类对象
            // 重写抽象方法
            @Override
            public void chili() {
                System.out.println("余干辣椒~");
            }
        };
        // 调用重写的方法
        abc.chili();
    }
}

我们发现可以直接new接口的方式重写其抽象方法,返回一个该接口的子类(该子类是匿名的);

2)使用匿名内部类创建线程
package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03 {
    public static void main(String[] args) {
        /**
         相当于:
         public class Xxx implements Runnable{
            @Override public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程1执行: " + i);
                }
            }
         }
         Runnable runnable = new Xxx();
         */
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程1执行: " + i);
                }
            }
        };
        // 创建一个线程类,并传递Runnable的子类
        Thread thread = new Thread(runnable);
        // 开启线程
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程: " + i);
        }
    }
}

2.2.5 使用Lambda表达式创建线程

  • 示例代码:
package com.dfbz.demo03;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        // 使用Lambda表达式获取Runnable实例对象
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("线程1: " + i);
            }
        };
        Thread thread = new Thread(runnable);
        thread.run();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程: " + i);
        }
    }
}

2.3 线程的操作

2.3.1 线程的休眠

  • public static void sleep(long millis):让当前线程睡眠指定的毫秒数

测试代码:

package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        // 使用匿名内部类开启1个线程
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //当i等于50的时候让当前线程睡眠1秒钟(1000毫秒)
                    if (i == 50) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }.start();
        // 使用匿名内部类开启第2个线程
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }.start();
    }
}

在JDK1.5退出了TimeUnit类,该类可以根据时间单位来对线程进行睡眠操作;

示例代码:

public static void main(String[] args) {
    new Thread("线程A"){
        @Override
        public void run() {
            try {
                // jdk1.5推出的新的睡眠方法
                TimeUnit.SECONDS.sleep(1);
                System.out.println("线程A....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }.start();
    System.out.println("main..");
}

2.3.2 线程的加入

多条线程时,当指定线程调用join方法时,线程执行权交给该线程,必须等到调用join方法的线程执行完全部任务后才会释放线程的执行权,其他线程才有可能争抢到线程执行权;

  • public final void join():让调用join方法的线程在当前线程优先执行,直至调用join方法的线程执行完毕时,再执行本线程;
  • public final void join(long millis):让线程执行millis毫秒,然后将线程执行权抛出,给其他线程争抢
1)join方法示例

【示例代码】:

package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        //创建线程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程1:" + i);
                }
            }
        });
        //创建线程2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程2:" + i);
                    if (i == 500) {
                        try {
                            //当i等于500的时候,让t1线程加入执行,直至执行完毕
//                            t1.join();
                            //当i等于500的时候,让t1线程加入执行,执行10毫秒之后交出执行权
                            t1.join(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}
2)join方法的应用场景

【join方法小案例】:

static int num = 0;
public static void main(String[] args) {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num=10;
        }
    };
    t1.start();
    
    System.out.println(num);            // ?
}

我们在main线程中开启了一个新的线程(t1),t1线程对num进行赋值,然后再main线程中进行打印,很显然num的值为0,因为t1线程的阻塞不会让main线程也阻塞,当t1线程阻塞时,main线程会继续往下执行;

【使用join方法改造】:

static int num = 0;
public static void main(String[] args) {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num = 10;
        }
    };
    try {
        // 必须让t1线程执行完毕才能执行下面的代码
        t1.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(num);            // 10
}

Tips:join方法一般应用于线程2依赖于线程1执行的返回结果时;

3)join方法注意事项

【注意事项1】:当线程执行join方法传递时间参数时,如果join线程任务执行完毕,则不必等待join时间结束;

static int count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count = 10;
        }
    };
    long startTime = System.currentTimeMillis();
    t1.start();
    // 让t1线程执行完毕
//    t1.join();
    // 让t1线程执行1s,然后代码继续往下执行
    t1.join(1000);
    // 让t1线程执行3s,但如果t1线程执行完毕了,该方法也会结束
//        t1.join(3000);
    long endTime = System.currentTimeMillis();
    // count【10】,time【2011】
    System.out.printf("count【%s】,time【%s】", count, (endTime - startTime));
}
  • 执行效果如下:
t1.join();
count【10】,time【2003】
----------------------------------------
t1.join(1000);
count【0】,time【1005】
----------------------------------------
t1.join(3000);
count【10】,time【2006】

【注意事项2】:当线程执行join方法时,优先执行join线程的任务,等到join线程任务执行完毕时才会执行本线程,但如果还有其他线程与执行join方法的线程同时存在时,则其他线程与join线程交替执行;

public static void main1(String[] args) {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    };
    Thread t2 = new Thread("t2") {
        @Override
        public void run() {
            while (true) {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    Thread t3 = new Thread("t3") {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    };
    t1.start();
    t2.start();
    t3.start();
}

执行代码,发现t1和t3线程交替执行;

2.3.3 守护线程

当用户线程(非守护线程)运行完毕时,守护线程也会停止执行,但由于CPU运行速度太快,当用户线程执行完毕时,将信息传递给守护线程,会有点时间差,而这些时间差会导致还会执行一点守护线程;

Tips:不管开启多少个线程(用户线程),守护线程总是随着第一个用户线程的停止而停止,例如JVM的垃圾回收器线程就是一个守护线程;

  • public final void setDaemon(boolean on):设置线程是否为守护线程

示例代码:

package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 2000; i++) {
                    System.out.println("守护线程1: " + i);
                }
            }
        });
        //将t1设置为守护线程
        t1.setDaemon(true);
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("用户线程2: " + i);
                }
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("用户线程3: " + i);
                }
            }
        });
        //开启三条线程,不管是t2还是t3线程执行完毕,守护线程都会停止
        t1.start();
        t2.start();
        t3.start();
    }
}

2.3.4 线程优先级

默认情况下,所有的线程优先级默认为5,最高为10,最低为1。优先级高的线程更容易让线程在抢到线程执行权;

通过如下方法可以设置指定线程的优先级:

  • public final void setPriority(int newPriority):设置线程的优先级。

示例代码:

package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程1: " + i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("线程2: " + i);
                }
            }
        });
        //设置优先级
        t1.setPriority(1);
        t2.setPriority(10);
        t1.start();
        t2.start();
    }
}

2.3.5 线程礼让

在多线程执行时,线程礼让,告知当前线程可以将执行权礼让给其他线程,礼让给优先级相对高一点的线程,但仅仅是一种告知,并不是强制将执行权转让给其他线程,当前线程将CPU执行权礼让出去后,也有可能下次的执行权还在原线程这里;如果想让原线程强制让出执行权,可以使用join()方法

  • public static void yield():将当前线程的CPU执行权礼让出来;

示例代码:

package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("线程1: " + i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i == 10) {
                        //当i等于10的时候该线程礼让(礼让之后有可能下次线程执行权还被线程2抢到了)
                        Thread.yield();
                    }
                    System.out.println("线程2: " + i);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

2.3.6 线程中断

1)interrupt中断线程
  • public void interrupt():将当前线程中断执行,并且将线程的中断标记设置为true;但是需要注意,如果被中断的线程正在sleep、wait、join等操作,那么将会出现InterruptedException异常,并且清空打断标记(此时打断标记还是false);
  • public boolean isInterrupted():获取当前线程的中断标记;

示例代码:

package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo13_线程中断 {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            while (true) {
 				System.out.println("t1: " + Thread.currentThread().isInterrupted());
            }
        }, "t1");
        t1.start();
        Thread.sleep(10);
        t1.interrupt();         // 中断线程,将中断状态设置为true
        System.out.println(t1.isInterrupted());         // true
    }
}

Tips:中断线程并且不是将线程停止,只是将线程的中断标记设置为true;

借助中断标记,我们可以采用如下的方式来优雅的停止线程:

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        while (true) {
            // 获取当前线程的中断标记
            boolean interrupted = Thread.currentThread().isInterrupted();
            if (interrupted) {
                System.out.println("线程被中断【" + interrupted + "】....");
                System.out.println("释放资源....");
                break;
            } else {
                System.out.println("执行任务【" + interrupted + "】.....");
            }
        }
    }, "t1");
    t1.start();
    Thread.sleep(10);
    t1.interrupt();         // 中断线程,将中断状态设置为true
}
2)中断线程的其他情况

需要注意的是,被中断的线程如果正在处于sleep、wait、join等操作中,将会抛出InterruptedException异常,然后清空打断标记(此时打断标记还是false);

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t1");
    // 启动线程
    t1.start();
    Thread.sleep(50);
    // 中断t1线程,将中断标记设置为true(但此时t1线程正在sleep,因此线程会出现异常,并且中断标记还是false)
    t1.interrupt();
    System.out.println(t1.isInterrupted());
}

2.3.7 线程的其他方法

1)线程退出
  • public final void stop():退出当前线程

示例代码:

package com.dfbz.demo04_线程的其他操作;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_线程的退出 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("hello【" + i + "】");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        // 退出线程
        t1.stop();
        System.out.println("end");
    }
}
2)线程挂起
  • public final void suspend():暂停当前线程的执行;
  • public final void resume():恢复被暂停的线程;

示例代码:

package com.dfbz.demo04_线程的其他操作;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_线程的挂起与恢复 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("hello【" + i + "】");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        // 挂起线程
        t1.suspend();
        System.out.println("线程挂起...");
        Thread.sleep(2000);
        t1.resume();
        System.out.println("线程恢复....");
    }
}

2.4 Callable实现线程

2.4.1 Callable的使用

我们前面学习过,Thread是Java中的线程类,Runnable接口封装了线程所要执行的任务;当线程开启后(调用start方法)则会执行Runnable中的run方法;Callable适用于执行某个任务后需要有返回值响应的情况。例如发送短信是否成功、订单是否更新成功、发起远程调用响应的结果等....

  • Callable使用示例:
package com.dfbz.demo02;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws Exception {
        // 创建一个Callable任务
        MyCallable myCallable = new MyCallable();
        // 封装成task(线程要执行的任务,最终会执行task里面封装的Callable里面的任务)
        FutureTask<String> task1 = new FutureTask<>(myCallable);
        FutureTask<String> task2 = new FutureTask<>(myCallable);
        // 开启线程执行任务
        new Thread(task1).start();
        new Thread(task2).start();
        // 获取任务执行结果(会造成线程阻塞,必须等线程任务完全执行完毕才会有结果返回)
        Object result_1 = task1.get();
        Object result_2 = task2.get();
        System.out.println("执行结果:【" + result_1 + "】");
        System.out.println("执行结果:【" + result_2 + "】");
    }
}
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "【" + i + "】");
        }
        return "执行任务成功!";
    }
}

2.4.2 Callable案例

创建API类,分别提供发送短信方法、文件下载方法;使用异步(使用多线程)和非异步方式(不使用多线程),查看执行效率;

package com.dfbz.demo02;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws Exception {
        // 没有使用多线程异步调用
        sync();
//        async();
    }
    // 异步调用
    public static void async() throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();
        Api api = new Api();
        // 发送短信的任务
        Callable<String> msgCallable = new Callable<String>() {
            @Override
            public String call() {
                String result = api.sendMsg();
                return result;
            }
        };
        // 下载文件的任务
        Callable<String> uploadCallable = new Callable<String>() {
            @Override
            public String call() {
                String result = api.sendMsg();
                return result;
            }
        };
        // 封装成Task
        FutureTask<String> msgTask = new FutureTask<String>(msgCallable);
        FutureTask<String> uploadTask = new FutureTask<String>(uploadCallable);
        // 执行任务
        new Thread(msgTask).start();
        new Thread(uploadTask).start();
        // 获取线程任务执行的结果集
        String msgResult = msgTask.get();
        String uploadResult = msgTask.get();
        System.out.println("发送短信:【" + msgResult + "】");
        System.out.println("下载文件:【" + uploadResult + "】");
        long endTime = System.currentTimeMillis();
        System.out.println("花费时间:【" + (endTime - startTime) + "】");
    }
    // 同步调用
    public static void sync() {
        long startTime = System.currentTimeMillis();
        Api api = new Api();
        // 发送短信
        String msgResult = api.sendMsg();
        // 下载文件
        String uploadResult = api.upload();
        System.out.println("发送短信:【" + msgResult + "】");
        System.out.println("下载文件:【" + uploadResult + "】");
        long endTime = System.currentTimeMillis();
        System.out.println("花费时间:【" + (endTime - startTime) + "】");
    }
}
class Api {
    /**
     * 模拟发送短信
     *
     * @return
     */
    public String sendMsg() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "短信发送成功!";
    }
    /**
     * 模拟下载文件
     *
     * @return
     */
    public String upload() {
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "文件下载成功!";
    }
}

三、线程安全

3.1 线程安全问题

我们前面的操作线程与线程间都是互不干扰,各自执行,不会存在线程安全问题。当多条线程操作同一个资源时,就会产生线程安全问题;

我们来举一个案例,从广州开往南昌的票数共有100张票,售票窗口分别有“广州南站”、“广州北站”、“广州站”等。

  • 定义卖票任务:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Ticket implements Runnable {
    //票数
    private Integer ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) {
                break;      //票卖完了
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第: " + (101 - ticket) + "张票");
            ticket--;
        }
    }
}
  • 测试类:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Ticket ticket = new Ticket();
        //开启三个窗口,买票
        Thread t1 = new Thread(ticket, "广州南站");
        Thread t3 = new Thread(ticket, "广州北站");
        Thread t2 = new Thread(ticket, "广州站");
        t1.start();
        t2.start();
        t3.start();
    }
}

查看运行结果:

12【多线程、锁机制、lock锁】_并发_08

发现程序出现了两个问题:

  1. 有的票卖了多次
  2. 卖票顺序不一致

分析卖了多次票:

12【多线程、锁机制、lock锁】_多线程_09

分析卖票顺序不一致:

12【多线程、锁机制、lock锁】_并发_10

3.2 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:窗口1线程操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

Java中提供了三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

3.2.1 同步代码块

1)同步代码块改造买票案例

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

语法:

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

同步锁

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

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

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

12【多线程、锁机制、lock锁】_线程安全_11

使用同步代码块改造代码:

package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Ticket implements Runnable {
    //票数
    private Integer ticket = 100;
    //锁对象
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            // 加上同步代码块,把需要同步的代码放入代码块中,同步代码块中的锁对象必须保证一致!
            synchronized (obj) {
                if (ticket <= 0) {
                    break;      // 票卖完了
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第: " + (101 - ticket) + "张票");
                ticket--;
            }
        }
    }
}

2)同步代码块案例

案例:要么输出"犯我中华者",要么输出"虽远必诛"

package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        //锁对象
        Object obj = new Object();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //加上同步代码块锁住
                    synchronized (obj) {
                        System.out.print("犯");
                        System.out.print("我");
                        System.out.print("中");
                        System.out.print("华");
                        System.out.print("者");
                        System.out.println();
                    }
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //加上同步代码块锁住
                    synchronized (obj) {
                        System.out.print("虽");
                        System.out.print("远");
                        System.out.print("必");
                        System.out.print("诛");
                        System.out.println();
                    }
                }
            }
        }.start();
    }
}

3)字节码对象

在使用同步代码块时,必须保证锁对象是同一个,才能实现线程的同步,不能使用不同的对象来锁不同的代码块;那么有什么对象只会存在一份的吗?答:任何类的字节码对象;

任何类的字节码对象都只会存在一次,在类加载的时候由JVM创建的;因此字节码锁也成为万能锁;

  • 获取一个类的字节码对象有三种方式:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo {
    public static void main(String[] args) throws Exception{
        // 任何类都会有一个class属性,通过类的class属性可以获取这个类的字节码对象
        Class<String> stringClass = String.class;
        // Object中有一个getClass()方法,该方法是获取这个类的字节码对象
        Class<? extends String> stringClass2 = new String().getClass();
        // Class类有一个静态方法forName(String className),该方法可以通过一个全包名获取一个类的字节码对象
        Class<?> stringClass3 = Class.forName("java.lang.String");
        System.out.println(stringClass == stringClass2);            // true
        System.out.println(stringClass == stringClass3);            // true
    }
}

Tips:以上三种方式都是获取JVM创建的字节码对象,而不是创建一个字节码对象,所有类的字节码对象都是在类加载的时候由JVM创建的;

  • 使用字节码对象来作为锁对象:
package com.dfbz.demo01;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized (Object.class) {           // 使用字节码对象作为锁对象
                        System.out.print("犯");
                        System.out.print("我");
                        System.out.print("中");
                        System.out.print("华");
                        System.out.print("者");
                        System.out.println();
                    }
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized (Object.class) {           // 使用字节码对象作为锁对象
                        System.out.print("虽");
                        System.out.print("远");
                        System.out.print("必");
                        System.out.print("诛");
                        System.out.println();
                    }
                }
            }
        }.start();
    }
}

3.2.2 同步方法

1)普通同步方法

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

注意:同步方法也是有锁对象的,对于静态方法的锁对象的当前类的字节码对象(.class),对于非静态的方法的锁对象是this;

语法:

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

使用同步方法:

package com.dfbz.demo02;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        Shower s = new Shower();
        //开启线程1调用show方法
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    s.show();
                }
            }
        }.start();
        //开启线程2调用show2方法
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    s.show2();
                }
            }
        }.start();
    }
}
class Shower {
    //非静态同步方法的锁对象默认是this
    public synchronized void show() {
        System.out.print("犯");
        System.out.print("我");
        System.out.print("中");
        System.out.print("华");
        System.out.print("者");
        System.out.println();
    }
    public void show2() {
        //使用this锁也能够保证代码同步
        synchronized (this) {
            System.out.print("虽");
            System.out.print("远");
            System.out.print("必");
            System.out.print("诛");
            System.out.println();
        }
    }
}

2)静态同步方法

普通同步方法的锁对象是当前对象的引用(this),静态同步方法的锁对象是当前类的字节码对象;

  • 示例代码:
package com.dfbz.demo02;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        Shower s = new Shower();
        //开启线程1调用show方法
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    s.show();
                }
            }
        }.start();
        //开启线程2调用show2方法
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    s.show2();
                }
            }
        }.start();
    }
}
class Shower2 {
    // 静态同步方法的锁对象是当前类的字节码对象
    public synchronized void show() {
        System.out.print("犯");
        System.out.print("我");
        System.out.print("中");
        System.out.print("华");
        System.out.print("者");
        System.out.println();
    }
    public void show2() {
        // 静态同步方法的锁对象是当前类的字节码对象
        synchronized (Shower2.class) {
            System.out.print("虽");
            System.out.print("远");
            System.out.print("必");
            System.out.print("诛");
            System.out.println();
        }
    }
}

3.2.3 Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

示例代码:

package com.dfbz.demo03;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        //创建锁对象
        ReentrantLock lock = new ReentrantLock();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    //开启锁
                    lock.lock();
                    System.out.print("虽");
                    System.out.print("远");
                    System.out.print("必");
                    System.out.print("诛");
                    System.out.println();
                    //释放锁
                    lock.unlock();
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    //开启锁
                    lock.lock();
                    System.out.print("犯");
                    System.out.print("我");
                    System.out.print("中");
                    System.out.print("华");
                    System.out.print("者");
                    System.out.println();
                    //释放锁
                    lock.unlock();
                }
            }
        }.start();
    }
}

3.2.4 线程死锁

多线程同步的时候,如果同步代码嵌套,使用相同锁,就有可能出现死锁;

  • 分析:

12【多线程、锁机制、lock锁】_锁机制_12

  • 示例代码:
package com.dfbz.demo04;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        String s1 = "s1";
        String s2 = "s2";
        new Thread() {
            public void run() {
                while (true) {
                    synchronized (s1) {
                        System.out.println(this.getName() + "s1");
                        synchronized (s2) {
                            System.out.println(this.getName() + "s2");
                        }
                    }
                }
            }
        }.start();
        new Thread() {
            public void run() {
                while (true) {
                    /*
					当线程0拿到s1执行s1中的打印语句时,如果线程切换到线程1
					那么线程1拿到了s2锁对象,此时就造成了线程死锁
					线程1想执行s1锁里面的代码执行不了,因为s1在线程0中还没有释放
					那么此时线程1就会切换到线程0
                    线程0也不会执行s2锁里面的代码,因为此时s2已经被线程0中的锁拿去了
                    还没有释放,因此造成了线程的死锁
                    两个都没有释放  都卡住了  线程就卡住了
                     */
                    synchronized (s2) {
                        System.out.println(this.getName() + "s2");
                        synchronized (s1) {
                            System.out.println(this.getName() + "s1");
                        }
                    }
                }
            }
        }.start();
    }
}

3.3 集合的线程安全问题

3.3.1 线程安全与不安全集合

我们前面学习集合的时候发现集合存在由线程安全集合和线程不安全集合;线程安全效率低,安全性高;反之,线程不安全效率高,安全性低,线程不安全的集合有:Vector,Stack,Hashtable等;

  • 查看Vector和Hashtable等源代码:

12【多线程、锁机制、lock锁】_锁机制_13

线程安全集合中的方法大部分都加上了synchronized关键字来保证线程的同步;

  • 线程不安全集合:

12【多线程、锁机制、lock锁】_lock锁_14

3.3.2 线程不安全集合测试

  • 数据覆盖问题:
package com.dfbz.demo05;
import java.util.ArrayList;
/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_集合的线程安全问题 {
    public static void main(String[] args) throws InterruptedException {
        ArrayList<String> arr = new ArrayList<>();
        for (int j = 0; j < 20; ++j) {
            new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    arr.add("1");
                    try {
                        // 然线程安全问题更加突出
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

运行代码,发现出现数组下标越界异常:

12【多线程、锁机制、lock锁】_线程安全_15

分析ArrayList源码:

  • 假设此时size为9,size+1并没有大于数组的默认长度(10),并没有造成数组的扩容,等待代码将集合的9下标赋值后,size还没来得及运算,CPU的执行权就被其他的线程抢走了,此时size仍旧为9,但此时集合中已经存储了10个元素了;等到其他线程来执行ensureCapacityInternal(9+1)--->ensureCapacityInternal--->ensureExplicitCapacity发现10-10还是小于0,依旧不扩容,代码执行elementData[size]=e时(还没执行),线程执行权由回到了第一条线程,size++,变为10,然后线程执行器又变回执行elementData[size++]=e这段代码时的那个线程,出现了elementData[10]=e,出现数组下标越界;

12【多线程、锁机制、lock锁】_lock锁_16

Tips:HashMap同样会出现这个问题,将集合换成Vector或者Stack等线程安全集合可以解决这些问题;或者使用JDK提供的其他线程同步集合也可以解决这些问题;




上一篇11【泛型、Map、异常】

下一篇13【线程等待、状态、线程池、File类】

JavaSE系列教程目录【JavaSE零基础系列教程目录】