Java语言基础(SE)-第八节 并发编程(Concurrent Programming)
线程的概念
进程(Process)
- 什么是进程?
- 在操作系统中运行的一个应用程序,比如同时打开QQ、微信,操作系统就会分别启动2个进程
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
- 在 Windows 中,可以通过“任务管理器”查看正在运行的进程
线程(Thread)
- 什么是线程?
- 1个进程要想执行任务,必须得有线程(每1个进程至少要有1个线程)
- 一个进程的所有任务都在线程中执行
- 比如使用酷狗播放音乐、使用迅雷下载文件,都需要在线程中执行
- 线程的串行
- 1个线程中任务的执行是串行的
- 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务
- 在同一时间内,1个线程只能执行1个任务
- 比如在 1 个线程中下载 3 个文件(分别是文件 A、文件 B、文件 C)
- 1个线程中任务的执行是串行的
多线程
- 什么是多线程
- 1 个进程中可以开启多个线程,所有线程可以 并行(同时) 执行不同的任务
- 进程 → 车间,线程 → 车间工人
- 多线程技术可以提高程序的执行效率
- 比如同时开启3个线程分别下载 3 个文件(分别是文件 A、文件 B、文件 C)
- 多线程的原理
- 同一时间,CPU 的1个核心只能处理1个线程(只有 1 个线程在工作)
- 多线程并发(同时)执行,其实是 CPU 快速地在多个线程之间调度(切换)
- 如果 CPU 调度线程的速度足够快,就造成了多线程并发执行的假象
- 如果是多核 CPU,才是真正地实现了多个线程同时执行
- 思考:如果线程非常非常多,会发生什么情况?
- CPU 会在 N 个线程之间调度,消耗大量的 CPU 资源,CPU 会累死
- 每条线程被调度执行的频次会降低(线程的执行效率降低)
- 多线程的优缺点
- 优点
- 能适当提高程序的执行效率
- 能适当提高资源利用率(CPU、内存利用率)
- 缺点
- 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调度线程上的开销就越大
- 程序设计更加复杂
- 比如线程之间的通信问题、多线程的数据共享问题
- 优点
默认线程
- 每一个 Java 程序启动后,会默认开启一个线程,称为主线程(main 方法所在的线程)
-
每一个线程都是一个 java.lang.Thread 对象,可以通过 Thread.currentThread 方法获取当前的线程对象
public static void main(String[] args) { //Thread[main,5,main] //通过查看Thread的toString可以看到[线程名称,优先级,线程组名称] System.out.println(Thread.currentThread()); }
开启新线程
-
第1种方法
//传入一个Runnable实例,在run方法中编写子线程需要执行的任务 Thread thread = new Thread(new Runnable() { @Override public void run() { //存放线程要执行的代码 System.out.println("开启了新线程:" + Thread.currentThread()); } }); //设置线程名称 thread.setName(""); //设置线程优先级 thread.setPriority(10); //启动线程 thread.start(); //开启了新线程:Thread[,10,main]
-
第2种方法
public class MyThread extends Thread { @Override public void run() { System.out.println("开启了新线程:" + Thread.currentThread()); } } MyThread myThread = new MyThread(); myThread.start(); //开启了新线程:Thread[Thread-1,5,main]
- 注意: 直接调用线程的 run 方法并不能开启新线程,调用线程的 start 方法才能成功开启新线程
- Thread 类自己实现了 Runnable 接口
多线程的内存布局(重点!!!)
Java的内存区域(前几节已说明):PC 寄存器、Java 虚拟机栈、堆(Heap)、方法区(Method Area)、本地方法栈(Native Method Stack),那么一个线程的内存是如何布局的呢?
- PC 寄存器(Program Counter Register):每一个线程都有自己的 PC 寄存器
- 也叫程序计数器,用来存储指向下一条指令的地址(偏移地址),也即将要执行的指令代码,生命周期与线程的生命周期保持一致,一旦线程结束,当前PC寄存器也不在作用。
- 即每个线程执行每句代码编译的指令时,都是读取自己PC寄存器指向的指令
- Java 虚拟机栈(Java Virtual Machine Stack):每一个线程都有自己的 Java 虚拟机栈
- 程序在执行方法时,会开辟栈空间,也叫帧栈,用来存储函数内部的局部变量,且执行函数内部的数据计算,一旦函数执行完,栈恢复,生命周期和线程一致。它保存方法的局部变量、部分结果,并参与方法的调用和返回。
- 本地方法栈(Native Method Stack):每一个线程都有自己的本地方法栈
- 本地方法:有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因,比如Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
- 一个Native Method就是一个Java调用非Java代码的接口,即该方法的实现由非Java语言实现,比如C.C+++
- 本地方法栈就是当java调用非java的方法时,需要开辟函数帧栈,需要栈空间,这块空间就是本地方法栈,与Java虚拟机栈类似。
- 堆(Heap):多个线程共享堆
- 方法区(Method Area):多个线程共享方法区
- 方法区:存储每一个类的结构信息(比如字段和方法信息、构造方法和普通方法的字节码(.class)等),类名、成员变量名称、方法名称等,也叫代码段
线程的状态
- 可以通过 Thread.getState 方法获得线程的状态(线程一共有6种状态,6个枚举值)
- NEW(新建):尚未启动
- RUNNABLE(可运行状态):正在 JVM 中运行
- 或者正在等待操作系统的其他资源(比如处理器)
- BLOCKED(阻塞状态):正在等待监视器锁(内部锁)
- 会消耗CPU时间片,一直在问锁是否被释放了
- WAITING(等待状态):在等待另一个线程
- 不会消耗CUP时间片,处于睡眠状态
- 调用以下方法会处于等待状态
- 没有超时值的 Object.wait
- 没有超时值的 Thread.join
- LockSupport.park
- TIMED_WAITING(定时等待状态)
- 调用以下方法会处于定时等待状态
- Thread.sleep
- 有超时值的 Object.wait
- 有超时值的 Thread.join
- LockSupport.parkNanos
- LockSupport.parkUntil
- 调用以下方法会处于定时等待状态
- TERMINATED(终止状态):已经执行完毕
线程的几个方法
- sleep、interrupt
- 可以通过 Thread.sleep 方法暂停当前线程,进入WAITING状态
- 在暂停期间,若调用线程对象的 interrupt 方法中断线程,会抛出 java.lang.InterruptedException 异常
Thread thread = new Thread(()-> { System.out.println("begin"); try { Thread.sleep(3000); } catch (InterruptedException e) { System.out.println("interrupt"); } System.out.println("end"); }); thread.start(); try { //主线程睡眠1s Thread.sleep(1000); } catch (InterruptedException e) {} //打断子线程的睡眠 thread.interrupt(); /* begin interrupt end */
- 可以通过 Thread.sleep 方法暂停当前线程,进入WAITING状态
- join、isAlive
- A.join 方法:等线程 A 执行完毕后,当前线程再继续执行任务。可以传参指定最长等待时间
- A.isAlive 方法:查看线程 A 是否还活着
Thread t1 = new Thread(()-> { System.out.println("t1-begin"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1-end"); }); t1.start(); Thread t2 = new Thread(()-> { System.out.println("t2-begin"); System.out.println("t1.isAlive - " + t1.isAlive()); try { //t1的线程执行完毕,才会继续执行 t1.join(); //等t1 1s钟,如果没等到继续执行 //t1.join(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1-state - " + t1.getState()); System.out.println("t1-isAlive - " + t1.isAlive()); System.out.println("t2-end"); }); t2.start();
-
打印结果
t1-begin t2-begin t1.isAlive - true t1-end t1-state - TERMINATED t1-isAlive - false t2-end
线程安全问题
- 多个线程可能会共享(访问)同一个资源:比如访问同一个对象、同一个变量、同一个文件
- 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题
- 什么情况下会出现线程安全问题?
- 多个线程共享同一个资源
- 且至少有一个线程正在进行写的操作(读取不会出问题,不会修改数据)
-
线程安全问题 – 示例,车站买票
public class Station implements Runnable { private int tickets = 100; /** * 卖一张票 * @return 是否还有票可以卖 * */ public boolean saleTicket() { if(tickets <1) return false; tickets --; String name = Thread.currentThread().getName(); System.out.println(name + "卖了1张,剩" + tickets + "张"); return tickets >0; } @Override public void run() { while (saleTicket()); } } Station station = new Station(); for (int i = 0; i < 4; i++) { Thread thread = new Thread(station); thread.setName("" + i); thread.start(); }
线程同步
- 可以使用线程同步技术来解决线程安全问题
- 同步语句(Synchronized Statement)
- 同步方法(Synchronized Method)
同步语句
public boolean saleTicket() {
//获得this对象的内部锁,加锁
synchronized (this) {
if(tickets <1) return false;
tickets --;
String name = Thread.currentThread().getName();
System.out.println(name + "卖了1张,剩" + tickets + "张");
return tickets >0;
}//释放锁
}
- synchronized(obj) 的原理
- 每个对象都有一个与它相关的内部锁(intrinsic lock)或者叫监视器锁(monitor lock),理解成每个对象都有一把锁
- 第一个执行到同步语句的线程可以获得obj的内部锁,在执行完同步语句中的代码后释放此锁,synchronized可以获取对象的锁,并锁住
- 只要一个线程持有了内部锁,那么其它线程在同一时刻将无法再获得此锁,当它们试图获取此锁时,将会进入BLOCKED状态
- 多个线程访问同一个 synchronized(obj) 语句时
- obj 必须是同一个对象,才能起到同步的作用
- obj可以是任一个对象不一定是this,但是必须要求每个线程访问的是同一个对象
同步方法
//修饰实例方法
public synchronized boolean saleTicket() {
if(tickets <1) return false;
tickets --;
String name = Thread.currentThread().getName();
System.out.println(name + "卖了1张,剩" + tickets + "张");
return tickets >0;
}
- synchronized 不能修饰构造方法
- 同步方法的本质
- 实例方法:synchronized (this)
- 就是等价于上面的同步语句
-
静态方法:synchronized(Class对象)
public synchronized static void test() {} public static void test2() { //类对象Station.class synchronized(Station.class) {} }
- 实例方法:synchronized (this)
- 同步语句比同步方法更灵活一点
- 同步语句可以精确控制需要加锁的代码范围
- 使用了线程同步技术后
- 虽然解决了线程安全问题,但是降低了程序的执行效率
- 所以在真正有必要的时候,才使用线程同步技术
单例模式(懒汉式)改进
//之前的单例模式存在线程安全问题
//懒汉式,解决线程安全问题
class Rocket {
//1.私有的静态的实例变量
private static Rocket instance = null;
//2. 构造方法私有化,让外界无法调用构造方法
private Rocket() {}
//3. 提供一个公共的静态的,返回一个唯一的那个实例
public static synchronized Rocket getInstance() {
if (instance == null) {
instance = new Rocket();
}
return instance;
}
}
几个常用类的细节(以下数据结构可以查看源码)
- 动态数组
- ArrayList:非线程安全
- Vector:线程安全
- 动态字符串
- StringBuilder:非线程安全
- StringBuffer:线程安全
- 映射(字典)
- HashMap:非线程安全
- Hashtable:线程安全
死锁(Deadlock)
- 什么是死锁?
- 两个或者多个线程永远阻塞,相互等待对方的锁
new Thread(()->{ //获取锁成功 synchronized ("1") { System.out.println("1-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //获取锁失败,被另外线程占用占用 synchronized ("2") { System.out.println("1-2"); } } }).start(); new Thread(()->{ //获取锁成功 synchronized ("2") { System.out.println("2-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //获取锁失败,被另外线程占用占用 synchronized ("1") { System.out.println("2-2"); } } }).start();
-
示例
public class Person { private String name; public Person(String name) { this.name = name; } public synchronized void hello(Person p) { System.out.format("[%s] hello to [%s]%n",name,p.name); //另外一个对象 p.smile(this); } public synchronized void smile(Person p) { System.out.format("[%s] hello to [%s]%n",name,p.name); } } Person jack = new Person("jack"); Person rose = new Person("rose"); //同时开口 new Thread(()->{jack.hello(rose);}).start(); new Thread(()->{rose.hello(jack);}).start();
线程间通信
- 可以使用 Object.wait、Object.notify、Object.notifyAll 方法实现线程之间的通信
- 若想在线程 A 中成功调用 obj.wait、obj.notify、obj.notifyAll 方法
- 线程A必须要持有obj的内部锁
- obj.wait :释放obj的内部锁,当前线程进入WAITING或TIMED_WAITING状态
- obj.notifyAll :唤醒所有因为 obj.wait 进入WAITING或TIMED_WAITING状态的线程
- obj.notify :随机唤醒1个因为 obj.wait 进入WAITING或TIMED_WAITING状态的线程
- 示例
-
Drop:传递的信息
package com.zh; public class Drop { private String food; //empty为true:代表消费者等待生产者生产东西 //empty为false:代表生产完毕,生产者等待消费者消化完食品 private boolean empty = true; //消费者获取,消费者线程执行 public synchronized String get() { //为什么不用if,要用while,因为wait();会抛出异常,接着会往下执行 //除非empty为false,否则不能继续执行。 while (empty) { try { //此时生产者没有生产东西,这卡住,等待add中通知生产完成生产 //理解:等待过程中释放了this的内部锁,以便于其他线程可以拿到,生产者才能进入add方法。 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } empty =true; //通知所有生产线程可以取食物了 notifyAll(); return food; } //生产者生产,生产者线程执行 public synchronized void add(String food) { //第一次时empty为true while (!empty) { try { //等待消费者告知,已经消费完了 //同理,释放锁,消费这才能进入get方法 wait(); } catch (InterruptedException e) {} } //设置标记, empty =false; //保存当前食物 this.food = food; //通知所有消费线程可以取食物了 notifyAll(); } }
-
Producer: 生产者
public class Producer implements Runnable { private Drop drop; public Producer(Drop drop) { this.drop = drop; } //生产食物 @Override public void run() { String foods[] = {"beef","bread","apple","cookie"}; for (int i = 0; i < foods.length; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) {} //将food[i],传递给消费者 drop.add(foods[i]); } //告诉消费者,不会生产任何东西了 drop.add(null); } }
-
Consumer:消费者
public class Consumer implements Runnable { private Drop drop; public Consumer(Drop drop) { this.drop = drop; } //拿食物 @Override public void run() { String food = null; while ((food = drop.get()) != null) { System.out.format("消费者接收到食物:%s%n", food); try { Thread.sleep(1000); } catch (InterruptedException e) {} } } }
-
main函数使用
//使用 Drop drop = new Drop(); //开启2个线程,2个线程中drop对象是同一个 (new Thread(new Consumer(drop))).start(); (new Thread(new Producer(drop))).start();
-
注意:
- 调用wait、notify必须是同一个obj对象
- 调用wait、notify的线程必须拥有obj对象的内部锁
-
ReentrantLock(可重入锁)
- ReentrantLock ,译为“可重入锁”
- 类的全名是:java.util.concurrent.locks.ReentrantLock
- 具有跟同步语句(加锁的语句)、同步方法(加锁的方法) 一样的一些基本功能,但功能更加强大
- 什么是可重入?
- 同一个线程可以重复获取同一个锁
- 其实 synchronized 也是可重入的
lock、tryLock
- ReentrantLock.lock :获取此锁
- 如果此锁没有被另一个线程持有,则将锁的持有计数设为1,并且此方法立即返回
- 如果当前线程已经持有此锁,则将锁的持有计数加1,并且此方法立即返回
- 如果此锁被另一个线程持有,并且在获得锁之前,此线程将一直处于休眠状态,此时锁的持有计数被设为1。
- ReentrantLock.tryLock :仅在锁未被其他线程持有的情况下,才获取此锁
- 如果此锁没有被另一个线程持有,则将锁的持有计数设为1,并且此方法立即返回 true
- 如果当前线程已经持有此锁,则将锁的持有计数加1,并且此方法立即返回 true。
- 如果锁被另一个线程持有,则此方法立即返回 false,不会阻塞当前线程
unlock、isLocked
- ReentrantLock.unlock :尝试释放此锁
- 如果当前线程持有此锁,则将持有计数减 1
- 如果持有计数现在为 0,则释放此锁
- 如果当前线程没有持有此锁,则抛出 java.lang.IllegalMonitorStateException
- ReentrantLock.isLocked :查看此锁是否被任意线程持有
ReentrantLock 在卖票示例中的使用
private Lock lock = new ReentrantLock();
private int tickets = 100;
public boolean saleTicket() {
//boolean flag = false;
try {
//不一定会加上锁,尝试加锁
//flag = lock.tryLock();
//上锁,一旦获取不到锁,当前线程会处于休眠状态,不会继续执行,直到获取锁为止
lock.lock();
if(tickets <1) return false;
tickets --;
String name = Thread.currentThread().getName();
System.out.println(name + "卖了1张,剩" + tickets + "张");
return tickets >0;
} finally {
//if(flag) {
//lock.unlock();
//}
//解锁
lock.unlock();
}
}
tryLock使用注意
Lock lock = new ReentrantLock();
new Thread(()->{
try {
lock.lock();
System.out.println("1");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("3");
lock.unlock();
}
}).start();
new Thread(()->{
boolean locked = false;
try {
//不管是否获取到锁,都不会阻塞当前线程
locked = lock.tryLock();
//如果获取不到锁,当前线程会阻塞,处于休眠状态,直至获取到锁为止,才继续执行
//lock.lock();
System.out.println(locked);
System.out.println("2");
}finally {
System.out.println("41");
if (locked) {
System.out.println("4");
lock.unlock();
}
}
}).start();
//打印,线程二使用trylock
1
false
2
41
3
//打印:线程二使用lock
1
3 //直到第一个线程把锁释放,第二个线程才会结束休眠,继续执行
false
2
41
线程池(Thread Pool)
- 从多线程的内存布局可知,每个线程都要开辟3块内存空间(PC寄存器、Java虚拟机栈、本地方法栈,其余2个是多个线程共享)
- 线程对象占用大量内存,在大型应用程序中,频繁地创建和销毁线程对象会产生大量内存管理开销
- 使用线程池可以最大程度地减少线程创建、销毁所带来的开销
- 线程池由 工作线程(Worker Thread) 组成
- 普通线程:执行完一个任务后,生命周期就结束了
- 工作线程: 可以执行多个任务(任务没来就一直等,任务来了就干活)
- 先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中
- 常用的线程池类型是固定线程池(Fixed Thread Pool)
- 具有固定数量的正在运行的线程
基本使用
//创建拥有5条工作线程的固定线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
//执行任务
pool.execute(()->{
//11_pool-1-thread-1
System.out.println(11 + "_" + Thread.currentThread().getName());
});
pool.execute(()->{
//22_pool-1-thread-2
System.out.println(22 + "_" + Thread.currentThread().getName());
});
pool.execute(()->{
//33_pool-1-thread-3
System.out.println(33 + "_" + Thread.currentThread().getName());
});
//关闭线程池
pool.shutdown();