java-并发-线程安全

多线程的可见性和有序性
———–多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。JMM规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。
当线程操作某个对象时,执行顺序如下:
(1) 从主存复制变量到当前工作内存 (read and load)
(2) 执行代码,改变共享变量值 (use and assign)
(3) 用工作内存数据刷新主存相关内容 (store and write)
当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。
线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本 (use),也就是说 read,load,use顺序可以由JVM实现系统决定。线程不能直接为主存中中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store- write),至于何时同步过去,根据JVM实现系统决定.有该字段,则会从主内存中将该字段赋值到工作内存中,这个过程为read-load,完成后线程会引用该变量副本,当同一线程多次重复对字段赋值时


    for(int i=0;i<10;i++)
     a++;  

线程有可能只对工作内存中的副本进行赋值,只到最后一次赋值后才同步到主存储区,所以assign,store,weite顺序可以由JVM实现系统决定


    public class Account {   

        private int balance;   

        public Account(int balance) {
            this.balance = balance;
        }   

        public int getBalance() {
            return balance;
        }   

        public void add(int num) {
            balance = balance + num;
        }   

        public void withdraw(int num) {
            balance = balance - num;
        }   

        public static void main(String[] args) throws InterruptedException {
            Account account = new Account(1000);
            Thread a = new Thread(new AddThread(account, 20), "add");
            Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println(account.getBalance());
        }   

        static class AddThread implements Runnable {
            Account account;
            int     amount;   

            public AddThread(Account account, int amount) {
                this.account = account;
                this.amount = amount;
            }   

            public void run() {
                for (int i = 0; i < 200000; i++) {
                    account.add(amount);
                }
            }
        }   

        static class WithdrawThread implements Runnable {
            Account account;
            int     amount;   

            public WithdrawThread(Account account, int amount) {
                this.account = account;
                this.amount = amount;
            }   

            public void run() {
                for (int i = 0; i < 100000; i++) {
                    account.withdraw(amount);
                }
            }
        }
    }  

第一次执行结果为10200,第二次执行结果为1060,每次执行的结果都是不确定的,因为线程的执行顺序是不可预见的。这是java同步产生的根源,synchronized关键字保证了多个线程对于同步块是互斥的,synchronized作为一种同步手段,解决java多线程的执行有序性和内存可见性,而volatile关键字之解决多线程的内存可见性问题。

synchronized关键字

java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区


    public synchronized void add(int num) {
         balance = balance + num;
    }
    public synchronized void withdraw(int num) {
         balance = balance - num;
    }  

一个线程执行临界区代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁
可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

生产者/消费者模式

经典的线程同步模型


    import java.util.ArrayList;
    import java.util.List;   

    public class Plate {   

        List<Object> eggs = new ArrayList<Object>();   

        public synchronized Object getEgg() {
            while(eggs.size() == 0) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }   

            Object egg = eggs.get(0);
            eggs.clear();// 清空盘子
            notify();// 唤醒阻塞队列的某线程到就绪队列
            System.out.println("拿到鸡蛋");
            return egg;
        }   

        public synchronized void putEgg(Object egg) {
            while(eggs.size() > 0) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
            eggs.add(egg);// 往盘子里放鸡蛋
            notify();// 唤醒阻塞队列的某线程到就绪队列
            System.out.println("放入鸡蛋");
        }   

        static class AddThread extends Thread{
            private Plate plate;
            private Object egg=new Object();
            public AddThread(Plate plate){
                this.plate=plate;
            }   

            public void run(){
                for(int i=0;i<5;i++){
                    plate.putEgg(egg);
                }
            }
        }   

        static class GetThread extends Thread{
            private Plate plate;
            public GetThread(Plate plate){
                this.plate=plate;
            }   

            public void run(){
                for(int i=0;i<5;i++){
                    plate.getEgg();
                }
            }
        }   

        public static void main(String args[]){
            try {
                Plate plate=new Plate();
                Thread add=new Thread(new AddThread(plate));
                Thread get=new Thread(new GetThread(plate));
                add.start();
                get.start();
                add.join();
                get.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("测试结束");
        }
    }  

