JavaEE学习——多线程基础和进阶

目录

第一个线程helloworld

创建一个类,继承Thread,重写run方法 是class

创建一个类,实现Runnable,重写run方法 是interface如下

继承Thread,重写run方法,基于匿名内部类

实现Runnable ,重写run,基于匿名内部类

当然了还可以用lambda表达式来写。

Thread的常见构造方法

如何终止一个线程?

1.手动设置一个标志位让run尽快结束。

2.直接使用Thread类,有提供现成的标志位,不用咋们手动去设置标志

等待一个线程join()

join的两种方法

线程的状态

线程安全

根本原因多个线程之间的调度是随机的,操作系统使用抢占式的操作来安排调度 。

多个线程同时修改同一个变量,容易产生线程安全问题,其他的穷狂都没事。但可以通过调整代码结构来规避这种情况

进行的修改不是原子的 

内存可见性引起的线程安全的问题

指令重排序引起的问题 

内存可见性引起的问题

 

volatite

wait和notify

wait和sleep区别


我们已经在上一节的内容中简单的介绍了进程和线程的内容和之间关系,在这一节中将会着重讲述在java编程中如何使用多线程来处理各种的问题。

第一个线程helloworld

public class demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        //myThread.run();

    }
}
class  MyThread extends  Thread{
    @Override
    public void run() {
        System.out.println("hello world");
    }
}


\\hello world

MyThread的run相当于线程的入口,方法,线程跑起来后要做什么事由它描述

start()这个操作就是在底层调用操作系统提供的创建线程的apl,同时在操作系统内核中创建出对应的pcb结构,并且加入到对应的链表中

此时这个新创建出来的线程就会参与到cpu的调度中去,执行run方法。

使用对象调用run方法也同样可以输出helloworld,但是二者由本质区别,start会调用系统apl但run只是个普通方法,没有调用系统apl,没有创建出新的线程出来

当点击运行程序的时候就会先创建出一个java进程,这个进程中至少包含了一个线程,这个线程也叫做主线程就,也就是负责执行main方法的线程。如果每个线程都可以独立的调用执行,就说创建了新的线程

public class demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for(int i=0;i<1000;i++){
            System.out.println("main");
        }
    }
}
class  MyThread extends  Thread{
    @Override
    public void run() {
        for(int i=0;i<1000;i++)
        System.out.println("hello world");
    }
}
/*
...
main
main
hello world
hello world
hello world
main
main
main
hello world
...
*/

使用start()两个内容交替打印输出,说明创建了新的线程,主线程和新线程是并发执行的关系,

如果使用run()。则会先打印完run方法的内容再打印run方法下面的循环。

sleep是Thread的一个静态方法,可以使得线程休眠,单位毫秒,但不可以直接使用,要捕获异常才可以。

class  MyThread extends  Thread{
    @Override
    public void run() {
        for(int i=0;i<1000;i++){
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("hello world");
        }
        
        
    }
}

小细节,在run方法内部只能try-catch没有办法throws,因为此处是方法重写,在父类的run来说,没有throws 异常的设定。

在java中通过Thread类创建线程的方法还有很多中写法

创建一个类,继承Thread,重写run方法 是class

创建一个类,实现Runnable,重写run方法 是interface如下

public class demo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t=new Thread(myRunnable);
        t.start();
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("hello world");
        }
    }
}
class MyRunnable 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("hello world");
        }
    }
}

Runnable这里将要完成的工作放到Run able中,再让Runnable和Thread配合,把线程要执行的任务和线程本身,解耦合。

当用并发编程的方式来完成某个工作,使用Runnable描述这个工作的具体细节,

使用多线程的方式就可以使用RUnnable搭配线程来使用,使用线程池的方式就可以使用Runnable搭配线程池来使用,使用协程的方式,就可以使用Runnable搭配协程。

简化上述创建线程的方法

继承Thread,重写run方法,基于匿名内部类

