前几天学习了 Java 多线程,作为一个懒癌后期的患者,一直拖到现在才把所学的记录下来,也算是复习了一遍 😂。希望大家多多支持喔!
在学习线程之前,我们先来了解一下进程吧!
进程
概述:正在运行的程序就是进程。进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
通过任务管理器,我们可以看到我们电脑现在的进程有哪些:
多进程的意义:计算机可以在一个时间段内同时执行多个任务,可以提高CPU的使用率。
思考:我们一边听音乐(网易云进程),一边写代码(IDEA进程),这两个任务是同时的吗?
对于多核CPU 它有可能是同时的,但是对于单核CPU来说,它在某一个时间点,它只能做一件事情。
但是我们在听音乐的时候,同时在写代码,我们感官上,这两个任务是同时进行的。但是实际CPU在运行进程的时候进行了程序间的高速切换,这个切换时间非常的短,所以我们就感觉两个进程是在同时进行的。
线程
在同一个进程中,可以同时执行多个任务。而这每一个任务,就是一个线程。
线程:是程序的执行单元,也是执行路径。线程是程序使用CPU资源的最基本单位。
单线程:只有一个执行单元或只有一条执行路径
多线程:有多个执行单元或多个执行路径
例如:我们平时写的这些程序使单线程的
public class Test {
public static void main(String[] args) {
System.out.println("代码块1");
method();
System.out.println("代码块2");
}
public static void method() {
System.out.println("代码块3");
function1();
function2();
System.out.println("代码块4");
}
private static void function1() {
}
private static void function2() {
}
}
多线程的意义:
- 线程的执行是抢占式的。每一个线程都要去抢占CPU资源(CPU执行权)。一个多线程的程序在执行时,如果一些线程必须等待的时候,CPU就会将资源提供给其他线程去使用这些资源。这样的话就提高了CPU的使用率。
- 对于进程来说,如果它是多线程的,在抢占CPU资源时,就有更大的几率抢占到CPU资源。提高该程序使用率。
多线程
实现方式一:
继承 Thread 类,重写 run() 方法。
步骤:
1 自定义 MyThread 类,继承 Thread 类
2 重写 run() 方法
3 创建 MyThread 对象
4 启动线程
public class MyThread extends Thread {
// 重写 run() 方法
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 创建 MyThread 对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
注:run() 和 start() 的区别是什么?
run():仅仅封装了线程所执行的代码,直接调用和普通方法没有区别。
start():首先启动线程,然后由 JVM 调用该线程的 run() 方法。
- 获取线程名称:
public final String getName()
:返回此线程的名称。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(getName() + i);
}
}
}
- 设置线程名称:
法一:public final void setName(String name)
:将此线程的名称更改为参数 name
myThread.setName("线程一");
法二:构造方法
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(getName() + i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 使用有参构造:
MyThread myThread = new MyThread("线程一:");
myThread.start();
MyThread myThread1 = new MyThread("线程二:");
myThread1.start();
}
}
- 获取当前正在执行的线程名称:
public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
String name = Thread.currentThread().getName();
System.out.println(name);
- 线程优先级:
线程有两种调度模型:
1、分时调度模型:所有的线程轮流使用 CPU,平均分配每个线程占用 CPU 的时间段。
2、抢占式调度模型:Java 是抢占式调度模型,会优先让优先级高的执行;优先级相同的线程,随机执行一个。(注:优先级高只代表它抢到 CPU 的概率较大,不一定必须是先执行的)
获取优先级的方法:public final int getPriority()
:返回此线程的优先级
设置优先级的方法:public final int setPriority(int newPriority)
:设置此线程的优先级
// 设置线程优先级。
myThread1.setPriority(10);
myThread3.setPriority(1);
// 获取线程优先级。
System.out.println(myThread1.getPriority());
System.out.println(myThread2.getPriority());
System.out.println(myThread3.getPriority());
/*
输出的结果:
10
5
1
*/
注:1. 默认优先级是5
2. 优先级的取值范围是 1-10
线程控制
- sleep():线程睡眠
public static void sleep(long miles)
:导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数
public class MyThread3 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("有异常!");
}
System.out.println(getName() + "-" + i);
}
}
}
public class MyThreadDemo3 {
public static void main(String[] args) {
MyThread3 myThread1 = new MyThread3();
MyThread3 myThread2 = new MyThread3();
MyThread3 myThread3 = new MyThread3();
myThread1.setName("喜羊羊");
myThread2.setName("美羊羊");
myThread3.setName("灰太狼");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
执行上面的程序,可以看到每个线程都是输出一次之后等待一秒再继续输入
- interrupt():线程中断
public void interrupt()
:中断线程
public class MyThread7 extends Thread {
@Override
public void run() {
System.out.println("线程开始执行" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("程序出现错误!");
}
System.out.println("线程结束执行" + new Date());
}
}
public class MyThreadDemo7 {
public static void main(String[] args) {
MyThread7 myThread1 = new MyThread7();
//启动线程
myThread1.start();
// myThread1 休眠时间超过三秒,就终止它。
try {
// 主线程休眠三秒。
Thread.sleep(3000);
myThread1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- join():线程加入
public void join()
:等待该线程终止
public class MyThread4 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + ": " + i);
}
}
}
public class MyThreadDemo4 {
public static void main(String[] args) {
// 创建线程类对象
MyThread4 myThread1 = new MyThread4();
MyThread4 myThread2 = new MyThread4();
MyThread4 myThread3 = new MyThread4();
// 设置名称。
myThread1.setName("线程一");
myThread2.setName("线程二");
myThread3.setName("线程三");
myThread1.start();
// 加入线程。
try {
myThread1.join();
} catch (InterruptedException e) {
System.out.println("出错了!");
}
myThread2.start();
myThread3.start();
}
}
执行上面程序,线程一执行完毕后,后面两个线程才开始抢占资源,进行执行
- yield():线程礼让
public static void yield()
:暂停当前正在执行的线程对象,并执行其他线程(可以减小抢占竞争)
public class MyThread5 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + ": " + i);
// 线程礼让
Thread.yield();
}
}
}
public class MyThreadDemo05 {
public static void main(String[] args) {
// 创建线程类对象
MyThread5 myThread1 = new MyThread5();
MyThread5 myThread2 = new MyThread5();
// 设置名称。
myThread1.setName("Andy");
myThread2.setName("Jay");
myThread1.start();
myThread2.start();
}
}
- setDeman():线程守护
public final void setDaemon(boolean on)
:将该线程标记为守护线程或用户线程(当正在运行的线程都是守护线程时,Java 虚拟机退出,程序结束)
public class MyThread6 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + ": " + i);
}
}
}
public class MyThreadDemo6 {
public static void main(String[] args) {
MyThread6 myThread1 = new MyThread6();
MyThread6 myThread2 = new MyThread6();
myThread1.setName("线程一");
myThread2.setName("线程二");
// 守护线程设置为true。主基地结束,其余两个线程一会也会结束。
myThread1.setDaemon(true);
myThread2.setDaemon(true);
myThread1.start();
myThread2.start();
// 获取主线程。
Thread.currentThread().setName("主基地:");
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
线程的生命周期:
实现方式二:
实现 Runnable 接口,重写 run() 方法。然后可以分配类的实例,在创建 Thread 时作为参数传递。
步骤:
1.创建自定义线程类。
2.重写 run 方法
3.创建自定义线程类对象
4.创建多个Thread类对象,将自定义线程类对象作为参数传递。
5.启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
// getName() 是Thread的方法,所以在这里应该这样获取线程名称
System.out.println(Thread.currentThread().getName() + i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建自定义线程类对象
MyRunnable mr = new MyRunnable();
// 创建多个Thread类对象,将自定义线程类对象作为参数传递
Thread t1 = new Thread(mr, "线程一");
Thread t2 = new Thread(mr, "线程二");
// 启动线程
t1.start();
t2.start();
}
}
注:为什么创建线程的第一种方法还要有第二种方法?
- 继承只能单继承,如果自定义线程类有父类,则它不能再继承Thread
- 第二种方式适合多个线程操作同一个资源这种情况,比较简洁。把线程和程序代码 数据进行有效分离,较好地体现了面向对象的思想。
案例:共有150张票,创建三个线程,模拟电影院三个窗口的卖票情况。
public class MyRunnable implements Runnable {
// 如果这里是继承 Thread 类,则需要用static来修饰票数,以保证三个线程共享同一个资源,三个窗口共卖这150张票。
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
// 根据现实情况,卖票会出现延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "张票。");
}
}
}
}
执行上面程序,会发现有一票多卖和负数票的情况,该问题与线程安全有关。
线程安全:
出现原因:
- 多线程环境
- 存在共享数据
- 存在多条语句操作该共享数据
为了解决这个问题可以将操作共享数据的这段代码包裹起来,在有线程访问这段代码时,其他线程不能访问。
同步代码块:synchronize
关键字:
synchronize(对象名){
多条语句;
}
同步代码块的对象是任意对象。
如果给这些线程传递同一个对象,就是相当于给了一个门,一把锁;如果传递不同的对象,就相当于有多个门,多把锁。
public class MyRunnable implements Runnable {
private int tickets = 50;
private Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
if (tickets > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出去标号为 " + (tickets--) + "的这张票");
}
}
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr, "窗口一:");
Thread t2 = new Thread(mr, "窗口二:");
Thread t3 = new Thread(mr, "窗口三:");
t1.start();
t2.start();
t3.start();
}
}
注:如果使用继承 Thread 类的方法,则需要将票数和锁对象声明为静态的,以保证为所有对象共用。
private static int ticket = 50;
private static Object obj = new Object();
执行以上代码,上面的一票多卖和负数票的问题都被解决了。
同步代码块的优缺点:
优点:解决了线程安全问题。
缺点:每个线程在执行前都要去判断锁对象,无形中增加了电脑负担。
同步方法:
格式一:synchronized
权限修饰符 返回值类型 方法名()
格式二:权限修饰符 synchronized
返回值类型 方法名()
public class SaleTicket implements Runnable {
private int ticket = 50;
@Override
public void run() {
ticket();
}
private synchronized void ticket() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "张票");
}
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
SaleTicket st = new SaleTicket();
Thread t1 = new Thread(st, "窗口一:");
Thread t2 = new Thread(st, "窗口一:");
Thread t3 = new Thread(st, "窗口一:");
t1.start();
t2.start();
t3.start();
}
}
同步方法的锁对象是this,而如果该同步方法是静态的,由于静态方法随着类的加载而加载,那么它的锁对象是该类的字节码文件(类名.class)。
Lock锁Lock
:这是一个接口,实现了比 synchronize
更语句和方法更广泛的操作。
实现子类:ReentrantLock
。
成员方法:void lock()
上锁;void unlock
解锁。
public class SaleTicket implements Runnable {
private int tickets = 50;
// 使用多态的方法创建锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 上锁。
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出去标号为 " + (tickets--) + "的这张票");
}
// 解锁
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread thread1 = new Thread(s, "窗口一:");
Thread thread2 = new Thread(s, "窗口二:");
Thread thread3 = new Thread(s, "窗口三:");
thread1.start();
thread2.start();
thread3.start();
}
}
注:上锁和解锁的位置和同步代码块的位置相同。
死锁:
同步代码块的弊端:效率低,而且如果出现了同步嵌套,就容易产生死锁的问题。
死锁:是指两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。
public class deadLock extends Thread {
private boolean flag;
private static Object lockA = new Object();
private static Object lockB = new Object();
// 构造方法
public deadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (lockA) {
System.out.println("if lockA");
synchronized (lockB) {
System.out.println("if lockB");
}
}
} else {
synchronized (lockB) {
System.out.println("else lockB");
synchronized (lockA) {
System.out.println("else lockA");
}
}
}
}
}
public class deadLockDemo {
public static void main(String[] args) {
deadLock deadLock1 = new deadLock(true);
deadLock deadLock2 = new deadLock(false);
deadLock1.start();
deadLock2.start();
}
}
生产者消费者模型
线程间通讯:不同种类的线程针对同一个资源进行操作
例如:不同线程操作同一个学生对象,一个线程用来设置学生对象,一个线程用来获取学生对象。
public class Student {
private String name;
private int age;
// 无参构造
// 有参构造
// get,set 方法
// 此处省略
}
public class SetThread implements Runnable {
private Student student;
private int x = 0;
public SetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while (true) {
synchronized (student) {
if (x % 2 == 0) {
student.setName("William");
student.setAge(30);
} else {
student.setName("Andy");
student.setAge(5);
}
x++;
}
}
}
}
public class GetThread implements Runnable {
private Student student;
public GetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while (true) {
synchronized (student) {
System.out.println(student.getName() + "---" + student.getAge());
}
}
}
}
public class StudentDemo {
public static void main(String[] args) {
// 创建学生对象
Student student = new Student();
// 创建自定义线程类对象
SetThread setThread = new SetThread(student);
GetThread getThread = new GetThread(student);
// 创建Thread类对象,将自定义线程类对象作为参数传递
Thread thread = new Thread(setThread,"线程一");
Thread thread2 = new Thread(getThread,"线程二");
// 启动线程
thread.start();
thread2.start();
}
}
生产者消费者模型线程安全问题
出现问题:
a. 相同数据出现多次()
b. 姓名和年龄不匹配(CPU执行的原子性和线程的随机性导致)
等待唤醒机制
问题:
第一次执行的时候,如果消费者先抢到 CPU 执行权,它就会去消费数据,但是此时的数据是默认值,没有任何意义,所以应该等待生产者生产完数据之后,再去消费。
如果生产者抢到 CPU 执行权,它就会生产数据,但是,如果下一次都是生产者抢到 CPU 执行权,它就会重复生产数据,这样是不合理的。应该等待消费者消费掉数据之后,再继续生产。
解决思路:
生产者:先看是否有数据,如果有,就等待,如果没有就生产数据。生产完毕之后通知消费者前来消费。
消费者:先看是否有数据,如果没有,就等待,如果有,就消费。消费完之后通知生产者生产数据。wait()
:线程等待notify()
:唤醒等待的线程
线程组
当项目中有许多线程需要设置一些相同的属性,比如都设置成守护线程。我们可以考虑根据线程的功能或者用途进行分组,然后针对组进行统一管理,这样的好处是方便分类操作和管理。
默认分组:
public static void method1() {
MyThreadGroup tg = new MyThreadGroup();
Thread thread1 = new Thread(tg);
Thread thread2 = new Thread(tg);
System.out.println(thread1.getThreadGroup().getName());
System.out.println(thread2.getThreadGroup().getName());
System.out.println(Thread.currentThread().getThreadGroup().getName());
}
设置分组:
public static void method2() {
ThreadGroup threadGroup = new ThreadGroup("守护线程");
MyThreadGroup tg = new MyThreadGroup();
Thread thread1 = new Thread(threadGroup, tg);
Thread thread2 = new Thread(threadGroup, tg);
System.out.println(thread1.getThreadGroup().getName());
System.out.println(thread2.getThreadGroup().getName());
System.out.println(Thread.currentThread().getThreadGroup().getName());
// 将这个组设置成守护线程
threadGroup.setDaemon(true);
}
线程池
在创建线程时,成本是比较高的,因为每一次创建线程时,都要与操作系统交互。而线程池会在程序启动时,提前创建一些线程放在线程池中等待使用,这样可以大大的提高执行效率。
特点:线程执行完毕后,不会死亡,而是重新回到线程池中,成为空闲状态等下下一个线程使用。
JDK5 之前需要手动配置线程池,JDK5 之后Java开始内置线程池。Executors
:工厂类
通过下面的方法获得线程池对象:
1、
2、public static ExecutorService newFixedThreadPool(int nThreads)
:创建一个可重用固定线程数的线程池
3、public static ExecutorService newSingleThreadExecutor()
:创建一个使用单个 worker 线程的 Executor
这些方法的返回值是 ExecutorService
对象,该对象表示一个线程池,它可以执行 Runnable对象 或者 Callable对象 对象代表的线程池。
操作步骤:
1、创建线程池对象
2、创建自定义类对象
3、提交 MyRunnable 到线程池
4、关闭线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1 创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
// 2 创建自定义线程类的对象
MyThreadPool mtp = new MyThreadPool();
// 3 提交Runnable实例
es.submit(mtp);
es.submit(mtp);
// 4 关闭线程池
es.shutdown();
}
}
多线程第三种实现方式: Callable
Callable:这是一个接口,类似于 Runnable 接口,但是,Callable有返回值,并且可以抛出异常。
所以,如果某些线程执行完毕后需要给我们返回一个执行结果时,我们可以使用Callable接口这些方式来实现多线程。
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 30; i++) {
System.out.println(i);
}
return null;
}
}
public class MyCallableDemo {
public static void main(String[] args) {
// 1 创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
// 2 创建自定义类对象
MyCallable mc = new MyCallable();
// 3 提交 Callable 对象
es.submit(mc);
es.submit(mc);
}
}
案例:使用两个线程分别求 1-100 的和(泛型的使用)
public class MyCallable implements Callable<Integer> {
private int num;
public MyCallable(int num) {
this.num = num;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < num; i++) {
sum += i;
}
return sum;
}
}
public class MyCallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(2);
MyCallable mc = new MyCallable(100);
MyCallable mc2 = new MyCallable(200);
Future<Integer> future = es.submit(mc);
Future<Integer> future2 = es.submit(mc2);
int num = future.get();
int num2 = future2.get();
System.out.println(num);
System.out.println(num2);
es.shutdown();
}
}
使用匿名内部类实现多线程
匿名内部类格式:
new 类名或接口名(){
重写方法;
};
new Thread(){
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
};
new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
){};
new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("hello" + i);
}
}
}
){
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("world" + i);
}
}
};
注:当同时重写了 Thread类 和 Runnable接口 中的 run() 方法,运行时执行的是 Thread类 中的 run() 方法。
定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台的方式执行,可以通过 Timer 和 TimerTask 类来实现定义和调度的功能。
概念:可以指定程序在某个指定的时间做某件工作。
Timer :线程的工具,用于在后台线程中安排将来执行的任务。可以将任务安排为一次性执行,或者以固定间隔重复执行。
方法:Timer()
:创建一个新的定时器。public void schedule(TimerTask task, long delay)
:在指定毫秒时间后执行task任务。public void schedule(TimerTask task, long delay, long period)
:在指定毫秒时间后执行task任务,并在指定间隔时间后再次执行。
TimerTask :可由 Timer 一次性或重复执行的任务。
方法:public boolean cancel()
:取消此定时器任务。public abstract void run()
:重写run()方法,重写的内容即要执行的任务。
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer timer = new Timer();
// 执行 Task 任务
timer.schedule(new MyTask(timer), 3000);
}
}
class MyTask extends TimerTask {
private Timer timer;
public MyTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
System.out.println("有内鬼,终止交易");
timer.cancel();
}
}
执行以上代码,可以看到三秒之后执行 run() 方法中的语句。
我是快斗,请多多指教!