多线程
Posted Sleepinglion
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关的知识,希望对你有一定的参考价值。
多线程
第一部分:线程
1. 并发和并行
并发:两个或多个事件在同一时间间隔发生。(交替执行)
并行:两个或多个事件在同一时刻发生。(同时执行)
2. 进程和线程
进程:是指一个内存中运行的应用程序,每个进程都有一块独立的内存空间,一个应用程序可以运行多个进
程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是进程从创建、运行到消亡
的过程。
线程:线程是进程中的一个执行单元,负责程序的运行,一个进程中至少有一个线程。一个进程中是可以有多
个线程的,这个应用程序也称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程可以包含多个线程。
3. CPU
cpu:又称中央处理器,包含运算器和控制器。
作用:对数据进行处理,控制电脑中软件和硬件的运行。
注意:电脑是可以有多个cpu的,但市场上的家用电脑一般都只有一个cpu。
4. 线程调度
分时调度:所有线程轮流分配cpu的使用权,平均分配每个线程占用cpu的时间。
抢占式调度:优先让优先级高的线程使用cpu,如果线程的优先级相同,会随机选择一个(线程随机性),java
使用的为抢占式调度。
5. 创建线程类
5.1 继承Thread类(java.lang.Thread)
- 创建Thread类的子类
- 子类重写Thread类中的run(),设置线程任务
- 创建Thread子类对象
- 调用Thread类中的start(),开启新的线程,执行run()
Thread类常用构造方法、方法
常用构造方法:
public Thread(); 分配一个新的线程对象
public Thread(String name); 分配一个指定名字的新的线程对象
public Thread(Runnable target); 分配一个带有指定目标的新的线程对象
public Thread(Runnable target,String name); 分配一个带有指定目标的新的线程对象并指定名字
常用方法:
public String getName(); 获取当前线程的名称
public void start(); 导致此线程开始执行;JVM调用此线程的run()
public void run(); 此线程要执行的任务在此处定义代码(线程代码体)
public static Thread currentThread(); 返回对当前正在执行的线程对象的引用
public void setName(String name) 设置当前线程的名称
public static void sleep(Long millis); 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
//1.创建Thread类的子类
class MyThread extends Thread{
private String name;
//2.重写run()
@Override
public void run() {
for (int i = 0; i < 15; i++) {
System.out.println("run: " + i);
}
}
}
public class Demo1Thread {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的start(),开启新线程,执行run();
mt.start();
//主线程会继续执行主方法中的代码
for (int i = 0; i < 15; i++) {
System.out.println("main:" + i);
}
}
}
上述多线程代码实现原理图:
多线程原理:内存图解
5.2 实现Runnable接口(java.lang.Runnable)
- 创建Runnable接口实现类
- 实现类重写Runnable接口中的run(),设置线程任务
- 创建接口实现类对象
- 创建Thread类对象:通过有参构造器传递实现类对象
- 调用Thread类中的start(),开启新的线程,执行run()
实现Runnable接口创建多线程的好处:
1.避免了单继承的局限性
java中一个类只能直接继承一个类,类继承了Thread类就不能再继承其它类
实现了Runnable接口,还能继承其它的类,实现其它的接口
2.增强了程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)
实现类中,重写了run():设置线程任务
创建Thread类对象,调用start(): 用来开启新线程
//1.创建Runnable接口的实现类
class RunnableImpl implements Runnable{
//2.重写Runnable接口的run();
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
public class Demo1Runnable {
public static void main(String[] args) {
//3.创建实现类对象
RunnableImpl mt = new RunnableImpl();
//4.创建Thread类对象:构造方法中传递Runnable接口中的实现类对象
Thread t = new Thread(mt);
//5.调用Thread类的start()
t.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
5.3 匿名内部类方式实现多线程的创建
/*匿名内部类实现多线程的创建:
匿名:没有名字
内部类:写在其它类内部的类
匿名内部类的作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成
把实现类,实现类接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
格式:
new 父类/接口(){
重写父类/接口中的方法
};
*/
public class Demo1InnerClassThread {
public static void main(String[] args) {
//线程的父类是Thread
new Thread() {
//重写Thread类中的run()
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "Java");
}
}
}.start();
//线程的接口是Runnable
Runnable r = new Runnable(){
@Override
public void run() {
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "程序员");
}
}
};
new Thread(r).start();
//简化接口的方式
new Thread(new Runnable(){
public void run(){
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "持续努力");
}
}
}).start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
第二部分:线程安全
1. 概述
多线程程序访问共享数据,会产生线程安全问题。
2. 线程安全问题产生的原理
3. 解决线程安全问题?
线程同步机制:
- 同步代码块
- 同步方法
- 锁机制
3.1 同步代码块
/*
模拟卖票案例:
创建3个线程,同时开启,对共享的票进行出售。
卖票案例出现了线程安全问题:卖出了不存在的票和重复的票。
解决线程安全问题的第一种方式:使用同步代码块
格式:
synchronized(锁对象){
需要同步操作的代码;
}
注意:
1.同步代码块中的锁对象,可以是任意对象。
2.必须保证多个线程使用的锁对象是同一个。
3.锁对象的作用:
把同步代码块锁住,只让一个线程在同步代码块中执行。
*/
public class Demo1Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用Thread类的start():开启新的线程,执行run()
t0.start();
t1.start();
t2.start();
}
}
class RunnableImpl implements Runnable{
//定义一个多个线程的票源
private int ticket = 100;
//创建一个锁对象
Object obj = new Object();
//设置线程任务:卖票
public void run(){
//使用死循环,让卖票操作重复执行
while(true) {
//先判断票是否存在
synchronized(obj){
if (ticket > 0) {
//提高程序安全问题出现的概率,线程休眠10ms
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
同步技术的原理图解:
3.2 同步方法
/*
解决线程安全问题的第二种方式:使用同步方法
实现步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中;
2.在方法上添加synchronized修饰符
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
*/
public class Demo1Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("run:" + run); //@1b6d3586
//创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用Thread类的start():开启新的线程,执行run()
t0.start();
t1.start();
t2.start();
}
}
class RunnableImpl implements Runnable{
//定义一个多个线程的票源
private int ticket = 100;
//设置线程任务:卖票
public void run(){
System.out.println(this); //@1b6d3586
//使用死循环,让卖票操作重复执行
while(true) {
payTicket();
}
}
/*
定义一个同步方法:
同步方法也会把方法内部的代码锁住,只让一个线程执行。
其锁对象是:实现类对象 new RunnableImpl();
也就是this。
*/
public synchronized void payTicket() {
//先判断票是否存在
if (ticket > 0) {
//提高程序安全问题出现的概率,线程休眠10ms
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
3.3 Lock锁
/* 解决线程安全问题的第三种方式:使用Lock锁
java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock(); 获取锁
void unlock(); 释放锁
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现线程安全问题的代码前调用Lock接口中的lock()获取锁
3.在可能会出现线程安全问题的代码后调用Lock接口中的unlock()释放锁
*/
public class Demo1Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用Thread类的start():开启新的线程,执行run()
t0.start();
t1.start();
t2.start();
}
}
class RunnableImpl implements Runnable{
//定义一个多个线程的票源
private int ticket = 100;
//创建一个ReentrantLock对象:多态
Lock l = new ReentrantLock();
public void run(){
//使用死循环,让卖票操作重复执行
while(true) {
//在可能会出现线程安全问题的代码块前调用lock(): 获取锁
l.lock();
//先判断票是否存在
if (ticket > 0) {
//提高程序安全问题出现的概率,线程休眠10ms
try {
Thread.sleep(10);
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//在可能会出现线程安全问题的代码块后调用unlock(): 释放锁
l.unlock(); //无论程序是否异常,都会把锁释放。
}
}
}
}
}
第三部分:线程状态
1. 线程状态图
2. 线程通信
2.1 线程间通信:
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
2.2 为什么要处理线程通信?
多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我
们希望他们有规律的执行,那么线程之间需要一些协调通信,以此来帮我们达到多线程共同操作同一份数据。
2.3 等待唤醒机制
保证线程间通信有效利用资源。
/* 等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老边要吃包子的种类和数量,调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)
创建一个老板线程(生产者):花了5秒钟做包子,做好包子之后,调用notify(),唤醒顾客吃包子
注意:
顾客和老板必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait(),notify()
Object类中的方法:
void wait(); 在其它线程调用此对象的notify()或notifyAll()方法前,导致当前线程等待
void notify(); 唤醒在此对象监视器上等待的单个线程
会继续执行wait()方法之后的代码。
*/
public class Demo1WaitAndNotify {
public static void main(String[] args) {
//创建锁对象;
Object obj = new Object();
//创建顾客线程:
new Thread(){
@Override
public void run() {
System.out.println("告知老板要吃包子的种类和数量:");
synchronized(obj){
try {
//调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//继续执行wait()方法之后的代码
System.out.println("饿死我了,开吃!");
}
}
}.start();
//创建老板线程:
new Thread(){
public void run(){
//花5秒做包子
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("包子已经做好,唤醒顾客吃包子。");
//做好包子之后,唤醒顾客吃包子
synchronized(obj){
obj.notify();
}
}
}.start();
}
}
/* 进入到TimeWaiting(计时等待)有两种方式:
1.使用sleep(long millis):在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long millis):wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
1.void notify(); 唤醒在此对象监视器上等待的单个线程
2.void notifyAll(); 唤醒在此对象监视器上等待的所有线程
*/
public class Demo2WaitAndNotify {
public static void main(String[] args) {
//创建锁对象;
Object obj = new Object();
//创建顾客线程:
new Thread(){
@Override
public void run() {
System.out.println("告知老板要吃包子的种类和数量:");
synchronized(obj){
try {
//调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)
obj.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//继续执行wait()方法之后的代码
System.out.println("饿死我了,开吃!");
}
}
}.start();
}
}
3. 线程池
3.1 概念
线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
好处:
a. 降低资源消耗。减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务。
b. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
c. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下
(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机。)
3.2 线程池的使用
import java.util.concurrent.*;
/* 线程池:JDK1.5之后提供
java.util.current.Executors;线程池的工厂类,用来生成线程池。
Executors中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池。
参数:
int nThreads: 创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口实现类对象,可以使用ExecutorService接口接收(面向接口编程)
java.util.current.ExecutorService;线程池接口
用来从线程池中获取线程,调用start(),执行线程任务。
submit(Runnable task) 提交一个Runnable任务用于执行
关闭/销毁线程池的方法:
void shutdown();
线程池的使用步骤:
1.使用线程池工厂类Executors里面提供的静态方法newFixedThreadPool生成一个指定线程数量的线程池
2.创建一个Runnable接口实现类,重写run(),设置线程任务
3.调用ExecutorService中的submit(),传递线程任务(实现类),开启线程,执行run()
4.调用ExecutorService中的shutdown(),销毁线程池(不建议执行)。
*/
public class Demo1ThreadPool {
public static void main(String[] args) {
//1.创建线程池:使用Executor类中的静态方法newFixedThreadPool(int nThreads)
ExecutorService es = Executors.newFixedThreadPool(2);
//3.调用ExecutorService中的submit(),传递线程任务(实现类对象),开启线程,执行run()
es.submit(new RunnableImpl());
//线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程还可以继续使用。
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
//4.调用ExecutorService中的shutdown(),销毁线程池(不建议执行!)
es.shutdown();
//线程池都没有了,不能再使用线程。
es.submit(new RunnableImpl());//抛出异常:RejectedExecutionException
}
}
//2.创建Runnable接口实现类,重写run()
class RunnableImpl implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName() + "创建了一个新的线程正在执行");
}
}
4. Lambda表达式
4.1 函数式编程思想概述
4.2 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
-
使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable、Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
-
使用Lambda必须具有上下文推断:
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为"函数式接口"。
4.3 Lambda表达式标准格式
/* Lambda表达式的标准格式:
由三部分组成:
a.一些参数
b.一个箭头
c.一段代码
格式:
(参数列表) -> {一段重写方法的代码}
解释说明格式:
():接口中抽象方法的参数列表,没有参数,就空着;有就写出参数,多个参数之间使用逗号分隔。
->:传递的意思,把方法传递给方法体{}
{}:重写接口中的抽象方法的方法体
*/
public class Demo1Lambda {
public static void main(String[] args) {
//使用匿名内部类的方式,实现多线程
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
}).start();
//使用Lambda表达式,实现多线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}).start();
//优化省略Lambda:
new Thread(() -> System.out.println(Thread.currentThread().getName() + "新线程创建了")).start();
}
}
4.4 Lambda的省略格式
以上是关于多线程的主要内容,如果未能解决你的问题,请参考以下文章