多线程
1. 线程简介
- 程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 进程:是执行程序的一次执行过正,是一个动态的概念。是系统资源分配的单元
- 线程:通常在一个进程中可以包含若干个线程,至少有一个,不然没有存在的意义。线程是CPU调度和执行的单位
核心概念:
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行有调度器安排调整,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当就会造成数据不一致
2. 线程实现(重点)
三种方式:
2.1 继承Thread类(重点)
不建议使用:避免OOP单继承局限性
package com.cao.Thread;
//创建线程方式一:继承Thread类,重写run方法,调用start开启线程
//总结:注意,线程开启不一定立即执行,是由cpu调度的
public class TestThread1 extends Thread{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 2000; i++) {
System.out.println("i am seeing code------>" + i);
}
}
public static void main(String[] args) {
// main方法,主线程
// 创建一个线程对象
TestThread1 t1 = new TestThread1();
// 调用start()开启线程
t1.start();
for (int i = 0; i < 2000; i++) {
System.out.println("我在看代码---->"+i);
}
}
}
2.2 实现Runnable接口(重点)
推荐使用:避免单继承局限性,灵活方便,方便同一个对象被对各线程使用
package com.cao.Thread;
//创建线程方式二:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 2000; i++) {
System.out.println("i am seeing the code------>" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现对象
TestThread2 t2 = new TestThread2();
//创建线程对象,通过线程对象来开启线程,代理
Thread tt2 = new Thread(t2);
tt2.start();
for (int i = 0; i < 2000; i++) {
System.out.println("我在看代码---->" + i);
}
}
}
龟兔赛跑
package com.cao.Thread;
/**
* 龟兔赛跑
*/
public class Race implements Runnable{
//胜利者
private static String winner;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
/*
if(Thread.currentThread().getName().equals("rabbit")){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
boolean flag = isGameOver(i);
if(flag){
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
// 判断游戏是否结束
private Boolean isGameOver(int steps){
// 判断是否有胜利者
if(winner != null){
return true;
}else{
if(steps >= 100){
winner = Thread.currentThread().getName();
System.out.println("the winner is "+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"turtle").start();
new Thread(race,"rabbit").start();
}
}
2.3 实现Callable接口(了解)
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future
result1 = ser.submint(t1); - 获取结果:boolean r1 = result1.get();
- 关闭服务:ser.shurdownNow();
3. Lambda表达式
3.1 为什么要用lambda表达式
- 避免匿名内部类定义过多
- 可以让代码更加简洁
- 去点无意义diamagnetic,只留下核心逻辑
3.2 函数式接口
-
定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
//此为一个函数式接口Runnable public interface Runnable{ public abstract void run();//只有一个抽象方法 } -
lambda方法是函数式编程
3.3 推导lambda表达式
package com.cao.lambda;
/**
* 推导lambda表达式
*/
public class LambdaTest {
// 3. 静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("i love study lambda222");
}
}
public static void main(String[] args) {
ILike i = new LIke();
i.lambda();
i = new Like2();
i.lambda();
// 4. 局部内部类
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("i love study lambda333");
}
}
i = new Like3();
i.lambda();
// 5. 匿名内部类,没有类的名称,必须借助接口或者父类
i = new ILike() {
@Override
public void lambda() {
System.out.println("i love study lambda444");
}
};
i.lambda();
// 6. 用lambda简化,i是ILike接口的引用,该接口只有一个抽象方法,所以直接省略冗余代码直接实现方法
i = ()->{
System.out.println("i love study lambda555");
};
i.lambda();
}
}
// 1. 定义一个函数式接口
interface ILike{
public void lambda();
}
// 2. 实现类
class LIke implements ILike{
@Override
public void lambda() {
System.out.println("i love study lambda111");
}
}
简化过程
// 原版lambda
lv = (String times)->{
System.out.println("i love you " + times + "times");
};
lv.love("three thousand");
// 简化1.参数类型
lv = (times)->{
System.out.println("i love you " + times + "times");
};
lv.love("three thousand");
// 简化2.括号
lv = times->{
System.out.println("i love you " + times + "times");
};
lv.love("three thousand");
// 简化3.大括号
lv = times->System.out.println("i love you " + times + "times");
lv.love("three thousand");
3.4 总结
- 多个参数也可以简化参数类型,需要都去掉参数类型且加括号
- 必须是函数式接口:接口里面只能有一个方法
- Lambda表达式只能有一行代码的情况下才能简化成为一行,否则需要用代码块
4. 线程状态
4.1 线程的5种状态
- new(新生状态):
Thread t = new Thread()线程一旦创建就进入到了新生状态 - 就绪状态:当调用
start()方法,线程立即进入就绪状态,但不意味着立即执行调度 - 运行状态:进入运行状态,线程才真正执行线程体的代码块
- 阻塞状态:当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待CPU调度执行
- dead(死亡状态):线程中断或者结束,一旦进入死亡状态,就不能再次启动
4.2 线程的几种方法
| Method | introduction |
|---|---|
| setPriority(int newPriority) | 更改线程的优先级 |
| static void sleep(long mills) | 让当前线程休眠指定的毫秒数的时长 |
| void join() | 等待该线程终止 |
| static void yield() | 在听当前正在执行的线程,并执行其他线程 |
| void interrupt() | 中断线程,别使用这个方式 |
| boolean isAlive() | 判断线程是否处于活动状态 |
4.3 线程停止
- 不推荐使用JDK提供的stop()、destroy()方法 【已废弃】
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量,当
flag = false,则终止线程运行
4.3 线程休眠 sleep
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
4.4 线程礼让 yield
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功!看CPU心情
package com.cao.Thread;
// 测试礼让线程
// 礼让不一定成功,看cpu心情
public class TestYield {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"t1").start();
new Thread(myThread,"t2").start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();//礼让
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
4.3 Join
-
Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞
-
可以想象成插队
package com.cao.Thread; // 测试join方法 public class TestJoin implements Runnable{ public static void main(String[] args) throws InterruptedException { TestJoin tj = new TestJoin(); Thread thread = new Thread(tj); thread.start(); for (int i = 0; i < 1000; i++) { if(i == 200){ thread.join(); //插队 } System.out.println("main" + i); } } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("线程VIP来了"+i); } } }
4.4 Thread.getState()
获取当前线程的状态:
- NEW
- RUNNABLE
- TIMED_WAITING
- BLOCKED
- TERMINATED
4.5 线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
-
线程的优先级用数字表示,范围从1~10
- Thread.Min_PRIORITY = 1
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
-
使用以下方式改变或获取优先级
-
getPriority()
-
setPriority(int xxx)
-
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度
-
优先级的设定建议在start()调度前
example
package com.cao.Thread; // 测试线程的优先级 public class TestPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority()); } public static void main(String[] args) { // 主线程优先级 System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority()); TestPriority priority = new TestPriority(); Thread t1 = new Thread(priority); Thread t2 = new Thread(priority); Thread t3 = new Thread(priority); Thread t4 = new Thread(priority); Thread t5 = new Thread(priority); Thread t6 = new Thread(priority); // 先设置优先级,再启动 t1.start(); t2.setPriority(1); t2.start(); t3.setPriority(4); t3.start(); t4.setPriority(Thread.MAX_PRIORITY);//10 t4.start(); } } -
4.6 守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如:后台记录操作日志,监控内存,垃圾回收等待
package com.cao.Thread;
// 测试守护线程
// 上帝守护你
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
// 默认是false表示是用户线程,正常的县城都是用户线程
thread.setDaemon(true);
thread.start();//上帝守护线程启动
new Thread(you).start();// you用户线程启动
}
}
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝保佑着你");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你一生都开心的活着");
}
System.out.println("Goodbye World!");
}
}
5. 线程同步(重点)
5.1 概念
- 现实生活中,会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,最天然的方法就是排队,一个一个来
- 并发:同一个对象被多个线程同时操作
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。此时我们需要用到线程同步,它其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
5.2 线程同步
-
Synchronized由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多项成竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
-
死锁
多个线程各自站有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题 。
死锁产生必须同时满足如下的4个条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
以上4个条件只要有一个不成立,则死锁不会发生。
-
Lock锁
-
从JDK5.0开始,Java提供了更强大的线程同步机制---通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
-
ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
package lock; import com.sun.xml.internal.bind.v2.model.annotation.RuntimeAnnotationReader; import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) { MyLock MyLock = new MyLock(); new Thread(MyLock).start(); new Thread(MyLock).start(); new Thread(MyLock).start(); } } class MyLock implements Runnable{ int ticketNum = 10; private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while(true){ try{ lock.lock(); if(ticketNum > 0){ System.out.println(ticketNum --); Thread.sleep(1000); }else { break; } }catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
-
5.3 Synchronized 与 Lock
| synchronized | Lock |
|---|---|
| 隐式锁,出了作用域自动释放 | 显式锁,手动开启与关闭 |
| 有代码块锁和方法锁 | 只有代码块锁 |
| 使用后,JVM花费较少时间调度线程,性能更好,并且具有更好的扩展性(提供更多的子类) |
优先级使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
6. 线程通信问题
-
Java提供了几个方法解决线程之间的通信问题
方法名 作用 wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 wait(long timeout) 指定等待的毫秒数 notify() 唤醒一个处于等待状态的线程 notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 -
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
- 如果仓库中没有产品,则生产者将产品放入长裤,否则停止生产并等待,知道仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
-
解决方式1:并发写作模型"生产者/消费者模式"--->管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
-
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具(共享单车)。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程看,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTIme:线程没有任务时最多保持多长时间后会终止
浙公网安备 33010602011771号