并发编程(Concurrent Programming)
进程(Process)、线程(Thread)、线程的串行多线程多线程的原理多线程的优缺点Java并发编程默认线程开启新线程`Runnable``extends Thread`多线程的内存布局线程的状态`sleep`、`interrupt``join`、`isAlive`线程安全问题线程安全问题 – 错误示例解决方案 - 线程同步线程同步 - 同步语句线程同步 - 同步方法单例模式(懒汉式)改进几个常用类的细节死锁(Deadlock)死锁示例1死锁示例2线程间通信线程间通信 - 生产者消费者模型ReentrantLock(可重入锁)`lock`、`trylock`ReentrantLock 在卖票示例中的使用ReentrantLock – `tryLock`使用注意线程池(Thread Pool)Java笔记目录可以点这里:Java 强化笔记(适合有基础的童鞋,不适合小白)
进程(Process)、线程(Thread)、线程的串行
什么是进程?
在操作系统中运行的一个应用程序
比如同时打开 QQ 、微信,操作系统就会分别启动 2个进程
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
在 Windows 中,可以通过“任务管理器”查看正在运行的进程
什么是线程?
1 个进程要想执行任务,必须得有线程(每 1 个进程至少要有 1 个线程)一个进程的所有任务都在线程中执行
比如使用酷狗播放音乐、使用迅雷下载文件,都需要在各自的线程中执行
线程的串行
1 个线程中任务的执行是串行的
如果要在 1 个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务在同一时间内,1 个线程只能执行 1 个任务
比如在 1 个线程中下载 3 个文件(分别是文件 A、文件 B、文件 C)
多线程
什么是多线程?
1 个进程中可以开启多线,所有线程可以并行(同时)执行不同的任务
进程 → 车间
线程 → 车间工人多线程技术可以提高序的执行效率
比如同时开启 3 个线程分别下载 3 个文件 (分别是A、文件 B、文件 C)
多线程的原理
同一时间,CPU 的 1 个核心只能处理 1 个线程(只有 1 个线程在工作)多线程并发(同时)执行,其实是CPU 快速地在多个线程之间调度(切换)如果 CPU 调度线程的速度足够快,就造成了多线程并发执行的假象如果是多核 CPU,才是真正地实现了多个线程同时执行
思考:如果线程非常非常多,会发生什么情况?
CPU 会在 N 个线程之间调度,消耗大量的 CPU 资源,CPU 会累死每条线程被调度执行的频次会降低(线程的执行效率降低)
多线程的优缺点
优点:
能适当提高程序的执行效率能适当提高资源利用率(CPU、内存利用率)
缺点:
开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能线程越多,CPU 在调度线程上的开销就越大程序设计更加复杂
比如线程之间的通信问题、多线程的数据共享问题
Java并发编程
默认线程
每一个 Java 程序启动后,会默认开启一个线程,称为主线程(main 方法所在的线程)每一个线程都是一个java.lang.Thread
对象可以通过Thread.currentThread
方法获取当前的线程对象
public static void main(String[] args) {// Thread[main,5,main]System.out.println(Thread.currentThread());}
根据Java源码可知,打印出来的Thread[main,5,main]
表示:
进程名为main
进程优先级为5进程组的名字为main
开启新线程
Runnable
public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 打印线程名System.out.println("开启了新线程:" + Thread.currentThread().getName());}});thread.setName("线程666"); // 设置线程名thread.start(); // Thread调用start方法之后,内部会调用run方法}
开启了新线程:线程666
可以用 Lambda 表达式改写:
public static void main(String[] args) {Thread thread = new Thread(() -> {// lambda 表达式System.out.println("开启了新线程:" + Thread.currentThread().getName());});// 不设置线程名则会自动命名, Thread-0, Thread-1, ...thread.start(); // Thread调用start方法之后,内部会调用run方法}
开启了新线程:Thread-0
extends Thread
Thread 类实现了 Runnable 接口 。
创建一个类 MyThread 继承 Thread 类:
public class MyThread extends Thread {@Overridepublic void run() {System.out.println("开启了新线程:" + Thread.currentThread().getName());}}
public static void main(String[] args) {Thread thread = new MyThread();thread.start();}
开启了新线程:Thread-0
注:
直接调用线程的 run 方法并不能开启新线程调用线程的 start 方法才能成功开启新线程
多线程的内存布局
PC 寄存器(Program Counter Register)每一个线程都有自己的 PC 寄存器Java 虚拟机栈(Java Virtual Machine Stack):
每一个线程都有自己的 Java 虚拟机栈堆(Heap)
多个线程共享堆方法区(Method Area)
多个线程共享方法区本地方法栈(Native Method Stack)
每一个线程都有自己的本地方法栈
线程的状态
可以通过Thread.getState
方法获得线程的状态(线程一共有 6 种状态)
NEW(新建):尚未启动RUNNABLE(可运行状态):正在JVM中运行
或者正在等待操作系统的其他资源(比如处理器)BLOKCED(阻塞状态):正在等待监视器锁(内部锁)WAITING(等待状态):在等待另一个线程
调用以下方法会处于等待状态 没有超时值的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
异常。
public static void main(String[] args) {Thread thread = new Thread(() -> {try {Thread.sleep(3000); // 睡眠3s} catch (InterruptedException e) {// 捕捉到异常则输出System.out.println("interrupt");}System.out.println("end");});thread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {} // 捕捉到异常什么也不做thread.interrupt();}
interruptend
join
、isAlive
A.join
方法:等线程 A 执行完毕后,当前线程再继续执行任务。可以传参指定最长等待时间。
A.isAlive
方法:查看线程 A 是否还活着。
public static void main(String[] args) {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.join(); // 等待t1执行完成再继续往下执行} 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 - begint2 - begint1.isAlive - truet1 - endt1.state - TERMINATEDt1.isAlive - falset2 - end
对比一下这两段代码细微的区别,t1.join(1000);
public static void main(String[] args) {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.join(1000); // 等待t1 1s,但是t1 睡了2s,1s过去后t1 还没运行完} 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 - begint2 - begint1.isAlive - truet1.state - TIMED_WAITINGt1.isAlive - truet2 - endt1 - end
线程安全问题
多个线程可能会共享(访问)同一个资源
比如访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题。
什么情况下会出现线程安全问题?
多个线程共享同一个资源且至少有一个线程正在进行写(write)的操作
例如:存钱取钱过程
卖票过程
线程安全问题 – 错误示例
编写一个站台类:
public class Station implements Runnable {private int tickets = 100;/*** 卖一张票*/public boolean saleTicket(){if(tickets < 1) return false; // 票卖完了,不卖了tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}@Overridepublic void run() {while(saleTicket()); // 只要能卖票就一只卖}}
public static void main(String[] args) {Station station = new Station();for (int i = 1; i <= 4; i++) {Thread thread = new Thread(station);thread.setName("" + i);thread.start();}}
会发现结果不是我们想要的,票数乱七八糟。
....2卖了1张票,还剩47张2卖了1张票,还剩45张2卖了1张票,还剩44张2卖了1张票,还剩43张2卖了1张票,还剩42张2卖了1张票,还剩41张2卖了1张票,还剩40张2卖了1张票,还剩39张2卖了1张票,还剩38张2卖了1张票,还剩37张2卖了1张票,还剩36张1卖了1张票,还剩47张1卖了1张票,还剩34张1卖了1张票,还剩33张1卖了1张票,还剩32张1卖了1张票,还剩31张1卖了1张票,还剩30张4卖了1张票,还剩46张4卖了1张票,还剩28张4卖了1张票,还剩27张4卖了1张票,还剩26张4卖了1张票,还剩25张4卖了1张票,还剩24张4卖了1张票,还剩23张4卖了1张票,还剩22张4卖了1张票,还剩21张4卖了1张票,还剩20张4卖了1张票,还剩19张4卖了1张票,还剩18张4卖了1张票,还剩17张3卖了1张票,还剩47张3卖了1张票,还剩15张3卖了1张票,还剩14张4卖了1张票,还剩16张4卖了1张票,还剩12张4卖了1张票,还剩11张4卖了1张票,还剩10张4卖了1张票,还剩9张4卖了1张票,还剩8张4卖了1张票,还剩7张1卖了1张票,还剩29张1卖了1张票,还剩5张1卖了1张票,还剩4张1卖了1张票,还剩3张1卖了1张票,还剩2张1卖了1张票,还剩1张1卖了1张票,还剩0张2卖了1张票,还剩35张4卖了1张票,还剩6张3卖了1张票,还剩13张
问题分析:
解决方案 - 线程同步
可以使用线程同步技术来解决线程安全问题
同步语句(Synchronized Statement)同步方法(Synchronized Method)
线程同步 - 同步语句
将上面错误示例的代码修改成如下,则正确了。
public boolean saleTicket(){synchronized (this) {if(tickets < 1) return false;tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}}
.....1卖了1张票,还剩49张1卖了1张票,还剩48张1卖了1张票,还剩47张1卖了1张票,还剩46张1卖了1张票,还剩45张1卖了1张票,还剩44张4卖了1张票,还剩43张4卖了1张票,还剩42张4卖了1张票,还剩41张4卖了1张票,还剩40张4卖了1张票,还剩39张3卖了1张票,还剩38张3卖了1张票,还剩37张3卖了1张票,还剩36张3卖了1张票,还剩35张3卖了1张票,还剩34张3卖了1张票,还剩33张3卖了1张票,还剩32张3卖了1张票,还剩31张3卖了1张票,还剩30张3卖了1张票,还剩29张3卖了1张票,还剩28张3卖了1张票,还剩27张3卖了1张票,还剩26张3卖了1张票,还剩25张3卖了1张票,还剩24张3卖了1张票,还剩23张3卖了1张票,还剩22张3卖了1张票,还剩21张3卖了1张票,还剩20张3卖了1张票,还剩19张3卖了1张票,还剩18张2卖了1张票,还剩17张2卖了1张票,还剩16张2卖了1张票,还剩15张2卖了1张票,还剩14张2卖了1张票,还剩13张2卖了1张票,还剩12张2卖了1张票,还剩11张2卖了1张票,还剩10张2卖了1张票,还剩9张2卖了1张票,还剩8张2卖了1张票,还剩7张2卖了1张票,还剩6张2卖了1张票,还剩5张2卖了1张票,还剩4张2卖了1张票,还剩3张2卖了1张票,还剩2张2卖了1张票,还剩1张2卖了1张票,还剩0张
synchronized(obj)
的原理:
每个对象都有一个与它相关的内部锁(intrinsic lock)或者叫监视器锁(monitor lock)第一个执行到同步语句的线程可以获得 obj 的内部锁,在执行完同步语句中的代码后释放此锁只要一个线程持有了内部锁,那么其它线程在同一时刻将无法再获得此锁
当它们试图获取此锁时,将会进入BLOCKED
状态。
多个线程访问同一个synchronized(obj)
语句时
obj 必须是同一个对象,才能起到同步的作用。
线程同步 - 同步方法
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 class Rocket {private static Rocket instance = null;private Rocket() {}public static synchronized Rocket getInstance(){if(instance == null){instance = new Rocket();}return instance;}}
几个常用类的细节
动态数组:
ArrayList
:非线程安全Vector
:线程安全
动态字符串:
StringBuilder
:非线程安全StringBuffer
:线程安全
映射(字典):
HashMap
:非线程安全Hashtable
:线程安全
死锁(Deadlock)
什么是死锁?
两个或者多个线程永远阻塞,相互等待对方的锁
死锁示例1
以下代码会造成死锁:
第一个进程获得了 “1” 的同步锁,又想要获得 “2” 的同步锁第二个进程获得了 “2” 的同步锁,想要获得进程 “1” 的同步锁第一个进程和第二个进程互相等待对方释放,谁也不会主动释放,造成了死锁。
public static void main(String[] args) {new Thread(() -> {synchronized ("1") {// 进程1获得了 "1" 的同步锁System.out.println("1 - 1");try{Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}synchronized ("2") {// 进程1想要获得 "2" 的同步锁System.out.println("1 - 2");}}}).start();;new Thread(() -> {synchronized ("2") {// 进程2获得了 "2" 的同步锁System.out.println("2 - 1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized ("1") {// 进程2想要获得 "1" 的同步锁System.out.println("2 - 2");}}}).start();;}
死锁示例2
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] smile to [%s]%n", name, p.name);}}
public class Deadlock {public static void main(String[] args) {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:食品Consumer`:消费者Producer:生产者main:测试类package com.yu;/*** @author yusael*/public class Drop {private String food;// empty为true代表:消费者需要等待生产者生产食品// empty为false代表:食品生产完毕,生产者要等待消费者消化完食品private boolean empty = true;/*** get方法在消费者线程中执行* @return*/public synchronized String get(){while(empty){try {wait();} catch (InterruptedException e) {}}empty = true;notifyAll();return food;}/*** add方法在生产者线程中执行* @param food*/public synchronized void add(String food){while(!empty){try {wait();} catch (InterruptedException e) {}}empty = false;this.food = food;notifyAll();}}
package com.yu;/*** 生产者* @author yusael*/public class Consumer implements Runnable {private Drop drop;public Consumer(Drop drop) {this.drop = drop;}@Overridepublic void run() {String food = null;while((food = drop.get()) != null){System.out.format("消费者接收到生产者生产的食物:%s%n", food);try {Thread.sleep(1000); // 消费者吃食物2秒} catch (InterruptedException e) {}}}}
package com.yu;/*** 消费者* @author yusael*/public class Producer implements Runnable {private Drop drop;public Producer(Drop drop) {this.drop = drop;}@Overridepublic void run() {String foods[] = {"beef", "bread", "apple", "cookie"};for (int i = 0; i < foods.length; i++) {try {Thread.sleep(1000); // 生产者生产食物2秒} catch (InterruptedException e) {}// 将foods[i]传递给消费者drop.add(foods[i]);}// 告诉消费者:不会再生产任何东西了drop.add(null);}}
package com.yu;public class Main {public static void main(String[] args) {Drop drop = new Drop();(new Thread(new Consumer(drop))).start(); // 开启消费者线程(new Thread(new Producer(drop))).start(); // 开启生产者线程}}
消费者接收到生产者生产的食物:beef消费者接收到生产者生产的食物:bread消费者接收到生产者生产的食物:apple消费者接收到生产者生产的食物:cookie
ReentrantLock(可重入锁)
ReentrantLock,译为“可重入锁”,也被称为“递归锁”
类的全名是:java.util.concurrent.locks.ReentrantLock
具有跟同步语句、同步方法(synchronized
)一样的一些基本功能,但功能更加强大
什么是可重入(rerntrant)?
同一个线程可以重复获取同一个锁其实synchronized
也是可重入的
public static void main(String[] args) {synchronized ("1") {synchronized("1"){System.out.println("synchronized是可重入锁");}}}
该例获取了两次 “1” 的内部锁,仍然可以执行,在有的语言中是不允许这样,那就不是可重入锁。
lock
、trylock
ReentrantLock.lock
:获取此锁
如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回如果此锁被另一个线程持有,并且在获得锁之前,此线程将一直处于休眠状态(相当于wait
),此时锁的持有计数被设为 1
ReentrantLock.tryLock
:仅在锁未被其他线程持有的情况下,才获取此锁
如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回 true如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回 true。如果此锁被另一个线程持有,则此方法立即返回 false
ReentrantLock.unlock
:尝试释放此锁
如果当前线程持有此锁,则将持有计数减 1如果持有计数现在为 0,则释放此锁如果当前线程没有持有此锁,则抛出java.lang.IllegalMonitorStateException
ReentrantLock.isLocked
:查看此锁是否被任意线程持有
ReentrantLock 在卖票示例中的使用
package com.mj;import java.util.concurrent.locks.ReentrantLock;public class Station implements Runnable {private int tickets = 50;// ReentrantLock lock = new ReentrantLock(); // 两个都行Lock lock = new ReentrantLock();/*** 卖一张票*/public boolean saleTicket(){lock.lock();try{if(tickets < 1) return false;tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}finally {lock.unlock();}}@Overridepublic void run() {while(saleTicket());}}
ReentrantLock –tryLock
使用注意
Lock lock = new ReentrantLock();new Thread(() -> {try {lock.lock();System.out.println("1");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();
Lock lock = new ReentrantLock();new Thread(() -> {boolean locked = false;try{locked = lock.tryLock();System.out.println("2");} finally {if(locked)lock.unlock();}}).start();
线程池(Thread Pool)
线程对象占用大量内存,在大型应用程序中,频繁地创建和销毁线程对象会产生大量内存管理开销。
使用线程池可以最大程度地减少线程创建、销毁所带来的开销。
线程池由工作线程(Worker Thread)组成
普通线程:执行完一个任务后,生命周期就结束了。工作线程:可以执行多个任务(任务没来就一直等,任务来了就干活);
先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中。
常用的线程池类型是固定线程池(Fixed Thread Pool)
具有固定数量的正在运行的线程
线程池简单使用:
public static void main(String[] args) {// 创建拥有5条工作线程的固定线程池ExecutorService pool = Executors.newFixedThreadPool(5);// 执行任务pool.execute(() -> {// Thread[pool-1-thread-1,5,main]System.out.println(Thread.currentThread());});pool.execute(() -> {// Thread[pool-1-thread-2,5,main]System.out.println(Thread.currentThread());});pool.execute(() -> {// Thread[pool-1-thread-3,5,main]System.out.println(Thread.currentThread());});// 关闭线程池pool.shutdown();}
【Java 并发编程】多线程 线程同步 死锁 线程间通信(生产者消费者模型) 可重入锁 线程池