多线程初阶-线程与进程的关系及线程的基本使用方法-Java

一、什么是线程

上篇引入了进程这个概念,最主要的目的是为了解决“并发编程”这样的问题,确实,多进程编程已经可以解决并发编程的问题了,已经可以利用起来cpu多核资源了。但是,进程太重了(这里的重,主要体现在“资源分配/回收”上),创建一个进程,销毁一个进程,调度一个进程,开销都比较大。此时,线程应运而生,线程也叫作“轻量级进程”。 在解决并发编程问题的前提下,想让创建、销毁。调度的速度更快一些,线程就起到了至关重要的作用

线程为什么更“轻量”? 是因为把多次申请资源/释放资源的操作给省下了。

二、线程和进程之间的区别与联系

一个进程中可以有一个或多个线程,同一个进程里的多个线程之间共用乐进程的同一份资源(主要指内存和文件描述符表)

# 内存: 比如线程1 new的对象,在线程2、3、4里都可以直接使用

# 文件描述符表: 比如线程1打开的文件,在线程在线程2、3、4里都可以直接使用

线程和进程之间到底是什么关系呢?这里给大家举个例子,就可以很好的明白了

假如你是开工厂的,起初规模比较小,你的工厂里只有一条生产线,此时原材料运到你的工厂,你加工好之后运走,一切顺利运行。如下图

某一天,为了把生产力扩大,有两种方案:

第一种方案:此时你在工厂旁边买了一块地,又建了一个工厂,开了一条生产线,重新搞物流体系。这就相当于多进程版本的方案,如下图

第二种方案:在工厂中再搞一套生产线,场地和物流体系都可以复用之前的,只是搞第一套生产线的时候,需要把资源申请到尾,后续再加新的生产线,复用之前的资源即可

这就是多线程版本的方案,如下图

这就说明了引入线程这个概念的必要性。操作系统实际调度的时候就是以线程为单位进行调度的

三、线程创建的五种写法

  1. 继承Thread,重写run方法

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

  1. 实现Runnable接口,重写run方法

#  Runnable作用,是描述一个“要执行的任务,“  run方法就是任务的执行细节
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello world");
    }
}

  1. 使用匿名内部类,继承Thread

public static void main(String[] args) {
        Thread t=new Thread(){   //匿名内部类: 创建了一个Thread的子类,(该子类没有名字,所以叫“匿名”)
            @Override
            public void run() {
                System.out.println("hello");;
            }
        };
    }

创建了一个Thread的子类 (这个子类没有名字),所以叫“匿名”

创建了子类的实例,并且让t引用指向该实例

  1. 使用匿名内部类,实现Runnable

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        
    }

这个写法和第2中是同一作用

  1. 使用Lambda表达式 最简单,推荐写法~

public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println("hello thread");
        });
    }

把线程要做的任务用lambda表达式来描述,直接把lambda传给Thread的构造方法

四、Thread类中 run和start的区别

Start是真正创建了一个线程(从系统这里创建的),并且让新线程调用run。线程是独立地执行流。

Run只是描述了线程要干的活是啥。如果直接在main中调用run,此时没有创建想新线程,全是main线程一个线程在干活

五、总结Thread类的基本用法

  1. Thread的常见构造方法:

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用Runnable对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target,String name)

使用Runnable对象创建线程对象,并命名

  1. Thread的几个常见属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

  • ID 是线程的唯一标识,不同线程不会重复

  • 名称是各种调试工具用到

  • 状态表示线程当前所处的一个情况,下面我们会进一步说明

  • 优先级高的线程理论上来说更容易被调度到

  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。

  • 是否存活,即简单的理解,为 run 方法是否运行结束了

  • 线程的中断问题,下面我们进一步说明

  1. -start() 启动一个线程

调用start方法,才真正的在操作系统的底层创建出一个线程。

  1. 中断一个线程- 调用interrupt()方法来通知

public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        thread.interrupt();
    }

Thread.currentThread() ->获取当前线程对象

is Interrupted() ->判断当前线程是否存活

在main线程中调用thread.interrupt(),可通知线程thread中断。

注意:此时只是通知线程thread该中断了, 但此线程是否真正中断具体情况具体分析.

thread 收到通知的方式有两种:

1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以InterruptedException 异常的形式通知,清除中断标志当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.

2. 否则,只是内部的一个中断标志被设置,thread 可以通过

  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志

  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

  1. 等待一个进程-join()

