多线程(二)

多线程(二),第1张

一、线程之间的数据共享—线程通信

      大部分场景下,几个线程之间是需要协调配合工作(线程之间需要进行数据交换),一起完成一个总目标的。由于我们的线程都属于同一个进程,所以共享有OS分配过来的同样的资源(其中优先关注内存资源)

1.JVM下的内存区域划分

PC保存区(PC);栈区(本地方法栈;虚拟机栈);堆;方法区;运行时常量池

      这些内存资源是属于进程的。理论上来讲,确实也是这个进程下所有线程的,但在进程内部也有分配机制,这些内存在线程中还会分配一次。(类比:家庭财务,属于家庭,但细分每个人都有对应花销)并且不是 *** 作系统做分配了,是进程内部分配。

      堆、方法区、运行时常量池是整个进程(JVM)只有一份,PC(保存PC值)、栈(虚拟机栈、本地方法栈)是每个线程独一份(各自有各自的)【对象是共有的,加载的类是共有的】

1)为什么每个线程要有自己的PC

每个线程都是独立的执行流,要下一条要执行的指令和其他线程无关,所以有自己的PC。

2)为什么每个线程都得有自己的栈

每个线程都是独立的执行流,各自有各自调用的方法链,有各自要处理的临时数据,所以栈也是独一份的。

3)体现到代码中的共有和私有

局部变量,保存在栈桢中,也就是保存在栈中,是线程私有

类对象(Class对象-关于类的对象)、静态属性,保存在方法区中,所以是线程之间共享的【前提:有访问权限】

对象(对象内部属性),保存在堆中,所以是线程之间是可以共享的【前提:线程持有该对象的引用】(见4)5)这个两个例子

1)区分不同内存块的r

/**
 * 变量本质是对一块内存的抽象
 * 观察下面r
 * 两个r只是名字都叫r,但不是同一块内存
 **/
public class Main {
    static class MyThread extends Thread{
        @Override
        public void run() {
            int r=0;//r存在于子线程run方法的栈帧中
            for (int i=0;i<1000;i++){
                r++;
            }
            System.out.println(Thread.currentThread().getName()+":"+r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int r=0;//r存在于主线程main的栈帧中
        MyThread t=new MyThread();
        t.start();//开启线程
        t.join();//等待线程结束
        System.out.println(r);//此处r仍为0,因为++的是子线程的r,不是主线程的
    }
}

2)一共在内存中开辟了几个名叫r的内存块

public class Main {
    static class MyThread extends Thread{
        private final int total;
        public MyThread(int total){
            this.total=total;
        }
        @Override
        public void run() {
            int r=0;//r存在于子线程run方法的栈帧中
            for (int i=0;i<1000;i++){
                r++;
            }
            System.out.println(Thread.currentThread().getName()+":"+r);
        }
    }

    /**
     * 一共在内存中开辟了几个名叫r的内存块
     * 4个
     * t1线程下的run方法下的r
     * t2.run.r
     * t3.run.r
     * 主线程.main.r
     */
    public static void main(String[] args) throws InterruptedException {
        int r=0;//r存在于主线程main的栈帧中
        MyThread t1=new MyThread(1000);
        t1.start();//开启线程
        MyThread t2=new MyThread(1000);
        t2.start();//开启线程
        MyThread t3=new MyThread(1000);
        t3.start();//开启线程
        t1.join();//
        t2.join();//等待线程结束
        t3.join();//等待线程结束
        System.out.println(r);//此处r仍为0,因为++的是子线程调用到的r,不是主线程的
    }
}

结果:

Thread-0:1000
Thread-2:1000
Thread-1:1000
0

3)r为静态属性时,整个进程中就这一个r

public class Main {
    static int r=0;//定义为静态属性
    //进程中就这一个r
    static class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                r++;
            }
            System.out.println(getName()+":"+r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t=new MyThread();
        t.start();
        t.join();
        System.out.println(Thread.currentThread().getName()+":"+r);
    }
}

结果:

Thread-0:1000
main:1000

分析:主线程虽然不对r做更改,但是子线程改变了r的值,主线程也就同步更改了,这也就是数据共享。

4)对象没有共享的情况

