多线程可见

进程和线程

进程和线程的关系

  1. 进程包含线程,一个进程可以有一个线程(主线程),也可以有多个线程,每个线程拥有一份PCB对象
  2. 进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位.
  3. 每个进程内拥有自己独立的虚拟地址空间和独立的文件描述符表,而一个进程里的所有线程公用进程里的资源,所以同一个进程里的线程会互相影响.

为什么会出现线程

首先 并发编程(1.单核cpu发展遇到瓶颈,为了提高算力,需要多核cpu,就用到了并发编程 2. 有些任务需要"等待IO",在这期间,可以利用并发编程去做其他的事情) 成为刚需, 其次 虽然进程也能实现并发编程,但是线程比进程更加轻量:

  1. 创建线程比创建进程更快
  2. 销毁线程比销毁进程更快
  3. 调度线程比调度进程更快
  4. 使用多线程就是没有了操作系统为进程分配资源的时间

java线程和操作系统线程的关系

操作系统内部实现了线程这样的机制,并对用户层提供了API进行使用,而java的Thread类可以认为就是对操作系统的API进行了进一步的抽象和封装

第一个多线程代码

通过自己的MyThread继承Thread类

class MyThread extends Thread{
    //重写run 明确新创建的线程干啥活
    @Override
    public void run() {
        while(true){
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main");
        Thread thread = new MyThread();
        thread.start();//真正开始创建线程(在操作系统内核中,创造出对应的PCB,PCB加入系统链表,参与调度)

    }
}

第一个并发编程代码

class MyThread extends Thread{
    //重写run 明确新创建的线程干啥活
    @Override
    public void run() {
        while(true){
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main");
        Thread thread = new MyThread();
        thread.start();//真正开始创建线程(在操作系统内核中,创造出对应的PCB,pcb加入系统链表,参与调度)

        while(true){
            System.out.println( "hello main");
            Thread.sleep(1000);
        }
    }
}

我们可以看到,进程由main主线程进入,然后main线程和thread交替进行.
(但是在阻塞一秒后,先唤醒main线程还是thread线程是不确定的,操作系统对线程的调度是随机性的)
在这里插入图片描述

创建线程的多种方法

1.继承Thread类

第一就是上文中创建类继承Thread类,并重写run方法

2.实现Runnable接口

第二种就是实现Runnable接口,并实现其中的run方法,
Runnable接口是个函数式接口,其中只有一个run方法

class MyRnnnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello myrunnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {

        //解耦合
        //任务内容 和 线程本身 分离开
        Runnable runnable = new MyRnnnable();//创建的runnable定义了任务
        Thread t = new Thread(runnable);//把任务交给thread
        t.start();//start创建具体线程

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

这样的优点是提高了解耦合度,重写的run方法只是定义了任务内容,然后通过Runnable把描述好的任务交给Thread实例,t.start()完成了线程的创建.

3.匿名内部类

匿名内部类可以说完成了三步: 1.继承 2. 方法的重写 3. 类的实例化

3.1 匿名继承Thread类

public static void main(String[] args) {
        
        //匿名内部类
        //继承  方法重写  实例化
        //只是准备好了    还是需要start创建   因为线程还是需要操作系统创建

        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
        };
        t.start();
    }

3.2 匿名Runnable接口

public static void main(String[] args) {
        //两步实现
        /*Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();*/

		// 一步实现
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

4. lambda表达式

public static void main(String[] args) {
        //lambda 表达式
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

通常选择 Runnable 来写更好一些,来完成更好的解耦.写代码注重 高内聚,低耦合.Runnable 单纯的只是描述了一个任务.

多线程节约时间

我们利用单线程和多线程来计算两个变量的自增,从0自增到20亿,并记录两个线程各自的时间来完成比较. 不过,此代码的时间戳在main线程中,为了完成多线程时间的记录,我们需要在main线程中调用t1.join和t2.join 来让main线程等待t1和t2线程执行完毕.

public class Demo {
    private static final long COUNT = 20_0000_0000;
    private static void serial(){
        //把方法执行时间记录
        //记录当前的毫秒时间
        long beg = System.currentTimeMillis();

        int a = 0;
        for(long i = 0; i < COUNT; i++){
            a++;
        }
        a = 0;

        for(long i = 0; i < COUNT; i++){
            a++;
        }

        long end = System.currentTimeMillis();
        System.out.println("单线程消耗时间: " + (end - beg) +" ms");

    }

    private static void concurrency(){
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
           int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() ->{
            int a = 0;
            for(long i = 0; i < COUNT;i++){
                a++;
            }
        });

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

        try{
            t1.join();
            t2.join();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("双线程执行事件:" + (end - beg)+ " ms");
    }
    public static void main(String[] args) {
        concurrency();
        serial();
    }
}

在这里插入图片描述
由此我们可以见到多线程确实对时间完成了优化,那么为什么不是完全的优化一半时间呢? 这是因为线程的创建也需要占用时间.

Thread类的属性和方法

创建线程对象的同时命名

利用Thread的构造方法可以对线程进行命名,如下将线程命名为 张三

public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while(true){
                System.out.println("Thread1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread");
        t.start();
    }

使用jconsole.exe来查看java进程

打开jdk文件夹下bin包中的jconsole.exe
在这里插入图片描述
右键以管理员身份运行,此时就可以看到正在运行的进程
在这里插入图片描述
点击连接后,大胆的选择不安全的连接
在这里插入图片描述
之后就可以得到我们创建的线程的信息
在这里插入图片描述

isDaemon 是否为后台程序

线程分为后台线程和前台线程,进程在执行完所有的前台线程后就会退出进程,而不管后台线程是否执行完毕.

线程启动 start

start决定了一个线程真正的启动,而run方法只是定义了线程要完成的任务

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello MyThread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "MyThread");
        t.start();

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
如此可见 start启动线程后 ,t线程和main线程随机执行
但是如果只调用run方法的话

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello MyThread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "MyThread");
        t.run();

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
可见只调用run方法的话,只是在main线程中调用了一个方法,并没有启动新的线程.

中断线程

在线程执行过程中,如果发生问题我们可以中断线程

手动设置标志位控制线程结束

设置isQuit变量来控制线程的结束

