600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 【Java 并发编程】多线程 线程同步 死锁 线程间通信(生产者消费者模型) 可重入

【Java 并发编程】多线程 线程同步 死锁 线程间通信(生产者消费者模型) 可重入

时间:2021-04-19 16:26:26

相关推荐

【Java 并发编程】多线程 线程同步 死锁 线程间通信(生产者消费者模型) 可重入

并发编程(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.joinLockSupport.parkTIMED_WAITING定时等待状态

调用以下方法会处于定时等待状态Thread.sleep有超时值的Object.wait有超时值的Thread.joinLockSupport.parkNanosLockSupport.parkUntilTERMINATED终止状态):已经执行完毕

线程的状态切换

sleepinterrupt

可以通过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

joinisAlive

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.waitObject.notifyObject.notifyAll方法实现线程之间的通信

若想在线程 A 中成功调用obj.waitobj.notifyobj.notifyAll方法

线程 A 必须要持有 obj 的内部锁

obj.wait:释放 obj 的内部锁,当前线程进入WAITINGTIMED_WAITING状态

obj.notifyAll唤醒所有因为obj.wait进入WAITINGTIMED_WAITING状态的线程

obj.notify随机唤醒 1 个因为obj.wait进入WAITINGTIMED_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” 的内部锁,仍然可以执行,在有的语言中是不允许这样,那就不是可重入锁。

locktrylock

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 并发编程】多线程 线程同步 死锁 线程间通信(生产者消费者模型) 可重入锁 线程池

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。