/**
 *有几个r
 * 两个
 * 1.t线程run方法的someObject引用指向的对象中的r
 * 2.主线程的main方法的someObject指向的对象中的r
 * 对象不是同一个对象,所以r有两个
 * 注意:虽然说对象是共享的,但前提是 *** 作的得是同一个数据
 *     如果数据不是同一个,即使是对象也不能说一定共享
 *    只能说,对象是可以被共享,但不是说对象一定被共享
 **/
public class Main {
   static class SomeObject{
       int r=0;
   }
    static class MyThread extends Thread{
        @Override
        public void run() {
            SomeObject someObject=new SomeObject();
            for (int i = 0; i < 1000; i++) {
                someObject.r++;
            }
            System.out.println(getName()+":"+someObject.r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SomeObject someObject=new SomeObject();
        MyThread t=new MyThread();
        t.start();
        t.join();
        System.out.println(Thread.currentThread().getName()+":"+someObject.r);
    }
}
//结果:
//Thread-0:1000
//main:0

5)对象之间共享的情况- *** 作的是同一批数据

package com.wy.about_data_share.demo5;
//想办法让子线程和主线程的引用指向同一个对象-利用构造方法
public class Main {
   static class SomeObject{
       int r=0;
   }
    static class MyThread extends Thread{
       private final SomeObject someObject;
       public MyThread(SomeObject someObject){
           this.someObject=someObject;//这个someObject指向主线程main方法栈的someObject指向的对象
       }
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                someObject.r++;
            }
            System.out.println(getName()+":"+someObject.r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SomeObject someObject=new SomeObject();//只有这个地方创建对象
        MyThread t=new MyThread(someObject);//把SomeObject对象的引用传入
        t.start();
        t.join();
        System.out.println(Thread.currentThread().getName()+":"+someObject.r);
    }
}

结果:

Thread-0:1000
main:1000

小结:

二、线程安全 1.演示线程不安全现象
package com.wy.thread_safe.phenomenon;

/**
 * 演示线程不安全现象
 **/
public class Main {
    //定义一个共享的数据--静态属性的方式来体现
    static int r=0;
    //定义加减的次数
    static final int COUNT=100_0000;//COUNT越大,出错概率越大,COUNT越小,出错概率越小
    //定义两个线程,分别对r进行加法和减法 *** 作
    static class Add extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }
    static class Sub extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add=new Add();
        add.start();
        Sub sub=new Sub();
        sub.start();
        //等待线程结束
        add.join();
        sub.join();
        //理论上,线程结束后,r被加了COUNT次,也被减了COUNT次
        //所以r结果应该是0,但是实际结果不一定,这就是线程不安全的现象
        System.out.println(r);
    }
}

      结果:(一个每次执行结果都不同的随机值)。单看代码“没有问题”(暂时)的情况下,结果是错误的(无法100%预期效果),这就是线程不安全的现象。

      COUNT越大,出错概率越大,COUNT越小,出错概率越小。

2.解释线程不安全现象出现的原因⭐⭐⭐ 1)开发者角度

1)多个线程之间 *** 作同一块数据了(共享数据)——不仅仅是内存数据

2)至少有一个线程在修改这块共享数据

【总结12即多个线程中至少有一个对共享数据做修改(写) *** 作

2)系统角度解释 a.原子性被破坏

      1)前置知识:1.java代码(高级语言)中的一条语句可能对应多条指令(eg:r++ *** 作,指令1:从r在的内存区域把数据加载到寄存器,指令2:完成+1 *** 作,指令3:寄存器中值写回内存);2.线程调度是可能发生在任意时刻的,但是不会切割指令。

      2)原子性(atomic)问题:

      程序员的预期是r++是一个原子性 *** 作(++指令全部完成或全部没完成)【r--同理】,但实际执行起来保证不了原子性,所以会出错。COUNT越大,出错概率越大,是因为COUNT越大,线程执行需要跨时间片的概率越大(碰到线程调度的概率越大),出错概率也就越大。原子性是破坏线程安全的最常见的原因