 private static boolean isQuit = false;

    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while(!isQuit){
                System.out.println("线程运行中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("新线程执行结束");
        });

        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("控制新线程退出");
        isQuit = true;

    }

使用Thread内置的标志位

1.Thread.interrupted() 是Thread类的静态方法 它的返回值是boolean, 他在默认情况下返回值是false,当在使用 thread 对象的 interrupted() 方法通知线程结束时,Thread.interruped的标志位变成true,然后在变回false.
2.Thread.currentThread().isInterrupted() 是Thread的实例方法, Thread.currentThread()返回的是当前Thread类的实例.

 public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(!Thread.currentThread().isInterrupted()){//获取到当前线程的实例  判断内置的标志位
                System.out.println("线程运行中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    System.out.println("即将退出");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    System.out.println("退出");
                    //break;
                }
            }
        });
        t.start();

        Thread.sleep(5000);
        System.out.println("控制新线程退出");
        t.interrupt();//对线程内部进行操作,若处于sleep阻塞状态,则会以 InterruptedException 异常的形式通知,清除中断标志,否则是对内置标志位进行操作

    }

当出现InterruptedException是,是否结束线程取决于catch()内部的写法,可以忽略,也可以选择结束

jion线程等待

多个线程之间的执行顺序是不一定的,我们无法彻底决定调度器调度哪个线程,但是我们可以使用一些手段,来决定谁是先执行的,使用join()实例方法

哪个线程调用的join,哪个线程就会优先执行,一直到调用join线程的run方法执行完毕

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });

        System.out.println(t.getState());
        t.start();
        t.join();

        System.out.println(t.getState());
    }

在这里插入图片描述

线程的状态

java中线程的状态是一个枚举类型

  1. NEW:任务分配完毕,但是线程还未启动的,即还未调用start方法的
  2. TERMINATED:工作完成了的,即线程的run方法执行完毕
  3. RUNNABLE:可工作的,即就绪的线程,为正在工作中或马上要进行工作的线程
  4. TIMED_WAITING:调用sleep()进入排队
  5. BLOCKED:表示当前线程在等待索,也是在排队
  6. WAITING:线程等待被唤醒状态,
    在这里插入图片描述

线程的安全

如果多线程代码运行的效果符合我们的预期想法,即在单线程下运行的效果,我们就认为线程是安全的

引起线程不安全主要有以下几个原因:
1.因为线程是抢占式执行,随机调度的,这是造成线程不安全的主要原因
2.多个线程对同一个变量进行修改操作
3.操作方式不是原子性的,会造成线程不安全
4.内存可见性问题(可见性指,一个线程对共享变量进行多次修改时,其他线程能及时的读取到修改后的变量的值.)
5.指令重排序:其是指在一段代码中,JVM、CPU指令集会对其进行优化,从而达到时间的更好地效率,这在多线程的情况下很可能使逻辑和之前不对等.

操作方式非原子的线程不安全

我们对同一个共享变量,使用两个线程对其分别进行50000次增加,最后来看一下变量的数值

class Counter{
    public int count;

    public void increase(){
        count++;
    }
}
public class Demo13 {
    public static Counter counter = new Counter();


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

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

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

        t1.join();
        t2.join();

        System.out.println("count: "+ counter.count);
    }
}

在这里插入图片描述
由此我们可以看到结果并不是我们预想的10万,这就是出现了线程安全的问题,
但是为什么是这样呢?

原因

这是因为count++ 其实是三个指令:
1.将内存中的count值读取到CPU的寄存器中
2.CPU的寄存器完成 +1 的运算
3.将寄存器中count的值重新读取到内存中

但是因为抢占式执行,随机调度的原因,两个线程的三个步骤执行可能会有很多种情况,
在这里插入图片描述
在这种情况时,t1线程 load 后,一个寄存器中count的值为0,
t2 load add save后,另一个寄存器中count的值为1,内存中count的值也变为1,
此时t1线程 add save后 寄存器中的count值为1,内存中count的值还为1,
这时,两个线程分别进行了count的自增,但是count的值为1.