实现Runnable ,重写run,基于匿名内部类

public class demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("hello world");
                }
            }
        };
    }
}

创建一个子类,子类继承Thread ,并且这个子类没有名字(匿名),类的创建在另一个类的里面,所以这就是匿名内部类。

在子类中重写了run方法

创建了该子类的实例,并且用t来进行指向。

public static void main(String[] args) {
        Thread t =new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("hello world");
                }
            }
        });
        t.start();
    }

创建一个Runnable的匿名内部类,重写run方法,再通过Thread的构造方法传给Thread。

当然了还可以用lambda表达式来写。

Thread t=new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    Thread.sleep(100);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("hello world");
            }
        });

->后面的内容就是run方法的内容,lambda表达式本质就是一个匿名函数,主要可以用来作为回调函数来使用

Thread的常见构造方法

Thread();

Thread(Runnabel target);用Runnable 创建线程对象

Thread (String name);创建线程对象并命名

Thread (Runnable target,String name);使用Runnable创建线程对象,并命名;

Thread(ThreadGroup group,Runnable target);创建线程并分组;

Thread的几个常见的属性

ID        getId();线程的唯一标识;

名称        getName();调式时会用到;

状态        getState();表示当前所处状态;

优先级        getPriority();会有一个数字表示优先级的高低,但作用不是很大;

是否后台        isDaemon();后台线程也叫做守护线程,后台线程不影响进程的结束,但前台线程会会影响到进程的结束,如果前台线程没有执行完,进程是不会结束的。

演示,将前台线程设置为后台线程

public static void main(String[] args) {
        Thread t=new Thread(()->{
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("hello world");
            }
        });
        //设置成为后台线程
        t.setDaemon(true);
        t.start();
        try {
            Thread.sleep(4000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("hello world");
    }

时间一到四秒,就算后台线程还没有结束也会因为前台线程的结束而结束

是否存活        isAlive();Thread对象,对应的线程再系统的内核中是否存活;Thread对象的生命周期并不是和系统中的线程完全一致,一般都是先创建好Thread对象,然后再手动调用start,内核才真正创建出线程。消亡的顺序不固定,此时就需要isalive来查看是否存活

是否被中断        isInterrupted();

如何终止一个线程?

一个线程的run方法执行完毕,就算结束了。这里的终止线程,就是想办法,让run能够尽快的执行完毕,正常情况下,不会出现,run没有执行完,线程突然就没了的情况。 

1.手动设置一个标志位让run尽快结束。

static  boolean isQuit=false;
    public static void main(String[] args) throws  InterruptedException{
        Thread t=new Thread(()->{
            while(!isQuit){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("hello world");
            }
        });
        t.start();
        //主线程这里执行一些其他的逻辑,让t结束
        Thread.sleep(3000);
        isQuit=!isQuit;
        //把t终止
    }
/*
hello world
hello world
hello world

Process finished with exit code 0

*/

isQuit必须写作成员变量才可以被访问到,写作局部变量就会报错 。

变量捕获机制,lambda内部看起来是引用外部的变量,但实际是将外部的变量再lambo内部中复制了一份。

变量捕获这里有一个限制,必须是final或是不可变的 。如果这个变量想要修改就不能进行变量捕获,如果外面的对这个变量进行修改,就会出现一个情况,外面的变量变了,里面的没有变,代码更容易出现问题。

写作成员变量就不是触发变量捕获的逻辑了,而是内部类访问外部的类的成员,本身就是可行的2

2.直接使用Thread类,有提供现成的标志位,不用咋们手动去设置标志

public static void main(String[] args) throws  InterruptedException{
        Thread t=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                    break;
                }
                System.out.println("hello world");
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }
Thread.currentThread()是获取当前线程的对象,直接写成t也没问题(t必须已经被初始化过)。
isInterrupted(),对象内部提供的一个标志位,通过这个方法来判断,True应该要结束,false不用结束。