public static void main1(String[] args) {
        Thread thread=new Thread(()->{
            for (int i = 0; i <3 ; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        try {
            thread.join();    //让main线程等待thread线程结束,(等待t的run执行完)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

大家可以试试人如果把join注释掉,会是什么现象?

附录:

方法

说明

public void join()

等待线程结束

public void join(long millis)

等待线程结束,最多等millis毫秒

public void join(long millis,int nanos)

同理,但可更高精度

  1. 获取当前线程的引用-

这个方法前面提到过。

方法

说明

public static Thread currentThread();

返回当前线程对象的引用

  1. 休眠当前线程-sleep()

也是比较熟悉的一组方法,切记,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

方法

说明

public static void sleep(long millis)throws InterruptedException

休眠当前线程millis毫秒

public static void sleep(long millis,int nanos)throws InterruptedException

可以更高精度的休眠

六、总结 Java 线程的几种状态

  1. NEW 创建了Thread对象,但是还没调用start(此时内核里还没创建对应的PCB)

  1. TERMINATED 表示内核中的PCB已经执行完毕了,但是Thread对象还在

  1. RUNNABLE 可运行的

  1. 正在CPU上执行的

  1. 在就绪队列里,随时可以去CPU上执行的

  1. WAITING

  1. TIMED_WAITING

  1. BLOCKED

状态之间的切换条件如图:

七、线程安全问题的原因和解决方案(重点)

多线程带来的风险-线程安全问题,造成线程不安全有以下几种原因:

  1. 根本原因:线程的抢占式执行,随机调度。此原因我们没办法解决,无能为力。

  1. 修改共享数据

当多个线程针对一个变量进行修改时,往往得到的结果与我们预期不同。

代码1:

class Counter1{
    public int count=0;

    public void add(){    
        count++;
    }
}
public static void main(String[] args) {   //两个线程修改共享资源 : 线程不安全
        Counter1 counter=new Counter1();

        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.add();
            }
        });

        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("count="+counter.count);    //count 不等于100000,  这就是线程不安全
    }

运行此代码,预期结果count=100000,但实际结果并不等于100000

  1. 原子性

其实对一个变量进行修改由三步操作组成的:

1)从内存把数据读到CPU

2)进行数据更新

3)把数据歇会CPU

如果不保证原子性会给多线程带来什么问题?

如果一个线程正在对一个变量进行操作,中途其他线程插入进来了,如果这个 操作被打断了,结果就可能是错误的

  1. 内存可见性问题

如果一个线程读,一个线程改,也可能出问题

代码2:

import java.util.Scanner;

class MyCounter{
    public int flag=0;
}
public static void main(String[] args) {
        MyCounter myCounter=new MyCounter();

        Thread t1=new Thread(()->{
            while (myCounter.flag==0){
                //循环体空着
            }
            System.out.println("t1循环结束");
        });

        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("输入一个整数:");
            myCounter.flag=scanner.nextInt();
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

预期:t2把flag改为非0的值之后,t1随之就结束 实际: t1未结束 flag变量加上volatile后,t1可以随之结束,

内存可见性问题,其他一些资料谈到JMM(Java Memory Model)java内存模型,从JMM的角度重新表述内存可见性问题

java程序里,主内存,每个线程还有自己的工作内存,t1线程进行读取的时候,只是读取了工作内存的值,t2线程进行修改的时候,先修改的自己的工作内存的值,然后再把工作内存的内容同步到主内存中。,但是由于编译器优化,导致t1没有重新的从主内存同步数据到工作内存,读取的结果就是t2“修改之前”的结果。

  1. 指令重排序

此原因本质上是编译器优化出bug了, 编译器觉得你写的代码太lj了,就自作主张的把你的代码调整了。

解决方案

1.使用synchronized关键字-监视器锁

(1)修饰方法

1) 修饰普通方法 锁对象就是this

2) 修饰静态方法 所对象是类对象(Counter.class)

(2)修饰代码块 显示/手动指定锁对象

修改代码1,给类Counter1中的add方法加上synchronized锁

class Counter1{
    public int count=0;

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

2.加volatile关键字

修改代码2,给flag加volatile关键字

class MyCounter{
    volatile public int flag=0;
}

八、synchronized和volatile 关键字的作用

1.synchronize的作用

(1)互斥作用

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

进入synchronized 修饰的代码块, 相当于 加锁

退出synchronized 修饰的代码块, 相当于 解锁

(2)刷新内存

synchronized的工作过程:

1)获得互斥锁

2)从主内存拷贝变量的最新副本到工作的内存

3)执行代码

4)将更改后的共享变量的值刷新到主内存

5)释放互斥锁

所以synchronized也能保证内存可见性。 具体参见后面volatile部分。

(3)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

2.volatile的作用

能保证内存可见性 , 那么什么是内存可见性呢?

一个线程针对一个变量进行读取从啊哦做,同时另一个线程针对这么变量进行修改,那么此时读到的值,不一定是修改之后的值~~ 这个读线程没有感知到变量的变化。比如以下代码:

import java.util.Scanner;

class MyCounter{
    public int flag=0;
}
public static void main(String[] args) {
        MyCounter myCounter=new MyCounter();

        Thread t1=new Thread(()->{
            while (myCounter.flag==0){
                //循环体空着
            }
            System.out.println("t1循环结束");
        });

        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("输入一个整数:");
            myCounter.flag=scanner.nextInt();
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

这个代码 预期:t2把flag改为非0的值之后,t1随之就结束

实际: t1未结束

给flag变量加上volatile后,t1可以随之结束,

class MyCounter{
    volatile public int flag=0;
}

给flag加上volatile关键字,意思是告诉 编译器,这个变量是“易变”的,你一定要每次都重新读取这个变量的内存内容,它指不定啥时候就变了,不敢再进行激进的优化了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值