声明一个Plate对象为plate,被线程A和线程B共享,A专门放鸡蛋,B专门拿鸡蛋。假设
1 开始,A调用plate.putEgg方法,此时eggs.size()为0,因此顺利将鸡蛋放到盘子,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有线程。
2 又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,自己进入了锁对象的阻塞队列。
3 此时,来了一个B线程对象,调用plate.getEgg方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,因此马上得到锁,开始往盘子里放鸡蛋,此时盘子是空的,因此放鸡蛋成功。
4 假设接着来了线程A,就重复2;假设来料线程B,就重复3。
整个过程都保证了放鸡蛋,拿鸡蛋,放鸡蛋,拿鸡蛋。

volatile关键字

olatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最彻底的同步要保证有序性和可见性,例如synchronized。任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于Valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的。

public class VolatileTest{

  public volatile int a;

  public void add(int count){

       a=a+count;

  }

}

当一个VolatileTest对象被多个线程共享,a的值不一定是正确的,因为a=a+count包含了好几步操作,而此时多个线程的执行是无序的,因为没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1)对变量的写操作不依赖于当前值。
2)该变量没有包含在具有其他变量的不变式中

public class VolatileTest{
  public volatile int a;
  public void setA(int a){
      this.a=a;
  }   

在没有volatile声明时,多线程环境下,a的最终值不一定是正确的,因为this.a=a;涉及到给a赋值和将a同步回主存的步骤,这个顺序可能被打乱。如果用volatile声明了,读取主存副本到工作内存和同步a到主存的步骤,相当于是一个原子操作。所以简单来说,volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。这是一种很简单的同步场景,这时候使用volatile的开销将会非常小。
线程的working memory只是cpu的寄存器和高速缓存的抽象描述,,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当个多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特性:原子性,有序性,可见性。动态的内存模型,甚至已经超越了JVM的范围。

jMM

1.程序计数器
是一个较小的内存空间,每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,看成是字节码的行号指示器。分支、循环、跳转、异常处理、线程回复等。每个线程私有一个计数器,独立存储,注意计数器内存区域是唯一没有规定没有任何oom的区域。
2.线程栈 -字节码服务
生命周期和线程一样,是线程私有的。线程的每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
3.本地方法栈-NATIVE 方法服务 sun spothot将以上两个栈合二为一了。
4.堆
每个线程的栈都是该线程私有的,堆则是所有线程共享的。当我们new一个对象时,该对象就被分配到了堆中。但是堆,并不是一个简单的概念,堆区又划分了很多区域,为什么堆划分成这么多区域,这是为了JVM的内存垃圾收集,似乎越扯越远了,扯到垃圾收集了,现在的jvm的gc都是按代收集,堆区大致被分为三大块:新生代,旧生代,持久代(虚拟的);新生代又分为eden区,s0区(From survivor),s1区(to survivor)。新建一个对象时,基本小的对象,生命周期短的对象都会放在新生代的eden区中,eden区满时,有一个小范围的gc(minor gc),整个新生代满时,会有一个大范围的gc(major gc),将新生代里的部分对象转到旧生代里。
JIT编译器的发展,和逃逸分析技术的成熟,不是所有的实例对象都在这儿了。
5.方法区
其实就是永久代(Permanent Generation),方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至 少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
6.常量池
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)
关于垃圾收集,在此不多说,流到垃圾收集那一章再详细说吧。关于java的同步,其实还有基于CPU原语的比较并交换的非阻塞算法(CAS),不过这个在java的并发包里已经实现了很多。
7、直接内存
不属于JMM,在nio引入中,基于channel和buffer的I/O模式,可以使用native函数直接分配堆外的内存,然后在一个存在于堆中directByteBuffer作为这快内存引用进行操作,会提升性能,避免了java堆和native堆的来回复制数据。

时间: 2024-09-14 16:14:51

java-并发-线程安全的相关文章

Java并发——线程间协作(wait、notify、sleep、yield、join)

1 线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线程创建完成时为新建状态,即new Thread(...),还没有调用start方法时,线程处于新建状态 Runnable:就绪状态,当调用线程的的start方法后,线程进入就绪状态,等待CPU资源.处于就绪状态的线程由Java运行时系统的线程调度程序(*thread scheduler*)来调度 R

