活动地址:CSDN21天学习挑战赛
目录
线程(thread)是一个程序内部的一条执行路径。
我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
多线程是指从软硬件上实现多条执行流程的技术。
多线程创建的三种方式
方式一:继承Thread
Thread类
Java是通过java.lang.Thread 类来代表线程的。
按照面向对象的思想,Thread类应该提供了实现多线程的方式。
//定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
//创建MyThread类的对象
//调用线程对象的start()方法启动线程(启动后还是执行run方法的)
public class MyThread extends Thread {
int i = 0;
//重写run()
public void run() {
for (; i < 100; i++) {
//当通过继承Thread类的方式实现多线程时,可以直接使用this获取当前执行的线程
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) {
for (int j = 0; j < 50; j++) {
//调用Thread类的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + j);
if (j == 10) {
//创建并启动第一个线程
new MyThread().start();
//创建并启动第二个线程
new MyThread().start();
}
}
}
}
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
方式二:实现Runnable接口
//定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
//创建MyRunnable任务对象
//把MyRunnable任务对象交给Thread处理。
//调用线程对象的start()方法启动线程
public class MyRunnable implements Runnable {
private int i;
@Override
public void run() {
for(;i < 50;i++) {
//当线程类实现Runnable接口时,要获取当前线程对象只有通过Thread.currentThread()获取
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for(int j = 0;j < 30;j++) {
System.out.println(Thread.currentThread().getName() + " " + j);
if(j == 10) {
MyRunnable t= new MyRunnable();
//通过new Thread(target,name)的方式创建线程
new Thread(t,"线程1").start();
new Thread(t,"线程2").start();
}
}
}
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
方式三:JDK 5.0新增:实现Callable接口
前面俩种他们重写的run方法均不能直接返回结果。不适合需要返回线程执行结果的业务场景。
JDK 5.0提供了Callable和FutureTask来实现。
这种方式的优点是:可以得到线程执行的结果。
//定义类实现Callable接口,重写call方法,封装要做的事情。
//用FutureTask把Callable对象封装成线程任务对象。
//把线程任务对象交给Thread处理。
//调用Thread的start方法启动线程,执行任务
//线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建对象
TestCallable testCallable = new TestCallable();
//封装成线程任务对象。
FutureTask<String> futureTask = new FutureTask<>(testCallable);
//启动线程
new Thread(futureTask).start();
//获取返回结果
String s = futureTask.get();
System.out.println(s);
}
//实现Callable接口
static class TestCallable implements Callable<String>{
//方法重写
@Override
public String call() throws Exception {
System.out.println("call execute");
return "success";
}
}
方法名称 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
总结
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
Thread的常用方法
Thread常用API说明
Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread(),sleep()睡觉。
方法名称 | 说明 |
---|---|
String getName() | 获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) | 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称 |
public static Thread currentThread(): | 返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让当前线程休眠指定的时间后再继续执行,单位为毫秒。 |
public void run() | 线程任务方法 |
public void start() | 线程启动方法 |
此方法是Thread类的静态方法,可以直接使用Thread类调用。 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
Thread的构造器
方法名称 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
安全隐患产生原因
存在多线程并发
同时访问共享资源
存在修改共享资源
一般要保证共享资源被多个线程使用时,要保证一个线程完成后,在进行下一个
线程同步
线程同步是为了解决线程的安全问题
线程同步的核心思想
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
解决方式一:同步代码块
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}
锁对象要求理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。
锁对象的规范要求
建议使用共享资源作为锁对象。
对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
解决方式二:同步方法
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
和同步代码块比较同步代码块锁的范围更小,同步方法锁的范围更大。
解决方式三:Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
方法名称 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
线程通信
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
通过共享一个数据的方式实现。根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。
方法名称 | 说明 |
---|---|
void wait() | 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待的单个线程 |
void notifyAll() | 唤醒正在等待的所有线程 |
代码例子:吃汉堡比赛
package org.example;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import static java.lang.Thread.sleep;
@Slf4j(topic = "c.Hanburg")
public class EatHanburg {
static int i1 = 0,i2 = 0,i3 = 0;
public static void main(String[] args) {
Coutainer coutainer = new Coutainer();
new Thread(()->{
while (coutainer.coutainers.size()<=coutainer.getCapacity()){//判断汉堡数量选择去不去做汉堡
coutainer.produce(new Hanburg());
try {
sleep(800);//做一个汉堡的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师一").start();
new Thread(()->{
while (coutainer.coutainers.size()<=coutainer.getCapacity()){
coutainer.produce(new Hanburg());
try {
sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师二").start();
new Thread(()->{
while (coutainer.coutainers.size()<=coutainer.getCapacity()){
coutainer.produce(new Hanburg());
try {
sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"厨师三").start();
new Thread(()->{
while (coutainer.coutainers.size()>0){//判断有没有汉堡
coutainer.eat();//吃汉堡
i1++;//吃的数量增加
try {
sleep(1000);//吃一个汉堡要多长时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客一").start();
new Thread(()->{
while (coutainer.coutainers.size()>0) {
coutainer.eat();
i2++;
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客二").start();
new Thread(()->{
while (coutainer.coutainers.size()>0) {
coutainer.eat();
i3++;
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客三").start();
// try {
// sleep(10000);
// if(i1>=i2){
// if(i1>i3){
// System.out.println("吃货冠军是:顾客一");
// }else{
// System.out.println("吃货冠军是:顾客三");
// }
// }else if(i2>=i3){
// System.out.println("吃货冠军是:顾客二");
// }else{
// System.out.println("吃货冠军是:顾客三");
// }
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
class Hanburg{
//定义汉堡类
public Hanburg() {
}
}
class Coutainer{
//汉堡容器
private final int capacity = 10;
List<Hanburg> coutainers = new LinkedList<>();
public int getCapacity() {
return capacity;
}
//做汉堡方法
public synchronized void produce(Hanburg hanburg){
while (coutainers.size() == capacity){//判断
System.out.println("汉堡满了");
try {
this.wait();//满了睡觉
} catch (InterruptedException e) {
e.printStackTrace();
}
}
coutainers.add(hanburg);//做汉堡
System.out.println(Thread.currentThread().getName()+"做汉堡现在有:"+coutainers.size()+"个");
this.notifyAll();//继续做
}
//吃汉堡方法
public synchronized void eat() {
while (coutainers.size() == 0) {//判断
System.out.println("汉堡没了");
try {
this.wait();//睡觉
} catch (InterruptedException e) {
e.printStackTrace();
}
}
coutainers.remove(coutainers.size() - 1);//吃汉堡
System.out.println(Thread.currentThread().getName() + "吃汉堡现在有:" + coutainers.size() + "个");
this.notifyAll();//继续做
}
}
这个代码是会一直执行的,如果想要产生冠军,需要给每个线程都定义Thread对象变量在注释的代码块中将其使用interrupt();方法,让所有的线程全部关闭。