12 java多线程
11.1 进程和线程
进程:
内存中运行的一个应用程序。
是系统进行资源分配和调度的一个独立单位。
线程:
进程中的执行流程。
一个程序至少有一个进程,一个进程至少有一个线程。
多线程:
在一个进程内,并发有多个线程在运行,那这个就是多线程。
并发运行的本质是利用CPU时间片轮询执行。
如果一个进程内运行一个线程,那这个程序就是单线程运行。
如果一个进程内运行多个线程,那这个程序就是多线程运行。
并发和并行
并发:利用CPU时间片轮询执行多线程。(如果有1个CPU核,那只能靠CPU时间片轮询执行N个线程)
并行:同一时间,有多个线性并行执行。(如果有2个CPU核,那并行运行2个线程)
11.2 主线程和子线程
11.2.1 主线程和子线程
主线程:
启动一个应用程序之后,最先启动的线程就是主线程。
任务: main()
默认主线程名:main
public class MainTest {
public static void main(String[] args) {
// main
System.out.println(Thread.currentThread().getName());
}
}
子线程:
在主线程以外开启的线程就是子线程。
子线程独立运行。
11.2.2 子线程三种方式
11.2.2.1 继承Thread类
继承Thread类
子线程的任务封装在run()方法中
系统默认的子线程的名字: Thread-0
package com.hainiu.th1;
public class ThreadTest1 {
public static void main(String[] args) {
// 创建线程对象
SubThread t1 = new SubThread();
// 启动线程,此时就绪状态,等待CPU运行
t1.start();
// 当run()运行完成,线程死亡
// 线程类对象只能启动一次,第二次就报错
// t1.start()
}
}
// 定义线程类
class SubThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
11.2.2.2 实现Runnable接口
定义类实现Runnable接口里的run(),run()就是线程体。
线程执行完没有返回值
package com.hainiu.th1;
public class ThreadTest2 {
public static void main(String[] args) {
// 创建Runnable实现类对象
SubRunnable runnable = new SubRunnable();
// 用Thread线程运行,此时进入就绪状态
new Thread(runnable).start();
}
}
// 定义类实现接口
class SubRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
11.2.2.3 实现Callable接口
如果想执行完线程,并获得线程的返回值,用此种方式。
package com.hainiu.th1;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建Callable实现类对象
SubCallable callable = new SubCallable();
// 用FutureTask包装,目的是能获取线程体返回值
FutureTask<String> futureTask = new FutureTask<>(callable);
// 用Thread启动线程,进入就绪状态
new Thread(futureTask).start();
// get()是个阻塞的方法,直到线程体执行完返回结果后,才会继续执行主线程
String threadName = futureTask.get();
System.out.println("main() -->" + threadName);
}
}
// 定义Callable实现类
// Callable<String> 泛型String,代表线程体执行完会要返回String对象
class SubCallable implements Callable<String>{
@Override
public String call() throws Exception {
String name = Thread.currentThread().getName();
System.out.println(name);
return name;
}
}
11.2.2.4 三种方式对比
继承Thread类:
不能共享数据(同一个对象的数据 )
线程体方法不抛异常
实现Runnable接口:
可以共享公共资源。
没有返回值
线程体方法不抛异常
实现Callable接口:
可以共享公共资源。
有返回值。
线程体方法抛异常
不能共享数据的例子:
package com.hainiu.th1;
public class ThreadTest4 {
public static void main(String[] args) {
Thread2 t1= new Thread2();
t1.start();
Thread2 t2 = new Thread2();
t2.start();
}
}
class Thread2 extends Thread {
// 算是公共资源,多个线程不能共享,只能自己用自己的
private int count;
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + ++count);
}
}
}
能共享数据的例子:
package com.hainiu.th1;
public class ThreadTest5 {
public static void main(String[] args) {
Thread2Runnable runnable = new Thread2Runnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
class Thread2Runnable implements Runnable{
// 公共资源,多个线程可以共享
private int count;
@Override
public void run() {
for(int i = 1; i <= 3; i++){
System.out.println(Thread.currentThread().getName() + "-->" + ++count);
}
}
}
11.3 线程状态
线程分成5种状态:
1)new(新建)
2)Runnable(就绪)
3)Running(运行)
4)Blacked(阻塞)
5)Dead(死亡)
新建:
当程序使用new关键字创建了一个线程后,该线程就处于新建状态。
就绪:
启动线程(调用start()) ,处于了 就绪状态,等待cpu的调用执行;
失去CPU执行权。
运行:
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞:
暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
当前正在执行的线程阻塞后,其他线程就可以获得执行机会。被阻塞的线程会合适的时候重新进入就绪状态,而不是运行状态。
何时进入阻塞状态:
1)线程执行了Thread.sleep(n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行;(不会释放锁)
2)线程执行了一个对象的wait()方法,进入阻塞状态;(会释放锁)
3)线程执行一个线程的join()方法,进入阻塞状态,当前线程等待这个线程执行完成后,才能执行;
4)线程要执行一段同步代码,没有获取到同步锁时,进入阻塞状态;
何时解除阻塞状态:
1)Thread.sleep(n),休眠N毫秒后进入就绪状态;
2)wait() 需要其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒;
3)join() 需要等到那个线程执行完后,当前线程进入就绪状态
4)获取到同步锁后,进入就绪状态;
死亡:
线程体执行完成。
线程体执行过程中抛异常。
11.4 线程同步
11.4.1 什么是线程同步?
同步:就是“排队”,排队的意思就是不让一起同时进行。
线程同步:几个线程之间进行排队,一个一个对共享资源进行操作。线程同步的目的就是避免线程同时执行。
11.4.2 线程安全问题
由于多个线程执行的不确定性引起执行结果的不稳定
package com.hainiu.th1;
public class BankTest1 {
public static void main(String[] args) {
Bank1Runnable runnable = new Bank1Runnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank1Runnable implements Runnable{
private int money;
@Override
public void run() {
// 当两个线程启动后,可能会存在线程安全问题
// 有可能两个线程同一时间同时存入100
for(int i = 1; i <= 3; i++){
this.money += 100;
System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
}
}
}
11.4.3 线程锁
为了解决可能存在的线程安全问题,可通过线程锁实现线程排队(一个一个的访问共享资源)
争抢使用公共资源权限 → 上锁 → 访问公共资源 → 释放锁
就好比,有一个房间,房间与外面连接的是个门,门里面有个锁,门外面有5个猴子排队进入吃香蕉。这个规则是一只猴子吃一根香蕉,无论多少只猴子同时进入,房间只提供一根香蕉。每次只有一只猴子进来,进来后从里面把门锁上(加锁),吃一根香蕉(抢的这个公共资源),吃完后把里面的锁打开(释放锁)出去,房间再提供一只香蕉,然后再放一只猴子进来吃香蕉……
上面的例子中,里面的猴子把门锁上后,外面的猴子打不开,只能等里面的猴子把锁打开(锁等待)。
如果猴子进房间后不锁门,后果可能就是剩下的4个猴子都进来抢同一根香蕉。这就好多个线程同时抢这个公共资源,可能出现安全问题。
11.4.3.1 synchronized
1)synchronized代码块
语法:
// 同步代码块锁谁取决于代码块里的逻辑
synchronized(obj){
...
//此处的代码就是同步代码块
}
代码示例:
package com.hainiu.th1;
public class BankTest2 {
public static void main(String[] args) {
Bank2Runnable runnable = new Bank2Runnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank2Runnable implements Runnable{
private int money;
@Override
public void run() {
for(int i = 1; i <= 3; i++){
// 通过同步代码块解决了线程安全问题
// 同步代码块锁谁取决于代码块里的逻辑
// 本例是对当前对象的属性进行操作,所以锁的是当前对象
synchronized (this){
this.money += 100;
System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
}
}
}
}
执行多次没有发现错误,都是排队往里存100
2)synchronied方法
语法:
// 谁调用了该同步方法,就锁谁
public synchronized 修饰符 方法名([方法参数]){
//方法体...
}
代码示例:
package com.hainiu.th1;
public class BankTest3 {
public static void main(String[] args) {
Bank3Runnable runnable = new Bank3Runnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank3Runnable implements Runnable{
private int money;
// 通过同步方法解决了线程安全问题
// 谁调用了该同步方法,就锁谁
public synchronized void setMoneyAndPrint(){
this.money += 100;
System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
}
@Override
public void run() {
for(int i = 1; i <= 3; i++){
setMoneyAndPrint();
}
}
}
3)两者区别
同步代码块能设置锁定的对象,而同步方法不能(谁调用同步方法锁谁)。
4)释放锁
同步代码块或同步方法执行完成。
报错抛异常
11.4.3.2 lock
通过显示定义同步锁对象来同步,在这种机制下,同步锁使用Lock 对象充当。
Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作。Lock 是控制多个线程对共享资源进行访问的工具。每次只能有一个线程对 Lock 对象进行加锁,线程开始访问共享资源之前应先获得 Lock 对象。
采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。
比较常用的是ReentrantLock(可重入锁)
可重入锁:
一个线程获取锁后,在没释放这个锁之前,还可以再次获取锁,这样防止死锁。
不可重入锁:
一个线程获取锁后,在没释放这个锁之前,不能再次获取锁。
比如:
// 一个普通锁
lock.lock(); // 第一次获取锁
lock.lock(); // 当第二次获取锁时,由于和上一个lock()操作同一个锁对象,那么这里就永远等待了
...
lock.unlock();
lock.unlock();
死锁:
两个线程,都等待对方释放锁,但都释放不了。
你给我offer我就告诉你。
代码格式:
public class X {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//...
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证相线程安全的代码
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}
代码示例:
package com.hainiu.th1;
import java.util.concurrent.locks.ReentrantLock;
public class BankTest4 {
public static void main(String[] args) {
Bank4Runnable runnable = new Bank4Runnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank4Runnable implements Runnable{
private int money;
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for(int i = 1; i <= 3; i++){
// 加锁
lock.lock();
try{
this.money += 100;
System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
}finally {
// 释放锁
lock.unlock();
}
}
}
}
11.5 线程池
11.5.1 概念
线程池:
装线程的池子。
线程池技术:
1)创建线程池时,会初始化指定个数的线程。
2)当要使用线程执行任务时,从线程池里获取线程,执行任务。
3)执行完任务后,还给线程池。
这样做到线程池里线程的复用。
线程池好处:
线程和任务分离,不需要使用线程时自己new线程对象,提升线程重用性;
提升系统响应速度。
创建线程用的时间为T1;
执行任务用的时间为T2;
销毁线程用的时间为T3;
那么使用线程池就免去了T1和T3的时间;
11.5.2 技术说明
线程池类图:
创建线程池的参数说明:
// new线程池的构造器
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 销毁线程的时间, 如果等于0不销毁
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue) // 线程安全的队列,可以保证每个线程获取一个任务,没有多个线程争抢一个任务的问题
Executors 类的静态方法 :
11.5.3 执行不带返回值的线程
执行不带有返回值的代码示例:
package com.hainiu.th1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest1 {
public static void main(String[] args) {
// 创建只有1个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
// 用1个线程串行运行10个任务
for(int i = 1; i <= 10; i++){
pool.submit(new TaskRunnable("任务" + i));
}
// 关闭线程池
pool.shutdown();
// // 创建有3个线程的线程池
// ExecutorService pool = Executors.newFixedThreadPool(3);
// for(int i = 1; i <= 10; i++){
// // 用3个线程运行10个任务
// pool.submit(new TaskRunnable("任务" + i));
// }
// // 关闭线程池
// pool.shutdown();
}
}
class TaskRunnable implements Runnable{
private String taskName;
public TaskRunnable(String taskName){
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行 " + this.taskName);
}
}
用1个线程串行运行10个任务:
用3个线程运行10个任务:
11.5.4 执行带返回值的线程
执行带有返回值的代码示例:
package com.hainiu.th1;
import java.util.concurrent.*;
public class ThreadPoolTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建有3个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
for(int i = 1; i <= 10; i++){
// 用这3个线程运行10个任务
Future<String> future = pool.submit(new TaskCallable("任务" + i));
String res = future.get();
System.out.println(res);
}
// 关闭线程池
pool.shutdown();
}
}
class TaskCallable implements Callable<String> {
private String taskName;
public TaskCallable(String taskName){
this.taskName = taskName;
}
@Override
public String call() {
// 将哪个线程执行哪个任务的信息返回
String result = Thread.currentThread().getName() + " 执行 " + this.taskName;
return result;
}
}
12 python多线程
12.1 概念
Python 代码的执行有Python 虚拟机来控制。对Python 虚拟机的访问由全局解释器锁(global interpreter lock,GIL)来控制。
GIL规定:在同一时刻,只有一个线程在运行。
Python虚拟机按照以下方式执行:
1)设置GIL;
2)切换到一个线程去执行;
3)运行:
a. 运行指定数量的字节码指令;
b. 或者线程主动让出控制;
4)把线程设置为睡眠状态;
5)解锁GIL;
6)再次重复以上所有步骤;
java:在同一时刻,可以有多个线程并行运行,取决于CPU核数,比如:CPU核是2, 那就能并行运行两个线程
python: 在同一时刻,只有一个线程在运行, 跟CPU核数没关系。
12.2 使用threading模块实现多线程
说明:
1)创建线程
导入模块threading,通过threading.Thread创建线程。
2)启动线程
通过线程对象调用start() 启动线程。
3)执行线程
通过run(),线程逻辑在run() 里实现。
代码示例:
#-*- encoding: utf-8 -*-
"""
thread_demo.py
Created on 21-9-11 下午2:46
Copyright (c) 21-9-11, 海牛学院版权所有.
@author: 野牛
"""
import threading
class ThreadDemo(threading.Thread):
def __init__(self, name):
# 因为父类有,所有需要主动调用
super(self.__class__, self).__init__()
self.name = name
# 线程体方法
def run(self):
print('ThreadDemo # run() ...')
thread_demo = ThreadDemo("hehe")
# 开启线程运行
thread_demo.start()
线程锁
import threading
import time
def task(n):
# 休眠1秒
time.sleep(1)
# 加锁
r_lock.acquire()
try:
for i in range(1, n + 1):
print(i)
finally:
# 释放锁
r_lock.release()
# 创建线程锁对象
r_lock = threading.RLock()
for i in range(1, 6):
t = threading.Thread(target=task,args=(3,))
t.start()
# -*- encoding: utf-8 -*-
"""
thread_demo.py
Created on 2022/7/21 15:47
Copyright (c) 2022/7/21, 海牛学院版权所有.
@author: 野牛
"""
import threading
import time
class Thread1(threading.Thread):
def __init__(self, name, n):
# 主动调用父类初始化
super(self.__class__, self).__init__()
self.name = name
self.n = n
# run()是线程体
def run(self):
# 休眠1秒
time.sleep(1)
# print(f'线程名:{threading.currentThread().getName()}')
r_lock.acquire()
try:
for i in range(1, self.n + 1):
print(i)
finally:
r_lock.release()
r_lock = threading.RLock()
for i in range(1, 6):
# 创建对象
th1 = Thread1(f'thread{i}', 3)
# 调用start()
th1.start()