b.内存可见性问题(读取到“脏数据”)

      1)前置知识:CPU中为了提升数据获取速度,一般在CPU中设置缓存(Cache)【指令执行速度>>内存的读写速度】

      CPU缓存和内存的关系:(缓存就是放数据的地方)

      现在的CPU,L1、L2缓存每个CPU都有,L3缓存是所有CPU共用一份。

       JVM规定了JVM内存模型(JVM Memory Model/JMM),把一个线程想象成一个CPU,主内存/主存储:真实内存;工作内存/工作存储:CPU中缓存的模拟(不需要区分L123)。

      线程的所有数据 *** 作(读/写),必须1.先从主内存加载到工作内存2.再在工作内存中处理3.处理结束后,再把数据存储回主内存。

      2)基于上面理解解释内存可见性问题

      一个线程对数据的 *** 作,很可能其他线程是无法感知的,甚至某些情况下,会被优化成完全看不到的结果

c.代码重排序,导致数据配合出问题

      1)前置知识:程序从某些角度就是一个状态机(a=0,b=1,c=2....这是一个状态,经历有限步骤,a=100,b=100,c=100...新状态【中间通过语句/指令来进行状态迁移这个过程】),留给编译器很大的空间做优化(通过把中间变换的无用步骤取掉达到优化)。

eg:

      我们写的程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一模一样的。【可以做优化的环节:编译器(javac)、类加载器(少见)、运行期间->JVM:JIT(Just In Time 即时编译),硬件(CPU)层面——最终结论:作为应用开发,是无从得知做了哪些优化的】【所做的优化可以有哪些:删除某些步骤,调整某些步骤的执行数据】

2)基于上面理解,引出重排序

      所谓重排序,就是指执行的指令和书写的指令并不一致。JVM规定了重排序的基本原则:happend-before规则:简要解释->JVM要求,无论怎么优化,对于单线程视角,结果不应该有改变,但并没有规定多线程的情况(因为不能规定),导致多线程环境可能出问题。

eg:理解:

      A本身不优化时B介入在苹果前时间点,A最后没有事情,但A优化后B就来不及防解药,A中毒,B的介入使A的优化出现问题,这也就是重排序情况下导致多线程出现的问题。

2.什么是线程安全

1)程序的线程安全:代码的运行结果100%符合预期,才能被称为线程安全。(理想情况,无法实 *** )

2)Java语境下,经常说某个类、某个对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全问题。eg:ArrayList就不是线程安全的:ArrayList实现中,完全没考虑过线程安全的任何问题,所以无法直接使用在多线程环境(多个线程同时 *** 作一个ArrayList)下。

1)多线程中,哪些情况下不需要考虑线程安全问题?(哪些情况是安全的)

1)几个线程之间互相没有任何数据共享的情况下,天生是线程安全的。

2)几个线程之间即使有数据共享,但是都是做读 *** 作,没有写 *** 作时,也是天生线程安全的。

2)作为程序员如何考虑线程安全问题

1)尽可能让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全了(如我们在线程(一)join()处的归并排序的例子:4个线程虽然处理的是同一个数组,但是我们提前画好了范围,每个排自己对应的区间对应内存的数据,所以其实也是没有数据共享的)

2)如果一定要共享,尽可能不去修改,只进行读 *** 作。(如static final int COUNT=...,即使多个线程同时使用COUNT也无所谓,因为COUNT只进行读 *** 作,不能被修改)

所以,我们接下来学习一些机制,目标是和JVM沟通,避免上述线程不安全问题的发生

三、保护线程安全的机制 1.之前学习过的一些常见类的线程安全的问题 1)线程不安全的:

      ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet都不是线程安全的。分析:ArrayList为什么不是线程安全的(多个线程同时对一个ArrayList对象有修改 *** 作时,结果会出错)

2)线程安全的:

      Vector、Stack、Dictionary、StringBuffer,这几个类都是Java设计失败的产品(StringBuilder vs StringBuffer,一个线程不安全,一个线程安全)

3)最常见的违反原子性的场景

1.read-write场景

      i++;;arr[size]=e;size++;

2.check-update场景

      if(a==10){a=...;}

2.锁(lock)

synchronized:锁/同步锁/monitor锁(监视器锁)

1)语法(synchronized修饰符) a.synchronized修饰方法(普通、静态)->同步方法

