java-多线程(1)

目录

一,进程和线程(​编辑)

二, 在java代码上编写多线程程序

第一种写法:

第二种写法:

第三种写法:

第四种写法:

第五种写法:

三,Tread类

1,Tread的方法和属性​编辑

2,线程的启动和终止

启动:

终止:

1)使⽤⾃定义的变量来作为标志位.

2)使⽤ Thread.interrupted() 代替⾃定义标志位.

3,等待

4,获取当前线程引⽤

5,休眠当前线程

四,线程的状态

1,NEW

2,TERMINALED

3,RRUNNABLE

4,BLOCKED

5,TIMED_WAITING

6,WAITING

五,线程安全(​编辑)

1,认识线程安全问题

2,线程安全问题的解决方案

线程安全的原因:

(1)线程在操作系统中,随机调度,抢占式执行[根本原因]

(2)多个线程修改同一个变量

(3)修改操作不是"原子性"的

(4)内存可见性问题

关键字:synchronized(){...}

死锁(​编辑):

1)可重入锁

2)两个线程,两把锁

3)N个线程,M个锁

解决:

1)避免锁嵌套

2)约定加锁顺序


一,进程和线程()

通过写特殊的代码,把多个CPU的核心利用起来,这样的代码就称为"并发编程",为实现"并发编程"使用多进程编程,最大的问题就是进程过"重",由于创建/销毁进程开销较大(时间,空间),一旦需求场景需要频繁创建销毁进程,开销就过于明显了(比如:服务器的开发,针对每个发送请求的客户端都要创建一个单独的进程,就会构成开销过大)

为了解决进程开销过大的问题,发明了"线程",线程可以理解为更轻便的进程,也能解决并发编程的问题,但是创建/销毁开销更小

所谓进程,在系统中,是通过PCB这样的结构体,然后用链表连接起来,对线程而言,同样也是通过PCB来描述的

一个进程,其实是一组PCB,一个线程是一个PCB.他们之间存在了包含关系,一个进程中包含了多个线程,此时每个线程都可以独立到CPU上调度执行

线程是系统"调度执行"的基本单位

进程是系统"资源配置"的基本单位

一个可执行程序,运行时(双击),操作系统会创建一个进程,给这个程序分配各种系统资源(CPU,硬盘,内存,网络宽带.....)同时也会在这个进程中创建一个或多个线程,这些线程再去CPU上调度执行

如果多个线程在一个进程中,那么每个线程又会有状态,优先级,上下文,记账信息,每个线程都独立在CPU上调度执行

一个进程有一个或多个线程,但是不能没有线程,同一个进程的多个线程,公用的同一份系统资源

线程比进程更轻量,主要就在创建线程于省去了"分配资源"的过程,销毁线程的过程,省去了"释放资源"的过程

一旦创建一个进程,同时也会创建一个线程,此时这个线程就会负责资源分配

一旦后序创建第二个,第三个线程,就不必在从新分配资源了

线程数量并非越多越好:

1)比如,超出了CPU核心的数目,此时,就无法在微观上完成所有的线程的"并行"执行,势必会存在严重的"竞争"关系.即使进程是数量增加,但是CPU还是原本的那些核心,此时,多进程里面的线程之间还是会存在竞争相同数目的CPU核心资源的关系

2)如果多个线程针对同一变量进行读写操作,就容易发生冲突,一旦发生冲突就会使程序产生bug(线程安全问题)

3)一个进程中,有多个线程时,一旦某个线程抛出异常,如果处理不好,就会使整个进程崩溃,因此,其他线程也会随之崩溃 (但是多个进程之间不会相互影响,一个进程崩溃了,也不会影响其他进程,这一点也称之为"进程的隔离性")

二, 在java代码上编写多线程程序

线程本身是操作系统提供的概念,操作系统提供API,供程序员使用.不同系统所提供的API是不同的.java(JVM)把这些系统的API封装好了,所以java程序员不需要过多的关注系统原生API,只需要了解java所提供的这套API就可以了.

