【Java并发编程】进程、线程、线程的串行、多线程原理、默认线程、开启新线程、多线程的内存布局、线程的状态、线程安全问题、单例模式、死锁、线程间通讯、ReentranLock(可重入锁)、线程池

九、并发编程

进程(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 程序启动后,会默认开启一个线程,称为主线程(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() {
		@Override
		public 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 {
	@Override
	public 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(终止状态):已经执行完毕

线程的状态切换

在这里插入图片描述

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();
}
interrupt
end

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 - begin
t2 - begin
t1.isAlive - true
t1 - end
t1.state - TERMINATED
t1.isAlive - false
t2 - 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 - begin
t2 - begin
t1.isAlive - true
t1.state - TIMED_WAITING
t1.isAlive - true
t2 - end
t1 - 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;
	}
	@Override
	public 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张票,还剩472卖了1张票,还剩452卖了1张票,还剩442卖了1张票,还剩432卖了1张票,还剩422卖了1张票,还剩412卖了1张票,还剩402卖了1张票,还剩392卖了1张票,还剩382卖了1张票,还剩372卖了1张票,还剩361卖了1张票,还剩471卖了1张票,还剩341卖了1张票,还剩331卖了1张票,还剩321卖了1张票,还剩311卖了1张票,还剩304卖了1张票,还剩464卖了1张票,还剩284卖了1张票,还剩274卖了1张票,还剩264卖了1张票,还剩254卖了1张票,还剩244卖了1张票,还剩234卖了1张票,还剩224卖了1张票,还剩214卖了1张票,还剩204卖了1张票,还剩194卖了1张票,还剩184卖了1张票,还剩173卖了1张票,还剩473卖了1张票,还剩153卖了1张票,还剩144卖了1张票,还剩164卖了1张票,还剩124卖了1张票,还剩114卖了1张票,还剩104卖了1张票,还剩94卖了1张票,还剩84卖了1张票,还剩71卖了1张票,还剩291卖了1张票,还剩51卖了1张票,还剩41卖了1张票,还剩31卖了1张票,还剩21卖了1张票,还剩11卖了1张票,还剩02卖了1张票,还剩354卖了1张票,还剩63卖了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张票,还剩491卖了1张票,还剩481卖了1张票,还剩471卖了1张票,还剩461卖了1张票,还剩451卖了1张票,还剩444卖了1张票,还剩434卖了1张票,还剩424卖了1张票,还剩414卖了1张票,还剩404卖了1张票,还剩393卖了1张票,还剩383卖了1张票,还剩373卖了1张票,还剩363卖了1张票,还剩353卖了1张票,还剩343卖了1张票,还剩333卖了1张票,还剩323卖了1张票,还剩313卖了1张票,还剩303卖了1张票,还剩293卖了1张票,还剩283卖了1张票,还剩273卖了1张票,还剩263卖了1张票,还剩253卖了1张票,还剩243卖了1张票,还剩233卖了1张票,还剩223卖了1张票,还剩213卖了1张票,还剩203卖了1张票,还剩193卖了1张票,还剩182卖了1张票,还剩172卖了1张票,还剩162卖了1张票,还剩152卖了1张票,还剩142卖了1张票,还剩132卖了1张票,还剩122卖了1张票,还剩112卖了1张票,还剩102卖了1张票,还剩92卖了1张票,还剩82卖了1张票,还剩72卖了1张票,还剩62卖了1张票,还剩52卖了1张票,还剩42卖了1张票,还剩32卖了1张票,还剩22卖了1张票,还剩12卖了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:测试类
/**
 * @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();
	}	
}
/**
 * 生产者
 * @author yusael
 */
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); // 消费者吃食物2秒
			} catch (InterruptedException e) {}
		}
		
	}
}
/**
 * 消费者
 * @author yusael
 */
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); // 生产者生产食物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 在卖票示例中的使用

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();
		}
	}
	
	@Override
	public 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();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值