1、线程介绍
(1) 在操作系统中运行的程序就是进程
。比如你的QQ,网易云音乐,腾讯视频等。说起进程。就不得不说下程序
。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。通常在一个进程中至少存在一个线程
,线程是CPU调度和执行的单位。
(2) 核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程。如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的;
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如CPU调度时间,并发控制开销;
- 每个线程在自己的工作内容交互,内存控制不当会造成数据不一致;
2、线程实现(重点)
(1)继承Thread类
- 自定义线程类继承
Thread
类; - 重写
run()
方法,编写线程执行体; - 创建线程对象,调用
start()
方法启动线程
public class Thread_1 extends Thread { //1.继承Thread类
@Override
public void run() { //2.重写run()方法
//子线程
for (int i = 0;i < 1000;i++) {
System.out.println("子线程:" + i);
}
}
public static void main(String[] args) {
Thread_1 t = new Thread_1(); //3.创建对象
t.start(); //4.调用start()
//mian主线程
for (int i = 0;i < 1000;i++) {
System.out.println("主线程:" + i);
}
}
}
执行结果可以看到两个线程交替进行:
(2)实现Runnable接口
- 自定义类实现
Runnable
接口 - 实现
run()
方法,编写线程执行体 - 将实现类作为参数来创建线程对象,调用
start()
方法启动线程
public class Thread_2 implements Runnable {
@Override
public void run() {
//子线程
for (int i = 0;i < 1000;i++) {
System.out.println("子线程:" + i);
}
}
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Thread_2 t_2 = new Thread_2();
//创建线程对象,通过线程对象来启动线程
Thread t = new Thread(t_2);
t.start();
//主线程
for (int i = 0;i < 1000;i++) {
System.out.println("主线程:" + i);
}
}
}
(3)实现Callable接口(了解,不是重点)
- 实现Callable接口,需要返回值类型(通过泛型指定)
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(t);
- 获取结果:boolean r1 = result1.get();
- 关闭服务:ser.shutdownNow();
public class Thread_4 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
//子线程
for (int i = 0;i < 10;i++) {
System.out.println(Thread.currentThread() + " 子线程:" + i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread_4 t1 = new Thread_4();
Thread_4 t2 = new Thread_4();
Thread_4 t3 = new Thread_4();
ExecutorService ser = Executors.newFixedThreadPool(3);
Future<Boolean> result1 = ser.submit(t1);
Future<Boolean> result2 = ser.submit(t2);
Future<Boolean> result3 = ser.submit(t3);
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();
ser.shutdownNow();
}
}
3、Thread——静态代理模式
//静态代理
interface Marry {
void marry();
}
//实际角色
class Person implements Marry {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public void marry() {
System.out.println(name + ",结婚了!!!");
}
}
//婚庆公司
class Company implements Marry {
private Marry marry;
public Company(Person person) {
this.marry = person;
}
@Override
public void marry() {
this.beforeMarry();
marry.marry();
this.afterMarry();
}
private void beforeMarry() {
System.out.println("布置现场!");
}
private void afterMarry() {
System.out.println("打扫现场,整理工具!");
}
}
public class StaticAgent {
public static void main(String[] args) {
String name = "小明";
Person person = new Person(name); //某人表示需要结婚
Company company = new Company(person); //联系婚庆公司
company.marry(); //婚庆公司代为主办方举办婚礼
}
}
事实上Thread类
就相当于静态代理,通过代理模式实现对Runnable
接口实现对象。marry()
方法就类似于start()
方法。
4、Lamda表达式
(1)为什么要使用Lamda表达式
- 避免匿名内部类定义过多;
- 可以使代码看起来更整洁;
- 去掉一堆没有意义的代码,只留下核心概念;
(2)理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在
- 函数式接口定义:
– 任何接口,如果只包含唯一的抽象方法
,那么它就是一个函数式接口;
– 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象;
(3)代码实现
//1.定义一个函数式接口
interface ILike {
void lambda();
}
//2.实现类
class Like implements ILike {
@Override
public void lambda() {
System.out.println("我是ILike的实现类Like!");
}
}
public class LambdaDemo {
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
}
}
显然在类LambdaDemo
的外面比较麻烦,对其进行优化(静态内部类
):
//1.定义一个函数式接口
interface ILike {
void lambda();
}
public class LambdaDemo {
//3.静态内部类
static class Like1 implements ILike {
@Override
public void lambda() {
System.out.println("我是ILike的静态内部类Like1!");
}
}
public static void main(String[] args) {
ILike like1 = new Like1();
like1.lambda();
}
}
进行局部内部类
改造:
//1.定义一个函数式接口
interface ILike {
void lambda();
}
public class LambdaDemo {
public static void main(String[] args) {
//4.局部内部类
class Like2 implements ILike {
@Override
public void lambda() {
System.out.println("我是ILike的局部内部类Like2!");
}
}
ILike like2 = new Like2();
like2.lambda();
}
}
还可以进行匿名内部类
的改造:
//1.定义一个函数式接口
interface ILike {
void lambda();
}
public class LambdaDemo {
public static void main(String[] args) {
//5.匿名内部类
ILike like3 = new Like(){
@Override
public void lambda() {
System.out.println("我是ILike的匿名内部类Like3!");
}
};
like3.lambda();
}
}
最后在Java8新特性之后用Lambda表达式
进行进一步简化:
//1.定义一个函数式接口
interface ILike {
void lambda();
}
public class LambdaDemo {
public static void main(String[] args) {
//6.Lambda表达式简化
ILike like4 = ()->{
System.out.println("我是ILike的Lambda表达式简化实现类Like4!");
};
like4.lambda();
}
}
其中()
中可以进行参数传递。
5、线程五大状态
5.1 线程状态
5.2 线程方法:
5.2.1 线程停止
- 建议线程正常停止——>利用次数,不建议死循环;
- 建议使用标志位;
- JDK不建议使用stop()和destory()方法停止线程;
5.2.2 线程休眠——sleep()
sleep(时间)
指定当前线程阻塞的毫秒数;- sleep存在异常InterruptedException;
- sleep阻塞时间达到后进入就绪状态;
- sleep可以模拟网延时,倒计时等;
- 每一个对象都有一个锁,
sleep不会释放锁
;
5.2.3 线程礼让——yield()
- 礼让线程,让当前正在执行的线程暂停,但不阻塞;
- 让线程从运行状态转为就绪状态;
让CPU重新调度,礼让不一定成功!看CPU心情
;
5.2.4 线程强制执行——join()
- Join合并线程,将其他线程阻塞,待此线程执行完毕后,在执行其他线程;
- 可以想象成插队;
public class JoinDemo implements Runnable {
@Override
public void run() {
//子线程
for(int i = 0;i < 100;i++) {
System.out.println("vip线程 " + i);
}
}
public static void main(String[] args) throws InterruptedException {
JoinDemo j = new JoinDemo();
Thread t = new Thread(j);
t.start();
//主线程
for(int i = 0;i < 100;i++) {
if(i == 50) {
t.join();
}
System.out.println("主线程 " + i);
}
}
}
5.3 线程状态——Thread.State
- NEW:尚未启动的线程处于此状态;
- RUNNABLE:在Java虚拟机中执行的线程处于此状态。
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态;
- WAITING:正在等待另一个线程执行特定动作的线程处于此状态;
- TIMED_WAITING:正在等待另一个线程执行动作达到指定时间的线程处于此状态;
- TERMINATED:已退出的线程处于此状态;
5.4 线程优先级
- Java提供一个线程调度器来监控程序启动后进入就绪状态的所有线程,线程调度器按照优先级决定哪个线程来执行;
- 线程优先级用数字表示,范围从1~10:
– Thread.MIN_PRIORITY=1;
– Thread.NORM_PRIORITY=5;(默认)
– Thread.MAX_PRIORITY=10; - 使用以下方式改变或获取优先级:
– getPriority() | setPriority(int priority);
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被先调用,这都取决于CPU的调度。
5.5 守护线程
- 线程分为
用户线程
和守护线程
; - 虚拟机必须确保用户线程执行完毕;
- 虚拟机不用等待守护线程执行完毕,如:后台记录操作日志,监控内存,垃圾回收等;
6、线程同步(重点)
6.1 多个线程操作同一资源,引起的并发问题
- 现实生活中,我们会遇到“同一个资源,多个人都想使用”的 问题,比如食堂排队打饭,每个人都想吃饭,最天然的办法就是排队一个个来。
- 处理多线程问题时,多个线程访问同一对象,并且某些线程 还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个同时访问此对象的线程进入到
对象的等待池
形成队列,等待前面线程使用完毕,下一个线程再使用。
6.2 线程同步
- 由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入
锁机制synchronized
。当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
– 一个线程持有锁会导致其他所有需要此锁的线程挂起;
– 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
– 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题;
线程不安全事例:
public class TestArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList();
for(int i = 0;i < 10000;i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
创建10000个线程,每个线程往list中添加一条数据,打印结果应该为10000条数据才对,而期间可能出现多个线程抢占list中同一地址造成数据覆盖导致实际数据量小于10000,可以看出ArrayList是线程不安全,进而感受到线程同步的重要性。
6.3 线程同步方法
6.3.1 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种方法:synchronized方法和synchronized块
;
同步方法:public synchronized void method(int args) {}
6.3.2 synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行;
缺陷:若将一个大的方法申明为synchronized将会影响效率
public class TestSynMethod {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"苦逼的我").start();
new Thread(buyTicket,"牛逼的你").start();
new Thread(buyTicket,"可恶的黄牛").start();
}
}
class BuyTicket implements Runnable {
private int ticketNums = 10; //票
boolean flag = true; //外部停止方式
@Override
public void run() {
//买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "拿到 " + ticketNums--);
}
}
6.4 同步块
- 同步块:synchronized
(Obj){ }
Obj
:称之为同步监听器
– Obj可以使任何对象,但是推荐使用共享资源作为同步监听器
– 同步方法中无需指定同步监听器,因为同步方法的同步监听器就是this,就是这个对象本身,或者是class【反射】- 同步监听器的执行过程:
– 第一个线程访问,锁定同步监听器,执行其中代码;
– 第二个线程访问,发现同步监听器被锁定,无法访问;
– 第一个线程访问完毕,解锁同步监听器;
– 第二个线程访问,发现同步监听器没有锁,然后锁定并访问。
public class TestSynArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList();
for(int i = 0;i < 10000;i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
6.5 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,进而导致两个或者多个线程都在等待对方释放资源陷入停滞情形。当某一同步块同时拥有“两个以上对象的锁
”时,就可能发生“死锁”问题。
6.5.1 产生死锁的四个必要条件
互斥条件
:一个资源每次只能被一个进程使用;请求与保持条件
:一个进程因请求资源而阻塞时,对已获得的资源保持不变;不剥夺条件
:进程已获得资源,在未使用完之前,不能强行剥夺;循环等待条件
:若干进程之间形成一种头尾相接的循环等待资源关系;
上面列出的条件中任意一个或多个被破解解可以避免死锁。
6.6 Lock锁
- 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用
Lock对象
充当; java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;ReentrantLock
类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁;
class A {
private final ReentrantLock lock = new ReentrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码
}catch (Exception e){
}finally {
lock.unlock();
//如果同步代码有异常,要将unlock()写入finally语句块
}
}
}
6.6.1 synchronized与Lock的对比
- Lock是显示锁(手动开启和关闭锁,不要忘记关闭);synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁;synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
- 优先使用顺序:
– Lock > 同步代码块(已经进入了方法体,分配了相应的资源) > 同步方法(在方法体之外)
7、线程通信问题
7.1 线程协作:生产者消费者模式
(1)应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费;
- 如果仓库中没有产品,则生产者将商品放入仓库,否则停止生产并等待,知道仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中放入产品为止;
(2)这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费;
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要生产新产品以供消费;
- 在生产者消费者问题中,仅有synchroized是不够的
– synchroized可组阻止并发更新同一个共享资源,实现同步;
– synchroized 不能用来实现不同线程之间的消息传递(通信);
(3)Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,知道其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意
:均是Object类的方法,都只能在同步方法或者同步代码中使用,否则会抛出异常IllegalMonitorStateException
(4)解决方式1
并发协作模型“生产者/消费者模式”——>管程法
- 生产者:负责生成数据的模块(可能是方法,对象,线程,进程);
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
- 缓冲区:消费者不能直接使用生产者数据,它们之间有个缓冲区;
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
//管程法
public class MonitorMethod {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者
class Productor extends Thread {
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
//生成
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("生产第 " + i + " 只鸡!");
container.push(new Chicken(i));
}
}
}
//消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Chicken chicken = container.pop();
System.out.println("消费了第 " + chicken.getId() + " 只鸡!");
}
}
}
//产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
//缓冲区
class SynContainer {
Chicken[] chickens = new Chicken[10]; //容器大小
int count = 0; //容器计数器
//生产者放入产品
public synchronized void push(Chicken chicken) {
if (count == chickens.length) {
//通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count] = chicken;
count++;
//通知消费者消费
this.notifyAll();
}
//消费者放入产品
public synchronized Chicken pop() {
if (count == 0) {
//通知生成者生成,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Chicken chicken = chickens[count];
//通知生成者消费
this.notifyAll();
return chicken;
}
}
(5)解决方式2
并发协作模型“生产者/消费者模式”——>信号灯法
//信号灯法
public class LightMethod {
public static void main(String[] args) {
TVProgram program = new TVProgram();
new Player(program).start();
new Spectator(program).start();
}
}
//生产者—>演员
class Player extends Thread {
TVProgram program;
public Player(TVProgram program) {
this.program = program;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if(i%2==0) {
program.play("正在播放:葫芦娃第" + i +"集!");
} else {
program.play("广告时间!!!");
}
}
}
}
//消费者—>观众
class Spectator extends Thread {
TVProgram program;
public Spectator(TVProgram program) {
this.program = program;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
program.watch();
}
}
}
//产品—>节目
class TVProgram {
//演员表演,观众等待 T
//观众观看,演员等待 F
String voice;
boolean flag = true;
//表演
public synchronized void play(String voice) {
if(!flag) { //演员等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
//唤醒观众观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if(flag) { //观众等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
//唤醒演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
8、线程池
(1)背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
(2)思路:提前创建好多线程,放入线程池,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用(类似于公共汽车)。
(3)好处:
- 提高响应速度(减少创建线程的时间);
- 降低资源消耗(重复利用线程池中的线程,不需要每次创建);
- 便于线程管理:
– corePoolSize:核心池大小
– maximumPoolSize:最大线程数
– keepAliveTime:线程没有任务时最多保持多长时间会终止
(4)
- JDK 5.0起提供了线程池相关API:
ExecutorService
和Executors
- ExecutorService:真正的线程池接口,常见子类
ThreadPollExecutor
:
– void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable;
– Future submit(Callable task):执行任务,有返回值,一般用来执行Callable;
– void shutdown():关闭线程池; - Executors:工具类、线程池的工厂类,用于创建 并返回不同类型的线程池