这就出现了线程不安全的问题

解决

像这种情况,我们就需要加锁()来解决问题, 通过synchronized加锁,使代码的执行变成原子的(就是整体的 将加锁中的代码完全执行完,才可以变换为令一个线程)

class Counter{
    public int count;

    public synchronized void increase(){
        count++;
    }
}
public class Demo13 {
    public static Counter counter = new Counter();


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

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

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

        t1.join();
        t2.join();

        System.out.println("count: "+ counter.count);
    }
}

在这里插入图片描述

synchronized 用法

synchronized的意思使"同步",其在不同的环境中有不同的意义,在多线程的环境下,"同步"的意义就是互斥,即一个在进行时,另一个无法插入.

我们称synchronized为对象的synchronized,某个线程执行到一个对象的synchronized时,另一个线程执行到同一个对象的synchronized就会阻塞等待.

java的 锁 是存在在对象头里的
在这里插入图片描述

针对每一把锁,操作系统内部都维护了一个等待队列,当某个锁被一个线程占有时,其他的线程想要进行加锁操作就加不上了,从而进入了阻塞等待的状态,一直到之前被锁住的线程执行完后,操作系统在调度其他等待锁的线程
在这里插入图片描述
synchronized是 可重入锁 的,即当同一个线程对同一个锁对象进行二次执行,是可以允许完成的.
如下图,两个increase都是对 this 对象加锁的

class Counter1{
    public volatile static int counter;

    public synchronized void increase1(){
        counter++;
    }
    public synchronized void increase2(){
        counter++;
    }
}
public class Demo26 {
    public static Counter1 counter = new Counter1();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() ->{
            counter.increase1();
            counter.increase2();
        });

        t1.start();
        t1.join();
        System.out.println(counter.counter);
    }

}

synchronized 使用示例

  1. synchronized直接修饰普通方法 锁的是synchronized对象
public class SynchronizedDemo {
  public synchronized void methond() {
 }
}
  1. synchronized修饰类方法 锁的是synchronized的类对象
public class SynchronizedDemo {
  public synchronized static void method() {
 }
}
  1. 修饰代码块: 需要明确指定锁哪个对象
    锁当前对象:
public class SynchronizedDemo {
  public void method() {
    synchronized (this) {
     
   }
 }
}

锁类对象

public class SynchronizedDemo {
  public void method() {
    synchronized (SynchronizedDemo.class) {
   }
 }
}

两个线程竞争竞争同一把锁才会产生竞争,两个线程分别尝试获取不同的锁不会产生竞争.

内存可见性导致的线程不安全

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

  1. 当线程要读取一个共享变量的时候, 会先把变量从内存拷贝到cpu的寄存器中, 再从寄存器读取数据.
  2. 当线程要修改一个共享变量的时候, 也会先修改寄存器中的副本, 再同步回内存

由于每个线程有自己的cpu寄存器位置, 这些寄存器中的内容相当于同一个共享变量的 “副本”. 此时修改线程1的寄存器中的值, 线程2 的寄存器不一定会及时变化.

  1. 初始情况下, 两个线程的工作内存内容一致.
  2. 一旦线程1 修改了 a 的值, 此时内存不一定能及时同步. 对应的线程2 寄存器中的 a 的值也不一定能及时同步.
static class Counter{
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            while(counter.flag == 0){

            }
            System.out.println("循环结束");
        });


        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            counter.flag = scanner.nextInt();
        });
        t1.start();

        t2.start();
    }

当flag的值修改后,循环并没有立刻退出
在这里插入图片描述
但是,在加入volatile后.就可以保证内存可见性
在这里插入图片描述

在这里插入图片描述
但是使用 synchronized 关键字加锁。不光保证原子性,还能保证内存可见性。被 synchronized 包裹起来的代码,编译器就不敢轻易的做出上面优化的那种操作。

wait 和 notify

wait和notify分别指阻塞和通知停止阻塞,更倾向于处理线程调度随机性的问题.
wait必须在加锁的代码块中使用,
wait 内部会做三件事:1、先释放锁 2、等待其他线程的通知 3、收到通知之后,重新获取锁,并继续往下执行。

public static void main1(String[] args) throws InterruptedException {
    Object object = new Object();
    //wait 哪个对象,就得针对哪个对象加锁。
    synchronized (object) {
        System.out.println("wait 前");
        object.wait();
        System.out.println("wait 后");
    }
}

在这里插入图片描述

public static Object locker = new Object();
    public static void main(String[] args) {
        Thread waitTask = new Thread(() ->{
            synchronized (locker){
                try{
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");

                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        waitTask.start();

        Thread notifyTask = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入任意内容");
            scanner.next();

            synchronized (locker){
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        notifyTask.start();
    }

在这里插入图片描述

notifyAll

假如一个锁对象有10个阻塞的线程,notify只会随机唤醒一个线程,而notifyAll则会唤醒所有阻塞的线程,但是仍处于随即调度状态.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值