java提供的Thread标准库,这个类就是负责完成多线程相关的开发

创建线程的写法:

1)继承Tread,重写run

2)实现Runnale,重写run

3)本质上与1一样,继承Tread,重写run,通过匿名内部类来实现

4)本质上与2一样,通过匿名内部类来实现

5)基于lambda表达式来创建线程

我可以运用以下方式来进行查看线程的详细信息:(程序需要处于运行状态)

第一种写法:

class MyThread extends Thread{
    @Override
    /*继承不是目的,为的是重写run
    像run这样的函数,用户手动定义了,但是没有手动调用,最终这个方法被系统/库/框架进行调用了
    此时这样的方法,就称为"回调函数*/
    public void run(){
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
public class Demo1 {
    //调用一个main方法的线程称为"主线程",一个进程至少一个线程,这个至少一个的线程就是主线程
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        //创建线程,这个线程和主线程并发/并行在CPU上执行
        myThread.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
        //调用start就会在进程内部,创建出一个新的线程,新的线程会执行run里面的代码
    }
}

重新定义一个类,让这个类继承Thread,目的是重写run方法,而run方法里面的逻辑是创造出的线程会执行的逻辑.

我们可以从结果上来看,打印的结果是hello thread和hello main交替出现的,说明多个线程之间谁先去CPU上执行调度,这个过程是不确定的,这个调度取决于操作系统,内核里的"调度器"实现的,调度器里有一套规则,但作为应用程序开发,没法进行干预,也感受不到

第二种写法:

class MyRunnable implements Runnable{
//Runnable就是用来描述"要执行的任务是什么".通过Tread来创建线程,
// 线程要执行的任务是通过Runnable来描述的,而不是通过Tread自己来描述的
    @Override
    public void run() {
        while (true){
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable=new MyRunnable();
        Thread thread=new Thread(myRunnable);
        thread.start();
        while (true){
            System.out.println("hello main");
                Thread.sleep(1000);
        }
    }
}

如果说第一种方法是重写Thread自己的run方法来描述线程需要执行的任务,那么这种写法就是通过runnable来描述线程的任务.同样,创建一个类实现runnable接口,重写run方法来描述线程的任务.然后用start来创建这个线程

第三种写法:

public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(){
        @Override
        public void run(){
            while (true){
                System.out.println("匿名内部类");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    };
    thread.start();
    while (true){
        System.out.println("hello main");
        Thread.sleep(1000);
    }

    /*
    1,定义匿名内部类,这个类是Tread的子类
    2,类的内部,重写了一个run方法
    3,创建了一个子类的实例,并且把实例的引用赋值给了tread
    这种写法通常是"一次性的",内聚性好一些
     */
}

本质上就是通过写匿名内部类的方法来代替重新定义一个类的方法

第四种写法:

Thread thread=new Thread(new Runnable() {
    @Override
    public void run() {
        while (true){
            System.out.println("hello Tread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
});
thread.start();
while (true){
    System.out.println("hello main");
    Thread.sleep(1000);
}

第五种写法:

public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
        while (true){
            System.out.println("hello Tread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
    thread.start();
    while (true){
        System.out.println("hello main");
        Thread.sleep(1000);
    }
}

lambda表达式来创建线程,其实类似于将写匿名内部类的写法进一步简化了

三,Tread类

1,Tread的方法和属性

TreadGroup线程组(把多个线程放到一个组里,方便统一设置线程的属性),但是现在很少使用线程组,线程的相关属性也用的不多,现在用的更多的是线程池

ID,JVM自动分配,不能手动设置,Tread对象的身份标识.通常情况下,一个Tread对象就对应系统内部一个线程,但是,也会存在Tread对象存在,但是线程已经没了/还没建立的情况.

优先级:设置不同的优先级,会影响到系统的调度,这里的"影响"是基于统计的影响,直接用肉眼观察,很难观察到效果

后台线程:如果一个线程在执行过程中,不能够阻止进程的结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就被称为"后台线程"

前台线程:如果一个线程在执行过程中,能够阻止进程的结束,这个线程就是"前台线程"

一个进程中,前台线程可以有多个(创建的线程默认是前台线程),必须所有的前台线程都结束,进程才能结束

是否存活:代码中,创建new Tread对象的生命周期和内核中实际的线程是不一样的,可能会出现Tread对象存在,但是内核的线程已经不存在的的情况

1,调用start前,对象存在,但是线程不存在.

2线程run执行完毕后,内核的线程已经不存在,但是Tread对象仍存在

注意:如果线程之间,调度的顺序是不确定的,如果两个线程都是sleep(3000)那么,两个线程谁先执行,谁后执行,顺序不一定(不一定不代表,双方概率相等,代码运行环境不同肯能会存在差异)

2,线程的启动和终止

启动:

start/run的区别:

run:描述了线程要执行的任务,也可以称为"线程的入口"

start本质是调用系统函数的API,系统的API会在系统内核里创建线程(建立PCB,加入来链表)

start的执行速度一般都是比较快的(创建线程,比较轻量)一旦start执行完毕,新线程就会开始执行,调用start不一定是主线程调用,任何线程都可以创建其他的线程

一个thread对象只能调用一次start. 线程的状态,由于java中,希望一个Tread对象只对应一个到系统中的线程,因此就会在start中,根据线程状态做出判断,如果Tread对象,是没有start,那么此时的状态是NEW,接下来就可以顺利调用start,如果已经调用过了start,就会进入到其他状态,只要不是NEW状态,接下来执行start就会抛出异常

终止:

如果A要将B线程终止,那么核心就是A要想办法让B线程方法执行完毕,此时,B就自然而然结束了,而不是B执行到一半,A就将B直接强制结束了

1)使⽤⾃定义的变量来作为标志位.

为什么isQuik,不能放到main()中???

原因:

变量捕获:是lamdba表达式/匿名内部类的一个语法规则

isQuit和lamdba定义在一个作用域中,此时,lamdba内部,是可以访问到外部(和lamdba处于同一作用域)的变量.java中变量捕获,是有特殊要求的,要求捕获的变量是final或者事实final(变量是final修饰或者不进行修改)

而写到外部,写成成员变量就可以了,是因为,此时走的语法是"内部类访问外部类成员变量",就与"变量捕获"无关了(lamdba本身就是一个"函数式接口"产生的"匿名内部类")

2)使⽤ Thread.interrupted() 代替⾃定义标志位.

(1)currentThread():是Tread的静态方法,通过这个方法就可以获取到调用这个方法线程的实例.哪

个线程调用,就返回的引用就指向哪个线程的实例

(2)isInterrupted():判断当前对象的中断标志位是否设置

(3)interrupt();可以设置标志位.

原因:

整个循环花费时间都处于sleep上,main调用interrupt的时候,大概率thread正处于睡眠中.此处interrupt不仅可以设置标志位,还可以把刚刚sleep的操作给唤醒.唤醒后,抛出了interruptedException异常,然后被catch捕获后执行catch中的语句,再次抛出异常RuntimeException,而这个异常没人catch,最终就到了JVM这一层,进程就直接异常终止了

更改为以下代码:

这时interrupt把sleep唤醒了,触发了异常,被catch捕获到了,虽然被catch住了但是循环还是继续进行.

原因:

sleep等阻塞被唤醒之后,就会清空刚刚设置的interrupted标志位.所以想要结束循环就要在catch中增加break或者return语句

更改为以下代码:

一个线程A想要使线程B终止,B收到这样的请求,会自行决定是否终止.如果想要忽略就可以利用sleep消除标志位的特性让代码继续执行.如果想要立刻终止,就可以在catch中加入return/break.如果想要等等,做一些收尾工作再结束,就可以在catch中写入其他逻辑,这些逻辑完成之后,在进行return

sleep消除标志位的逻辑是JVM的内部逻辑,需要结合JVM的源码才能看到这样的操作

3,等待

在A中调用,B.join,意思为,让A线程等待B线程结束,A线程再继续.任何线程之间,都是可以相互等待的,不是说必须是main线程等待别人,等待线程也不一定是两个线程之间的,一个线程,可以同时等待多别的线程,若是若干线程之间也能相互等待

实际开发中,类似的"等待机制",都会设置"超时时间"(最多等多久,如果还没结束,就不等了)

public static void main(String[] args) throws InterruptedException {
    Thread thread1=new Thread(()->{
        for (int i = 0; i < 3; i++) {
            System.out.println("hello thread1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("线程Thread1结束");
    });
    Thread thread2=new Thread(()->{
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < 4; i++) {
            System.out.println("hello thread2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("线程Thread2结束");
    });
    thread1.start();
    thread2.start();
    System.out.println("main开始等待");
    thread1.join();
    thread2.join();
    System.out.println("main等待结束");
}

4,获取当前线程引⽤

Thread.currentThread()

5,休眠当前线程

Thread.sleep(1000);

线程执行sleep的时候,会使这个线程不参与CPU调度,从而把CPU资源让出来,给别人使用.也把sleep这样的操作称之为"放权",放弃CPU的使用权利.有的场景中,发现某个CPU占中率过高,就可以通过sleep来进行改善

四,线程的状态

六种状态:

1,NEW

当前对象有了,但是内核的线程还没有(还没调用过start)

2,TERMINALED

当前对象虽然还在,但是内核的线程已经销毁了(线程已经结束了)

3,RRUNNABLE

就绪状态,正在CPU上运行或者随时可以去CPU上运行

4,BLOCKED

因为锁竞争引起的堵塞

5,TIMED_WAITING

有超时时间的等待

6,WAITING

没有超时时间的等待

我们也可以用jconsole来看,我们可以通过线程状态来确定堵塞原因

五,线程安全()

1,认识线程安全问题

private static int count;
public static void main(String[] args) throws InterruptedException {
    Thread thread1=new Thread(()->{
        for (int i = 0; i < 50000; i++) {
            count++;
        }
    });
    Thread thread2=new Thread(()->{
        for (int i = 0; i < 50000; i++) {
            count++;
        }
    });
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    System.out.println("count:"+count);
}

产生原因:

count++.分为三个指令(1)从内存上读取count.到寄存器中(2)寄存器中的count值加一(3)将寄存器的值写回到内存中. 在执行count++过程中,CPU可能会在执行完(1)后调度走/执行完(1)(2)后调度走/执行完(1)(2)(3)后调度走,前两种情况就会导致出现bug

线程调度是随机的,这是线程安全问题的罪魁祸⾸.随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数.这就导致最终结果是一个不确定的值,而这个值一定小于10w

对于CPU而言:一条指令是CPU上不可分割的最小单位,CPU在执行调度切换线程的时候,势必会执行完一条完整的指令

2,线程安全问题的解决方案

线程安全的原因:

(1)线程在操作系统中,随机调度,抢占式执行[根本原因]
(2)多个线程修改同一个变量
(3)修改操作不是"原子性"的
(4)内存可见性问题

就(1)而言:无法干预,这时操作系统内核负责的工作,作为应用层的程序员无法干涉

就(2)而言:取决于需求,有些场景不可这样改

就(3)而言:加锁,

关键字:synchronized(){...}

()里面不是参数,而是需要锁的对象.{...}里面的对象,是需要打包到一起的操作.此处synchronized是JVM提供的功能,synchronized底层就是JVM通过c++来实现的,进一步,也是依靠操作系统所提供的API实现加锁的,而操作系统的API则是来自于CPU上支持的特殊指令来实现的,因此不仅仅是java可以加锁,其他语言也可以加锁操作

synchronized:进入代码块就会进行加锁,出来就会进行解锁.

由于Thread1和Thread2都是对同一对象进行加锁,Thread1先锁的,Thread1就加锁成功了,于是Thread1就继续执行里面的代码,Thread2后锁的,发现对象已经被被人先锁了,于是Thread2 只能排队等.因此这两者之间就不会进行穿插了,也不会进行彼此之间的覆盖了

锁对象的作用,就是用来区分两个线程是否针对"同一个对象"加锁.是针对同一个对象加锁,此时就会出现"阻塞"(锁竞争/所冲突)不是针对同一个对象加锁,此时不会出现"阻塞",两个线程任然是随机调度的并发执行

锁对象,只要是Object(或者是其子类)都是可以的,不可以用内置类型int,double....

加锁后的代码,是要比join的串行效率要高,加锁只是将代码中的一小部分进行串行,其他部分仍然是并发执行的

也可以写成类对象(编写java代码,本身就是java文件,通过javac编译成.class文件,JVM运行的时候,把.class文件加载到内存中,形成了对应的类对象)

这个对象与其他任意对象没有区别,换句话说,写成了类对象,本质上是一种"偷懒的做法"

synchronized修饰方法:

class Counter{
    public int count;
    public void add(){
        synchronized (this){
            count++;
        }
    }

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

两者等价

所谓线程安全的类vector/stringbuffer/hashtable/stack,都是因为类里面含有 synchronized,但是并非写了synchronized线程就一定安全,换言之,到底是否需要加synchronized都是需要看具体场景的.向上述这种"无脑加锁"是不可取的.因此以上这些类都不推荐使用

等价于

static方法,没有this的,static方法,也叫做"类方法"和具体的实例无关,只是和类相关,所以这就相当于针对对应的类对象加锁

锁对象,锁的对象是什么不重要,重要的是锁的对象是否是同一个对象,如果是同一个对象,那么就会产生阻塞,不是同一个,就不会产生阻塞

死锁():

1)可重入锁

以上情况就会出现死锁状态,首先synchronized (counter)会进行上锁,然后进入,执行add时,又有一个锁,而这个锁在(synchronized (counter))时,已经上锁了,想要执行add就需要解锁,但是想要解锁就需要执行完synchronized (counter)的代码块,至此就锁死了.

但是可以正确运行,这是因为java为了减少程序员写出死锁的概率,引入的特殊机制,解决上述死锁问题"可重入锁".相同的代码如果换成c++/python就会死锁

可重入锁,在锁中,会额外记录以下当前是那个线路,如果再次加锁,发现加锁的线程就是当前锁持有的线程,则并不会进行任何加锁操作,也不会进行任何阻塞操作,而是直接放行,往下执行代码

如果有多层上锁,针对这种情况,可以引入一个计数器,初始情况下是0,当遇到一个{则+1,遇到}则-1,当count=0时,则解锁,如果不是不释放锁

2)两个线程,两把锁

1)线程1 现针对A加锁,线程2 针对B加锁

2)线程1 不释放锁A的情况下,在针对B加锁.同时,线程2 不释放B的情况下,在针对A加锁

private static Object locker1=new Object();
private static Object locker2=new Object();

public static void main(String[] args) {
    Thread thread1=new Thread(()->{
        synchronized (locker1){
            System.out.println("t1加锁locker1完成");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2){
                System.out.println("t1加锁locker2完成");
            }
        }

    });
    Thread thread2=new Thread(()->{
        synchronized (locker2){
        System.out.println("t2加锁locker2完成");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker1){
                System.out.println("t2加锁locker1完成");
            }
        }

    });
    thread1.start();
    thread2.start();
}

3)N个线程,M个锁

死锁的4个必要条件:

(1)锁是互斥的[锁的基本特性]

(2)锁是不可被抢占的[锁的基本特性](线程1拿了锁A,如果线程1不释放锁A,那么线程2是不能拿锁A)

(3)请求保持[代码结构](线程1,拿到A之后,保持A锁的前提下,请求B锁)

(4)循环等待/环路等待/循环依赖[代码结构],多个线程取锁过程中出现循环的等待

解决:

1)避免锁嵌套

2)约定加锁顺序

最常⽤的⼀种死锁阻⽌技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进⾏编 号 (1, 2, 3...M). N 个线程尝试获取锁的时候, 都按照固定的按编号由⼩到⼤顺序来获取锁. 这样就可以避免环路等待.

  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值