文章目录
前言
一次只跑一条程序,已经不能满足于我们的日常编程,而我们都能够一遍吃饭一遍看电视,那么程序为什么不能执行着主线程的同时再多跑几条线程呢?
一、多线程
1.进程,线程,多线程
进程:当程序运行的时候就会形成一个进程,而一个进程里面又根据程序的功能可以形成多个线程,每一个线程对应着程序所提供的功能,而当我们同时进行两个线程的时候就是多线程。
2.继承Thread类
在Java程序中如果要实现多线程有三种方法,其为一个类两个接口,类就是Thread类,接口则是Runnable接口和Callable接口。这里主要是通过继承Thread类来进行多线程的操作
示例代码public class Test2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
System.out.println("我在学习++日语"+i);
}
}
public static void main(String[] args) throws InterruptedException{
Test2 test2 = new Test2();
test2.start();
for (int i = 0; i < 60; i++) {
System.out.println("我在学习中文"+i);
}
}
}
部分结果:
我在学习中文43
我在学习++日语37
我在学习中文44
我在学习++日语38
我在学习中文45
我在学习++日语39
由部分结果我们可以看到,线程不一定立即执行,而是会等待cpu的调用。需要注意的是,开启线程是调用start方法而不是直接调用run()方法
1)同时下载多个网图通过继承Thread类
通过Io工具类的FileUtils.copyURLToFile()方法来把网上的图片下载下来,然后通过多线程进行多张图片的同时下载
示例代码public class Test2 extends Thread {
private String url = null;
private String name = null;
public Test2(String url,String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
DownLoad downLoad = new DownLoad();
try {
downLoad.fileDownLoad(url,name);
System.out.println("已下载了"+name);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Test2 test1 = new Test2("https://i0.hdslb.com/bfs/live/3cbafec446f49594a98fe8becbdc72d644438cf1.jpg@672w_378h_1c.webp","图片1.webp");
Test2 test2 = new Test2("https://i0.hdslb.com/bfs/archive/d37e475c854e82f1a6c29ad3cfdf6c3bc1dd7887.jpg@672w_378h_1c.webp", "图片2.webp");
test1.start();
test2.start();
}
}
class DownLoad{
public void fileDownLoad(String url,String name) throws IOException{
FileUtils.copyURLToFile(new URL(url),new File(name));
}
}
3.实现Runnable接口
与继承Thread时的代码并没有太大区别。实现Runnable接口来进行多线程的步骤一般是 实现Runnable接口,重写run()方法,通过创建线程对象去启动
示例代码public class Test2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 40; i++) {
System.out.println("我在学习++日语"+i);
}
}
public static void main(String[] args) throws InterruptedException{
Test2 test2 = new Test2();
new Thread(test2).start();
for (int i = 0; i < 60; i++) {
System.out.println("我在学习中文"+i);
}
}
}
1)下载多个网图通过实现Runnable接口
不需要修改其他的地方只需要把启动的地方改为创建Thread对象再来使用start方法即可
示例代码public class Test2 implements Runnable{
private String url = null;
private String name = null;
public Test2(String url,String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
DownLoad downLoad = new DownLoad();
try {
downLoad.fileDownLoad(url,name);
System.out.println("已下载了"+name);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Test2 test1 = new Test2("https://i0.hdslb.com/bfs/live/3cbafec446f49594a98fe8becbdc72d644438cf1.jpg@672w_378h_1c.webp","图片1.webp");
Test2 test2 = new Test2("https://i0.hdslb.com/bfs/archive/d37e475c854e82f1a6c29ad3cfdf6c3bc1dd7887.jpg@672w_378h_1c.webp", "图片2.webp");
new Thread(test1).start();
new Thread(test2).start();
}
}
class DownLoad{
public void fileDownLoad(String url,String name) throws IOException{
FileUtils.copyURLToFile(new URL(url),new File(name));
}
}
2)实现Runnable接口与继承Thread的区别
继承Thread的方法是子类继承Thread类实现多线程能力,而实现Runnable接口则是实现Runnable接口来获得多线程的能力,但是两者启动线程的方法有别,前者是通过子类对象.statrt()来开启多线程,后者则是通过创建一个Thread对象并传入目标对象.statrt()来开启多线程。而实现Runnable接口比继承Thread类的一个好处就是避免了单继承的局限性
4.实现Callable接口
与实现Runnable接口的时候时,从重写run()方法变更到了重写call()方法,开启线程也从new Thread().start()复杂化到了四个步骤
示例代码//创建执行服务
ExecutorService pool = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> submit = pool.submit(test2);
//返回结果
Boolean result = submit.get();
//关闭服务器
pool.shutdownNow();
5.静态代理
一个类中有一个方法,但是这个方法只能做一件事的时候就可以使用代理。而代理类则比目标类功能强大,但又互不干扰。
示例代码public class Test2{
public static void main(String[] args) {
//
ProxyComp proxyComp = new ProxyComp(new TargetMen());
proxyComp.happyMarry();
}
}
//目标接口
interface Marry{
void happyMarry();
}
//创建需要代理的类,并实现接口
class TargetMen implements Marry{
//重写方法
@Override
public void happyMarry() {
System.out.println("代理人");
}
}
//创建代理类
class ProxyComp implements Marry{
private TargetMen targetMen;
//创建有参构造,传入需要代理的类
public ProxyComp(TargetMen targetMen) {
this.targetMen = targetMen;
}
@Override
//重写方法
public void happyMarry() {
Before();
System.out.println("========");
this.targetMen.happyMarry();
System.out.println("========");
After();
}
//创建代理类做不到的方法
private void Before(){
System.out.println("在代理前需要处理的事务");
}
private void After(){
System.out.println("在代理前需要处理的事务");
}
}
6.Lambda表达式
本质是用来简化匿名内部类的,其使用条件是必须位函数接口,且接口中的方法不多于一个
未使用lambda表达式示例代码public class Test2{
public static void main(String[] args) {
//使用匿名方式实现接口
Marry marry =new Marry(int a) {
@Override
public void happyMarry(int a) {
System.out.println("我是匿名"+a);
}
};
marry.happyMarry(2);
}
}
//目标接口
interface Marry{
void happyMarry(int a);
}
使用lambda表达式示例代码
public class Test2{
public static void main(String[] args) {
//使用lambda表达式
Marry marry =a-> System.out.println("我是匿名"+a);
marry.happyMarry(2);
}
}
//目标接口
interface Marry{
void happyMarry(int a);
}
7.线程的五个状态
1)线程的新生态
当我们创建线程的时候,这时候就是处于新生状态
2)线程的就绪态
当我们使用.start()方法时为就绪态
3)线程的运行态
线程获取cpu的权限开始运行的时候为运行态
4)线程的阻塞态
当线程因为某种原因放弃cpu的使用权的时候,暂停运行,此时为阻塞态。而阻塞态有三种情况:
1、等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成
2、同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态
3、其他阻塞:调用了sleep()或join()时,或发出了I/O请求时候。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5)线程的死亡态
当线程运行完成就进入了死亡态,转让cpu资源,让其他线程使用cpu
8.观测线程状态
上面已经讲述了线程的五个状态,而现在就通过实际代码来更直观的查看线程从新生到死亡的状态
示例代码public class Test1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("啊哈哈哈线程等待咯~"+i);
}
}
}
class ThreadStart{
public static void main(String[] args) throws InterruptedException{
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
//创建线程,此时为新生态
System.out.println(thread.getState());
//线程启动
thread.start();
//此时为进行态
Thread.State state = thread.getState();
System.out.println(state);
while (state!=Thread.State.TERMINATED){
Thread.sleep(100);
state = thread.getState();
}
//线程执行完成,死亡
System.out.println(thread.getState());
}
}
1)线程的停止
在Java中我们查看Thread 的源码的时候,我们可以看到stop方法已经被废弃了,所以自然状态下,要结束一个线程只能等其自动完成后停止。当然我们也可以手动变更标识位的值来结束一个线程
示例代码public class Test2 implements Runnable{
//创立标识位并设置为活跃状态
private boolean flag = true;
@Override
public void run() {
int i=0;
while(flag){
System.out.println(Thread.currentThread().getName()+"线程运行在"+i++);
}
}
//创建停止线程方法
public void stop(){
//将标识位改为停止态
this.flag = false;
}
public static void main(String[] args) {
Test2 test2 = new Test2();
//创建一个新进程
new Thread(test2,"线程1").start();
//开始一千次循环,当循环到900次的时候终止循环
for (int i = 0; i <1000 ; i++) {
System.out.println("main"+i);
if(i==900){
test2.stop();
}
}
}
}
2)线程的阻塞–sleep
通常是让线程进入假堵塞的状态,比如模拟接收信息需要延迟一点时间之类的任务
示例代码public class Test1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i+1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
thread.start();
}
}
这个代码就是模拟从1s数到10s的任务,其中关键之处就是Thread.sleep(1000);这里的代码就
3)线程的阻塞–礼让
礼让就是指,有两个线程A\B,此时A在cpu上运行,然后A礼让cpu的位置,然后和B一起重新让cpu进行调度,此时的结果就是A再次上cpu或者B得到礼让然后上cpu进行运行
示例代码import com.galgame.eriya.pojo.Admin;
import com.galgame.eriya.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.http.HttpResponse;
public class Test1 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"号线程启动");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"号线程停止");
}
}
class TestYield{
public static void main(String[] args) {
Test1 test1 = new Test1();
new Thread(test1,"线程1").start();
new Thread(test1,"线程2").start();
}
}
线程2号线程启动
线程1号线程启动
线程1号线程停止
线程2号线程停止
这里我们可以看到当2号线程启动后就开始礼让,并礼让成功,然后1号线程上cpu运行并一直运行到结束并未再次礼让,最后当1号结束后2号继续运行
4)线程的阻塞–join
简单来说,就是比如你在做核酸检测的时候明明快要排到你了,但是却来了一群胡搅蛮缠的二流子,因为你打不过他们所以只能等待他们做完后你才能接着做。而转换到线程来说就是,当主线程开始执行后,接收到join的指令后,vip线程就开始上cpu执行而只有等到vip线程执行结束后,主线程才能继续执行
示例代码import com.galgame.eriya.pojo.Admin;
import com.galgame.eriya.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.http.HttpResponse;
public class Test1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("啊哈哈哈鸡汤来咯~"+i);
}
}
public static void main(String[] args) throws InterruptedException {
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
thread.start();
for (int i = 0; i < 500; i++) {
if(i==200){
thread.join();
}
System.out.println("主线程"+i);
}
}
}
9.线程的优先级
在生活中,我们做任何事都有一个优先级的顺序,比如我们得先把肚子填饱才能去做其他的事情,所以能吃饱饭是优先级最高的事情,其他的事情就比吃饱饭的优先级要低。而换到程序上来看也是一样的,每个线程都有自己的优先级,优先级的不同代表着执行的顺序不同,而优先级又是从1-10这样排序的,而我i们也可以通过getPriority()和setPriority()来查看一个线程的优先级和修改一个线程的优先级。当然,在我们设置了线程的优先级顺序后cpu也不一定完全按照这个优先级来调度
示例代码public class Test1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
class ThreadStart{
public static void main(String[] args){
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
Thread thread3 = new Thread(test1);
Thread thread4 = new Thread(test1);
thread.setPriority(4);
thread.start();
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.setPriority(6);
thread2.start();
thread3.setPriority(2);
thread3.start();
thread4.setPriority(Thread.MIN_PRIORITY);
thread4.start();
}
}
main-->5
Thread-0-->4
Thread-1-->10
Thread-2-->6
Thread-4-->1
Thread-3-->2
这里我们可以看到主线程一定会在所有线程执行前执行,但是我们之前给线程1设置了最高优先级后任然没有在主线程之后执行,而是由线程0最先执行。由此可见,对于cpu来说线程的优先级更多的是一种参考,真正执行的时候还得看它的心情
10.守护线程
1、线程分为守护线程和用户线程
2、jvm虚拟机必须等待用户线程结束才能结束
3、jvm虚拟机不要要等待守护线程结束就可以结束
示例代码
public class Test1 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("守护还活着");
}
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("用户线程活了"+i+"天了");
}
System.out.println("用户线程死了");
}
}
class ThreadStart{
public static void main(String[] args){
Test1 test1 = new Test1();
You you = new You();
Thread thread = new Thread(test1);
thread.setDaemon(true);
thread.start();
Thread thread1 = new Thread(you);
thread1.start();
}
}
这段程序的结果是当用户线程结束后没过多久,守护线程也结束了。按理说,守护线程会永远执行下去,但是当jvm停止后它也就停止了,所以这正印证了jvm虚拟机不要要等待守护线程结束就可以结束
11.线程同步和锁
1)同步方法和方法块
我们知道,在实现runnable接口后的线程是不安全的,就比如写一个买票的程序,一共有10张票,三个人去抢,但是可以知道的是一张票三个人都会看到,所以会导致三个人可能都会拿到同一张票在没有同步的程序里,而更直观的是在银行取钱的业务中,你有着100元,你和你的兄弟都要取出这一百元,然后你们同时登录取钱都会显示账户有100元,当两人同时点击取钱按钮后,在没有同步的线程中,你们兄弟二人都会拿到100元。
所以给需要的方法同步,才能有效解决这样的问题。而给方法加上同步的方法则是 public synchronized void way(){}在返回值前添加synchronized即可,而同步方法块只需要在方法里面添加一个synchronized(){}在里面写要同步的方法即可
示例代码--买票public class Test1 implements Runnable {
//设置线程状态标志
private boolean flag =true;
//设置票数
int trick=10;
@Override
public void run() {
while(flag){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
//购票方法
public synchronized void buy(){
//如果票清空则把线程状态设为结束
if (trick<=0){
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到票");
trick--;
}
}
class BuyTrick{
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread = new Thread(test1,"线程1号");
Thread thread1 = new Thread(test1, "线程二号");
thread.start();
thread1.start();
}
}
这里我在buy这个方法前面加了synchronized ,这样就会在某一个人买票的时候锁住票,只有当这个人确确实实的拿到了票。且总票数减少后才会再次开放购买
2)死锁
死锁的本质其实就是一个进程占用了一个资源,但是这个资源一次只能被一个进程使用,而另一个进程也占用着一个资源。最后两个进程都需要对方的资源,却又都锁着对方需要的资源,此时就会发生死锁
示例代码public class Test1 implements Runnable {
//设置一个进程一次只能被一个进程所调用
static Phone phone = new Phone();
static Cpu cpu = new Cpu();
int choose;//选择
String name;//选择的进程
public Test1(int choose, String name) {
this.choose = choose;
this.name = name;
}
@Override
public void run() {
try {
use();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//进程使用方法
public void use()throws InterruptedException{
if(choose==0){
//当第一个进程开始
synchronized (phone){//拿到了phone这个资源并上锁
System.out.println(Thread.currentThread().getName()+"拿到了电话锁");
Thread.sleep(1000);
synchronized (cpu){//在拿到phone后想要继续拿到cpu这个资源
System.out.println(Thread.currentThread().getName()+"拿到了cpu锁");
}
}
}else{
synchronized (cpu){//拿到了cpu这个资源并上锁
System.out.println(Thread.currentThread().getName()+"拿到了cpu锁");
Thread.sleep(1000);
synchronized (phone){//在拿到cpu后想要继续拿到phone这个资源
System.out.println(Thread.currentThread().getName()+"拿到了电话锁");
}
}
}
}
}
class Phone{}
class Cpu{}
class Main{
public static void main(String[] args) {
Test1 t1 = new Test1(0, "进程1");
Test1 t2= new Test1(1, "进程2");
//启动线程
new Thread(t1).start();
new Thread(t2).start();
}
}
Thread-0拿到了电话锁
Thread-1拿到了cpu锁
当执行这段程序时,我们会发现一直卡在Thread-0拿到了电话锁 Thread-1拿到了cpu锁 这里然后就没有动静了,但是程序并没有结束此时就发生了死锁
想要结束死锁,只有破除一下条件的任意一种即可
1、互斥条件:一个资源一次只能被一个进程所调用
2、请求与保持条件:一个进程在请求资源阻塞的时候,对其所持有的资源不会释放
3、不剥夺条件:在进程使用完资源前不会强行剥夺资源
4、若干进程形成头尾相接的循环等待资源的关系
而上面代码想要解除死锁状态很简单,只需要把嵌套顺序改变即可
示例代码//当第一个进程开始
synchronized (phone){//拿到了phone这个资源并上锁
System.out.println(Thread.currentThread().getName()+"拿到了电话锁");
Thread.sleep(1000);
}
synchronized (cpu){//在拿到phone后想要继续拿到cpu这个资源
System.out.println(Thread.currentThread().getName()+"拿到了cpu锁");
}
3)lock锁
相比于synchronized方法来说
1、lock是显示锁,没有关闭不会自动关闭,synchronized是隐式锁出了作用域后自动关闭
2、lock只能锁代码块不能锁方法,而synchronized是可以锁方法的
3、lock更高效
示例代码-之前的购票系统的lock版public class Test1 implements Runnable {
//
private final ReentrantLock lock = new ReentrantLock();
//设置线程状态标志
private boolean flag =true;
//设置票数
int trick=10;
@Override
public void run() {
while(flag){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
//购票方法
public void buy(){
lock.lock();
try {
//如果票清空则把线程状态设为结束
if (trick<=0){
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到票");
trick--;
}finally {
lock.unlock();
}
}
}
class BuyTrick{
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread = new Thread(test1,"线程1号");
Thread thread1 = new Thread(test1, "线程二号");
thread.start();
thread1.start();
}
}
12.管程法
本质就是生产者和消费者的问题,进程在原则上是互不干扰的,偶尔是能够在一个进程中设置flag然后停止一个进程。而要两个进程之间又关联就需要一个缓存区,这里就拿鸡的生产和消费来举例子
示例代码public class Test1 {
public static void main(String[] args) {
Buffered buffer = new Buffered();
//生产者开始
new Producer(buffer).start();
//消费者开始
new Consumer(buffer).start();
}
}
//生产者类
class Producer extends Thread{
Buffered buffer;
public Producer(Buffered buffer) {
this.buffer = buffer;
}
@Override
public void run() {
//开始生产
for (int i = 0; i <100 ; i++) {
try {
buffer.push(new Chicken(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者类
class Consumer extends Thread{
Buffered buffer;
public Consumer(Buffered buffer) {
this.buffer = buffer;
}
@Override
public void run() {
//开始消费
for (int i = 0; i <100 ; i++) {
try {
buffer.buy();
System.out.println("消费了"+buffer.buy().id+"只鸡");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//鸡类
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//创建缓存区
class Buffered{
//需要大小
Chicken[] chickens = new Chicken[10];
//计数器
int count=0;
//需要生产者放入产品
public synchronized void push (Chicken chicken)throws InterruptedException{
//如果容器满了,通知消费
while (count==chickens.length){
this.wait();
}
//如果容器没满,把鸡放进去
chickens[count]=chicken;
count++;
this.notifyAll();
}
public synchronized Chicken buy() throws InterruptedException{
//判断能否消费
while(count==0){
System.out.println("不能消费");
this.wait();
}
//缓存区不为0则能消费
count--;
Chicken chicken =chickens[count];
this.notifyAll();
//吃完了通知在生产
return chicken;
}
}
13.线程池
1)线程池的好处
1、把线程都放入一个线程池中后,不需要再频繁的创建和销毁,能够实现用完后再放回
2、效率高
3、降低了资源消耗,并且便于线程的管理
2)线程池的创建
要使用线程池就需要创建ExecutorService接口并使用Executors类中的newFixedThreadPool方法
示例代码public class Test1 {
public static void main(String[] args) throws InterruptedException{
MyThread myThread = new MyThread();
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(myThread);
service.execute(myThread);
service.execute(myThread);
Thread.sleep(3000);
service.shutdown();
System.out.println("是否已经关闭"+service.isShutdown());
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"号进程");
}
}
pool-1-thread-1号进程
pool-1-thread-3号进程
pool-1-thread-2号进程
是否已经关闭true
总结
这估计是我Java篇的最后的文章了,不是不想深入,在校的时候已经自学到微服务了。但是找工作很是碰壁,无奈之下只能转运维了。