synchronized int add(...){...}

    //synchronized修饰普通方法
    synchronized int add(){
        return 0;
    }
    //synchronized修饰静态方法
    private synchronized static int sub(){
        return 0;
    }
b.synchronized作同步代码块

synchronized(引用){...}

前言:

//类名.class:是一个引用,指向关于这个类对象(不是这个类实例化出来的对象,而是这个类数据表现出的对象(每个被加载进来的类都可以通过类名.class访问到))

//每个被加载的类有且仅有一个Class对象

synchronized void method(){
    ...
}
等价于
void method(){
    synchronized (this){
        ...
    }
}

static synchronized void method(){
    ...
}
等价于
static void method(){
    synchronized (类.class){
        ...
    }
}

所以,只要解释清楚同步代码块如何工作,就能理解同步方法怎么工作,这两个本质上等价

2)锁:理论上就是一段被多个线程之间共享的数据

两种状态

a.锁上状态(locked) b.打开(unlocked)

执行语句时可以将语句用锁包起来:

c.尝试加锁的内部 *** 作

整个尝试加锁的 *** 作已经被JVM保证了原子性

当锁没有锁上时:

if(lock==false){//说明锁没有锁上

lock=true;//当前线程把锁lock

return;//正常向下执行

}

当锁已经锁上时:

//已经有线程锁上了,让多线程的其他线程进入阻塞队列等待加锁,同时让出CPU

Queue<线程(该锁的阻塞队列)> queue=...;

queue.add(Thread.currentThread());//把自己加到锁上,等待这把锁被释放

//既然我们无法获取到锁,所以就应该让出CPU

Thread.currentThread().state=阻塞;//锁的状态因为等锁就变为阻塞状态

Thread.yield();//这个理解为让出CPU

d.释放锁的 *** 作(纯理论)

这个过程由系统保证了原子性

loke=false;

从等待锁的阻塞队列中,选一个线程出来,恢复CPU

Thread t=queue.poll();

t.state=就绪;//状态变为就绪,等待分配CPU

      当多个线程都有加锁 *** 作,并且申请的是同一把锁时,会造成加锁过程中的代码(叫做临界区)s会互斥(mutex/exclusive)着进行【临界区代码必须持有锁才能进行,而每次只有一个线程能持有锁,所以临界区代码一定是互斥进行的,只有某个持有锁的进行完了,才能从一堆其他持有锁的线程中选另一个进行...】

package com.wy.thread_safe.exclusive;
public class Main {
    //这个对象用来当锁对象
    static Object lock=new Object();
    static class MyThread1 extends Thread{
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100000; i++) {
                    System.out.println("我是张三");
                }
            }
        }
    }
    static class MyThread2 extends Thread{
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100000; i++) {
                    System.out.println("我是李四");
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1=new MyThread1();
        t1.start();
        Thread t2=new MyThread2();
        t2.start();
        //不加锁时,一会张三一会李四交替出现
        //加锁后(两个都是加锁的,看哪个锁先抢上),谁先抢到谁打印完才开始另一个的打印——印证互斥,即不能同时在执行,一定互斥在进行
//锁锁住哪里哪里的代码就有互斥性
    }
}
e.判断以下哪些会互斥,哪些不会?

互斥的必要条件:线程都有加锁 *** 作并且锁的都是同一个对象

t1线程t2线程t1和t2执行的代码块是否互斥
s1.m1()【有加锁 *** 作,对this加锁,this就是s1指向的对象,this==s1】s1.m1()【有加锁 *** 作,对this加锁,this就是s1指向的对象,this==s1】有互斥
s1.m1();有加锁,this==s1s3.m1();有加锁,this==s3s1==s3,有互斥
s1.m1();有加锁加的s1s2.m1();有加锁加的s2s1,s2是两个不同对象,不等,没有互斥
s1.m1();有加锁,对this加锁,即对s1加锁s1.m2()有加锁,m2静态方法是对SomeObject.class加锁s1指向的对象与SomeObject.class指向的对象不一样(类型都不一样:SomeObject s1;Class SomeObject.class),不是同一把锁,不会互斥。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/722678.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存