详解Java传统线程同步通信技术

编写代码实现以下功能 子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次. 分析 1)子线程循环10次与主线程循环100次必须是互斥的执行,不能出现交叉,下面代码中通过synchronized关键字实现此要求: 2)子线程与主线程必须交替出现,可以通过线程同步通信技术实现,下面代码中通过bShouldSub变量实现此要求: 其他需要注意的地方 1)其中business变量必须声明为final类型,因为在匿名内部类和局部内部类中调用

Java 并发/多线程教程(五)-相同线程

       本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!        相同线程是一并发框架模型,是一个单线程系统向外扩展成多个单线程的系统.这样的结果就是多个单线程并行运行. 为什么是单线程系统?         你也许会感到好奇,为什么当今还有人设计单线程系统.单线程系统之所以这么普及,是因为单线程系统相对于多线程并发系统更为简单.单线程系统不需要与其他线程共享任何数据.这就使得单线程系统可以使用非并发的数据结构,可以更

《 Java并发编程从入门到精通》 Java线程池的监控

本文是< Java并发编程从入门到精通>第9章 线程的监控及其日常工作中如何分析的9.1节 Java线程池的监控.   看不到不等于不存在!让我们来看看工作中是如何找问题解决问题的. 鸟欲高飞先振翅,人求上进先读书. 京东,亚马逊,当当均有销售. 9.1 Java线程池的监控 如果想实现线程池的监控,必须要自定义线程池继承ThreadPoolExecutor类,并且实现beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一

Java 并发/多线程教程(七)-创建和启动java线程

      本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正! 创建和启动线程 在java中创建一个线程如下: Thread thread = new Thread(); 调用方法start()来启动一个线程: thread.start();         这个例子没有指定线程执行任何代码,线程将会在启动之后停止.         有两种方式指定线程应该执行什么代码.第一种方式就是创建一个Thread的子类并覆写run()方法.第

Java并发编程:线程池的使用(转)

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool

超越线程池:Java并发并没有你想的那么糟糕

很多人一直唠叨着并发中的新概念.然而,许多开发人员还没有机会把过多的注意力都放在上面.在这篇文章中,我们将带您了解Java 8 streams. Hadoop. Apache Spark. Quasar fibers以及响应式编程,让你迅速入门.尤其是如果你不经常用它们的话.一句话,它并不遥远,它就在我们身边. 我们该怎么做? 谈到并发,一种很好的方式来形容当前的问题是来回答几个小问题以便更好的了解它: 它是一个数据处理任务么?如果是这样的话,它可以分解为独立的任务单元么? 操作系统.虚拟机和你

Java并发编程示例(十):线程组_java

对线程分组是Java并发API提供的一个有趣功能.我们可以将一组线程看成一个独立单元,并且可以随意操纵线程组中的线程对象.比如,可以控制一组线程来运行同样的任务,无需关心有多少线程还在运行,还可以使用一次中断调用中断所有线程的执行. Java提供了ThreadGroup类来控制一个线程组.一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程. 根据<Effective Java>的说明,不再建议使用ThreadGroup.建议使用Executor. --D瓜哥特此

深入解析Java并发程序中线程的同步与线程锁的使用_java

synchronized关键字 synchronized,我们谓之锁,主要用来给方法.代码块加锁.当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码.当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段.但是,其余线程是可以访问该对象中的非加锁代码块的. synchronized主要包括两种方法:synchronized 方法.synchronized 块. synchron

Java并发编程示例(九):本地线程变量的使用_java

共享数据是并发程序最关键的特性之一.对于无论是继承Thread类的对象,还是实现Runnable接口的对象,这都是一个非常周重要的方面. 如果创建了一个实现Runnable接口的类的对象,并使用该对象启动了一系列的线程,则所有这些线程共享相同的属性.换句话说,如果一个线程修改了一个属性,则其余所有线程都会受此改变的影响. 有时,我们更希望能在线程内单独使用,而不和其他使用同一对象启动的线程共享.Java并发接口提供了一种很清晰的机制来满足此需求,该机制称为本地线程变量.该机制的性能也非常可观.