等待一个线程join()

多个线程是并发执行的,具体的执行过程,都是由操作系统负责调度的,操作系统的调度线程的过程,是随机的。

无法确定线程的主席那个的先后顺序,等待线程,就是一种规划,线程结束顺序的手段。

AB两个线程,希望B先结束,A后结束,那么就让A线程中调用B。join的方法,此时B线程还没有执行完,A线程就会进入阻塞状态,就相当于给B留下了执行的时间,B执行完了之后,A再从阻塞状态中恢复过来,并且继续往后执行。

如果A执行B。join时B已经执行结束了,那么A就不必进入阻塞。演示如下

public static void main(String[] args) {
        Thread B=new Thread(()->{
           for(int i=0;i<5;i++){
               System.out.println("BBBB");
               try {
                   Thread.sleep(500);
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
           }
            System.out.println("B结束了");
        });
        Thread A=new Thread(()->{
            for(int i=0;i<3;i++){
                System.out.println("AAAA");
                try {
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            try {
                B.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println("A结束了");
        });
        B.start();
        A.start();
    }


BBBB
AAAA
BBBB
AAAA
AAAA
BBBB
BBBB
BBBB
B结束了
A结束了

Process finished with exit code 0

阻塞让代码暂时不继续执行了,该线程暂时不去cpu上参与调度。

sleep也能让线程阻塞,但有时间限制,超过一定时间就不等了

注意sleep(0)是一个特殊的技巧,让当前的线程放弃cpu让它准备下一轮的调度

join的阻塞是死等。不见不散

join的两种方法

join();

join(long millis);最大的等待时间,更加推荐,给我们的程序留有余地

join还有wait的等待

也可以被Interrupt方法唤醒

线程的状态

NEW :Thread对象创建好了但是还没有调用

RUNNABLE:就绪状态

BLOCKED:因为锁产生阻塞

WAITING:因为调用wait 产生阻塞

TIMED-WAITING:因为sleep产生阻塞

TERMINATED:工作完成了

注意join如果带时间就是TIMED-WAITING,不带时间就是WAITING

线程安全

观察下面代码

public static void main(String[] args)throws  InterruptedException {
        Counter counter  = new Counter();
        Thread t1=new Thread(()->{
           for(int i=0;i<10000;i++){
               counter.increase();
           }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<10000;i++){
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
class  Counter{
    public int count=0;
    public void increase(){
        count++;
    }
}

//13848

理论上来说,因该输出20000才对,但实际达不到,并且每次的答案都不相同

出现了线程安全的问题。

count++的操作本质是三个步骤,先将内存中的数据加载到cp的寄存器中(load),把寄存器中的数据进行+1(add),把寄存器中的数据写回到内存中(save)。由于两个线程互相调用同一个东西,这个三个步骤执行的顺序会不同,会互相插入,就会出现没有加上的情况

虽然在一个cpu核心上,寄存器就这么一组,但是两个线程,可以视为是各自有各自的一组寄存器,本质上是”分时复用“的

根本原因多个线程之间的调度是随机的,操作系统使用抢占式的操作来安排调度 。
多个线程同时修改同一个变量,容易产生线程安全问题,其他的穷狂都没事。但可以通过调整代码结构来规避这种情况
进行的修改不是原子的 

如果修改操作,能够按照原子的放过是来完成,此时也不会有线程安全问题

count++不是原子的

直接赋值是原子

判定加赋值也不是原子

配合枷锁可以将一组操作打包成一个原子的操作

内存可见性引起的线程安全的问题
指令重排序引起的问题 

所以上述问题可以使用加锁来解决,可以给方法枷锁。这样这个方法就成了一个整体,当t1线程将锁释放后,t2才能从阻塞中醒来。

synchronized public void increase(){
        count++;
    }

Synchronized的使用方式

java给我们提供的枷锁方式(关键字);

搭配代码块来完成的

进入代码块就枷锁,出了代码块就解锁

加锁的目的是为了互斥使用资源(互斥的修改变量)

如果两个线程对同一个对象枷锁,就会出现锁竞争/冲突 ,一个线程枷锁成功,一个线程枷锁阻塞

如果针对不同的对象,就不会阻塞等待的操作

内存可见性引起的问题

private  static int isQuit=0;
    public static void main(String[] args)throws  InterruptedException {
        Thread t1=new Thread(()->{
            while(isQuit==0){
                ;
            }
            System.out.println("t1结束了");
        });
        Thread t2=new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入isquit的值");
            isQuit=sc.nextInt();
        });
        t1.start();
        t2.start();
    }

理论上输入非0数字,线程t1就会停止,但实际上无论输入什么t1线程都会处于循环当中

程序在编译运行的时候,java编译器和jvm可能会对代码做出一些优化,保持原有的逻辑不变的情况下,提高代码的执行效率

编译器优化,本质是靠代码,智能的对代码进行调整,大部分都ok,但还是有机会会出错。如上代码。

t1中isquit()本质是两个操作,第一个是load,第二个是jcmp比较并跳转

但实际上,只有第一次load,后面的都被优化掉了,但没想到isquit的值在另一个线程中被修改了。

但可以用另外的方式来弥补这个

比如让线程休眠一下,重启时就会重新读入。

Thread t1=new Thread(()->{
            while(isQuit==0){
                try {
                    Thread.sleep(0);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("t1结束了");
        });

volatite

用它来修饰一个变量,编译器就知道这个变量是易变的,编译器就会禁止上述的优化,就可以保证t1在循环中始终可以读取。

private volatile static int isQuit=0;

本质是保证变量的内存的可见性,

wait和notify

这两个也时多线程编程中的重要工具,

这个个主要用于协调线程顺

前者时等待,后者时通知唤醒,二者搭配使用

wati在执行时会做三件事

1解锁

2阻塞等待

3当被其他线程唤醒之后,就会尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑

wait要解锁的前提时先能加上锁

public class demo14 {
    public static void main(String[] args)throws  InterruptedException {
        Object object = new Object();
        synchronized (object){
            object.wait();
        }
    }
}

这样才可以正常跑起来。

几个注意事项

要想让notify能够顺利唤醒wait,就需要保证wait和notify都是使用童一个对象调用的

wait和notify都需要放到synchronized之内的。虽然notify不涉及解锁操作,但是java也强制要求,notify要放到synchroinized中

如果进行notify的时候,另一个线程并没有处理wait状态,此时,notify相当于大空了,但没有任何副作用

private  static  Object object=new Object();
    public static void main(String[] args)throws  InterruptedException {
        Thread t1=new Thread(()->{
            while(true){
                synchronized (object){
                    System.out.println("t1kaishi");
                    try {
                        object.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("t1jieshu");
                }
            }
        });
        t1.start();
        Thread t2=new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (object){
                    System.out.println("t2notify kaishi");
                    object.notify();
                    System.out.println("t2notify jieshu");
                }
            }
        });
        t2.start();
    }




t1kaishi
t2notify kaishi
t2notify jieshu
t1jieshu
t1kaishi

当有多个线程时, notify只会幻想一个线程,具体是哪一个不知道吗,完全随机。

notifyAll唤醒全部

如果想唤醒指定的线程,那么就多设置几个对象就可以了

wait和sleep区别

sleep是有一个明确的时间的,到达时间,自然就被唤醒了,也能提前幻想,使用Interrupt就可以

wait默认是一个死等,一直等到有其他线程notify,wait也能够被Interrupt提前唤醒。

前一个唤醒后还可以接着工作,但后一个唤醒就是告知线程要结束了。

wait也有一个带时间的版本,和join类似。

因此优先使用waitnotify

到这里多线程的基础就结束了,主要介绍多线程的一些基本的概念,以及Thread的基本使用方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值