多线程
并发和并行
- 并行:在同一时刻,有多个任务在多个CPU上同时进行
- 并发:在同一时刻,有多个任务在单个CPU上交替进行
进程和线程
- 进程:进程简单地说就是在多任务操作系统中,每个独立执行的程序,所以进程也就是“正在进行的程序”。(Windows系统中,我们可以在任务管理器中看到进程)
- 线程:线程是程序运行的基本执行单元。当操作系统执行一个程序时,会在系统中建立一个进程,该进程必须至少建立一个线程(这个线程被称为主线程)作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个线程。
什么是多线程?
- 是指从软件或者硬件上实现多个线程并发执行的技术。
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
多线程开发:并发编程
实现方式1-继承Thread类
- 创建一个子类,继承Thread类
- 在子类中,编写让线程帮助完成的任务
重写Thread类中的run方法 - 启动线程
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("新线程:" + i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程:" + i);
}
}
}
实现方式2-实现Runnable接口
- 创建一个子类,实现Runnable接口
- 在子类中,重写Runnable接口中的方法:run
- 创建Thread类对象,并把实现了Runnable接口的子类对象,作为参数传递给Thread类对象
- 启动线程
class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("新线程:" + i);
}
}
}
public class Demo2 {
public static void main(String[] args) {
RunnableImpl r = new RunnableImpl();
Thread t = new Thread(r);
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程:" + i);
}
}
}
实现方式3-线程池
线程池的概述
-
线程使用存在的问题
- 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
- 如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源
-
线程池的认识
- 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 线程池使用大致流程:
- 创建线程池指定线程开启的数量
- 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务
- 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行
- 如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务
-
线程池的好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建,就能立即执行
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,服务器死机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池处理Runnable任务
-
线程池API的学习
-
java.util.concurrent.ExecutorService是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。获取线程池我们使用工具类java.util.concurrent.Executors的静态方法:
public static ExecutorService newFixedThreadPool(int num)
指定线程池最大线程池数量获取线程池 -
线程池ExecutorService的相关方法:
方法 解释 < T >Future< T > submit(Callable< T > task) 提交执行任务方法 Future< ? > submit(Runnable task) 提交执行任务方法 void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务
-
-
练习:
-
使用线程池模拟游泳教练教学生游泳。游泳馆(线程池)内有3名教练(线程),游泳馆招收了5名学员学习游泳(任务)
-
实现步骤:
- 创建线程池指定3个线程
- 定义学员类实现Runnable
- 创建学员对象给线程池
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class StudentTask implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "在教授学员游泳"); } } public class Test1 { public static void main(String[] args) { // 创建一个线程池,3个线程 ExecutorService es = Executors.newFixedThreadPool(3); // 把线程任务交给线程池执行 es.submit(new StudentTask()); es.submit(new StudentTask()); es.submit(new StudentTask()); es.submit(new StudentTask()); es.submit(new StudentTask()); } }
-
实现方式4-callnable
-
概述
public interface Callable<V> { V call() throws Exception; }
Callable与Runnable的不同点
- Callable支持结果返回,Runnable不行
- Callable可以抛出异常,Runnable不行
-
Callable任务处理使用步骤
- 创建线程池
- 定义Callable任务
- 创建Callable任务,提交任务给线程池
- 获取执行结果
<T> Future<T> submit(Callable<T> task)
提交Callable任务方法返回值类型Future的作用就是为了获取任务执行的结果
Future是一个接口,里面存在一个get方法用来获取值
-
使用线程池计算从0~n的和,并将结果返回
import java.util.concurrent.*; public class Demo4 { public static void main(String[] args) { ExecutorService es = Executors.newFixedThreadPool(10); int n = 10; Future<Integer> result = es.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { Thread.sleep(1000); int sum = 0; for (int i = 1; i < n; i++) { sum += i; } return sum; } }); try { System.out.println(result.get()); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } } }
两种创建线程方式对比
优点 | 缺点 | |
---|---|---|
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可扩展性差,不能再继承其他的类 |
线程类的常用功能
-
在java程序中运行的线程都有属于自己的名字。
- 例如:main方法,就代表主线程(线程名字:main)
- 新线程名字:Thread-0、Thread-1…
方法 解释 String getName() 返回此线程的名称 void setName(String name) 将此线程的名称更改为等于参数name,通过构造方法也可以设置线程名称 public static Thread currentThread() 返回对当前正在执行的线程对象的引用 public static void sleep(long time) 让线程休眠指定的时间,单位为毫秒 public void join() 具备阻塞作用,等待这个线程死亡,才会执行其他线程
线程安全问题
案例:卖票
发生线程安全问题的原因:多个线程对同一个数据,进行读写操作,造成数据错乱
卖票案例数据安全问题的解决
为什么出现问题?
- 多线程操作共享数据
如何解决多线程安全问题?
- 基本思想:让共享数据存在安全的环境中,当某一个线程访问共享数据时,其他线程是无法操作的
怎么实现?
- 把多条线程操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
同步机制
-
线程的同步
- java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性
-
三种实现同步的方式
- 同步代码块
- 格式:synchronized(任意对象) { 多条语句操作共享数据的代码 }
- 默认情况锁时打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
- 锁对象可以是任意对象,但是多个线程必须使用同一把锁
- 同步方法
- 格式:修饰符 synchronized 返回值类型 方法名(方法参数){ }
- 就是把synchronized关键字加到方法上,保证线程执行该方法的时候,其他线程只能在方法外等着
- 同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
- 注意:同步方法时不能指定锁对象的,但是有默认存在的锁对象
- 对于非static方法同步锁就是this
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。class类型的对象
- 锁机制(Lock)
- 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
- Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
- Lock时接口,不能直接实例化
- 注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unlock()方法之间。一定确保unlock最后能够调用
- ReentrantLock是Lock的一个实现类,构造方法:
- ReentrantLock():创建一个ReentrantLock的实例
- 同步代码块
-
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
线程的死锁
- 死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的。
- 死锁产生条件分析:
- 多个线程
- 存在锁对象的循环依赖
线程的状态
- 在java.lang.Thread.State这个枚举中给出了六种线程状态,这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析
虚拟机中线程的六种状态:
- 新建状态(NEW)
- 创建线程对象
- 就绪状态(RUNNABLE)
- start方法
- 阻塞状态(BLOCKED)
- 无法获得锁对象
- 等待状态(WAITING)
- wait方法
- 计时等待(TIMED_WAITING)
- sleep方法
- wait方法
- 结束状态(TERMINATED)
- 全部代码运行完毕
线程间的通讯
-
线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:
-
等待方法
方法 解释 void wait() 让线程进入无限等待 void wait(long timeout) 让线程进入计时等待 以上两个方法调用会导致当前线程释放掉锁资源
-
唤醒方法
方法 解释 void notify() 随机唤醒在此对象监视器(锁对象)上等待的单个线程 void notifyAll() 唤醒在此对象监视器上等待的所有线程 以上两个方法调用不会导致当前线程释放掉锁资源
-
注意:
- 等待和唤醒方法,都要使用锁对象调用(需要在同步代码块中使用)
- 等待和唤醒方法应该使用相同的锁对象调用
-
-
生产者消费者案例
-
概述
- 生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
// 公共资源类 public class Resource { public static int num = 0; public static String lock = "对象锁"; } // 消费者类 public class ConsumerTask implements Runnable { @Override public void run() { synchronized (Resource.lock) { while (true) { // 判断桌子上有没有汉堡 if (Resource.num == 0) { // 如果没有就等待 System.out.println("桌子上没汉堡,消费者等待。。。"); try { Resource.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { // 开吃 System.out.println("消费者消费中。。。"); Resource.num--; // 吃完唤醒等待的生产者继续生产 System.out.println("消费者唤醒生产者"); Resource.lock.notify(); } } } } } // 生产者类 public class ProducerTask implements Runnable { @Override public void run() { synchronized (Resource.lock) { while (true) { //判断桌子上有没有汉堡 if (Resource.num != 0) { System.out.println("桌子上有汉堡,生产者等待。。。"); // 如果有就等待 try { Resource.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { // 如果没有就生产 System.out.println("生产者开始生产。。。"); Resource.num++; // 生产完唤醒消费者消费 System.out.println("生产者唤醒消费者"); Resource.lock.notify(); } } } } } // 测试类 public class Test1 { public static void main(String[] args) { ProducerTask pt = new ProducerTask(); ConsumerTask ct = new ConsumerTask(); Thread pt1 = new Thread(pt); Thread ct1 = new Thread(ct); pt1.start(); ct1.start(); } }
-