java并发编程学习1--基础知识

CodeZenithPro
• 阅读 2419

【java内存模型简介

JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,如果高速缓存不经过内存的时候,也是不可见的一种表现。在Java程序中,内存本身是比较昂贵的资源,其实不仅仅针对Java应用程序,对操作系统本身而言内存也属于昂贵资源,Java程序在性能开销过程中有几个比较典型的可控制的来源。synchronized和volatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(memory barrier)的指令,来刷新缓存,使缓存无效,刷新硬件的写缓存并且延迟执行的传递过程,无疑该机制会对Java程序的性能产生一定的影响。

【java线程的运行机制

在java虚拟机进程中,执行程序代码的任务是由线程看来完成的。每个线程都有一个独立的程序计数器和方法调用栈。程序计数器:pc寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。方法调用栈:用来跟踪线程运行中一系列方法的调用过程,栈中的元素称为栈帧。每当线程调用一个方法,就会压栈一个新帧,帧用来保存方法的参数,局部变量,运算过程中产生的临时数据。java虚拟机的主线程是它从启动类的main()方法开始运行。此外,用户也可以创建自己的线程,两种方式:继承 Thread 类,实现 Runnable 接口。
但是运行一个线程必须使用Thread.strat(),切记:1.不可直接运行run(),直接运行run()只是单纯的方法调用,并不会产出新的线程。2.不要随意覆盖start(),如果必须覆盖记得首先调用super.start()。线程是不会顺序执行的,一切都由操作系统调度决定,并且一个线程只能启动一次,第二次启动会抛出:IllegalThreadStateException,但是并不会影响之前启动的线程工作。

public class MyRunnable implements Runnable{
            @Override
            public void run() {
                    System.out.println("runnable running");
           }
    }

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

【java线程状态

新建状态:new 语句创建的状态,此时它和其他java对象一样,仅仅在堆中被分配了内存。

就绪状态:当一个线程被其他线程调用了start(),此时jvm会为它创建程序计数器和方法调用栈。处于改状态的线程位于可运行池,等待获取CPU的执行权。

运行状态:处于改状态的线程占用CPU,正在执行程序代码。如果计算机只有一个单核CPU那么永远hi只有一个线程处于改状态。只有处于就绪状态的线程才可能成为运行状态。

阻塞状态:线程因为某些原因放弃了CPU暂停执行。此时线程放弃CPU的执行权,直到进入就绪状态才可能再次变为运行状态。阻塞状态3中情况:

  1. 对象等待池阻塞:线程执行了某个对象的wait(),线程被jvm放入这个对象的等待池之中。(用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。)
  2. 对象同步锁阻塞:线程试图获取对象的同步锁,如果同步锁已经被其他线程持有,jvm会把该线程放入对象锁池中。
  3. 其他阻塞状态:当前线程执行sleep(),或者调用其它线程的join()(把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。),或者发出了IO请求。

死亡状态:线程退出run(),有可能是正常执行完成,也有可能遇见异常退出。但是都不会对其他线程造成影响。Thread类有isAlive()(新建与死亡状态返回false,其余状态返回true)判断线程是否存活。

【线程调度

一个单核CPU在一个时刻只能执行一个机器指令。线程只有通过获得CPU才能执行自己的程序代码。所谓多线程的并发执行,其实从宏观上来看:各个线程轮流获得CPU的使用权,分别执行各自的任务。jvm采用抢占式调度模型,是指先让高优先级线程获得CPU。如果优先级相同,随机选择一个执行。处于运行状态的线程或一直执行,直到不得不放弃CPU,一般有如下原因:

1. jvm让其放弃CPU转入就绪状态。        
2. 线程因某些原因进入阻塞状态。       
3. 运行结束退出run()。

值得注意一点:java的线程优先级使用Thread.setPriority(int)设置,通常三个静态常量选择:Thread.MAX_PRIORITY(默认:10),Thread.MIN_PRIORITY(默认:1),Thread.NORM_PRIORITY(默认:5)。但是各个操作系统的线程优先级并不相同,所以为了确保程序能够在不同平台正常执行,我们只是用这三个值,不会使用1-10中的其他数字。常用方法:

  • Thread.sleep(long millis): 当前线程放弃CPU进入阻塞状态,经过milli毫秒后恢复就绪状态,不放弃对象锁的持有。

  • Thread.yield(): 让出CPU执行权进入就绪状态,给另一个拥有相同或者大于优先级的线程,如果没满足条件的线程,则什么都不做。

  • Thread.join(): 当前线程调用另一个线程的join(),并且等待被调用线程执行完后再继续执行。

  • Object.wait(): 当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。waite()和notify()必须synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

  • Object.notify(): 执行该方法的线程随机唤醒对象等待池中的一个线程,并将其装入对象锁池之中。

【线程的同步与并发

并发编程三个概念:

  1. 原子性:一个操作或者多个操作,要么全部成功,要么全部失败。

  2. 可见性:当多个线程访问同一变量时,一个线程修改了该变量的值,其他线程能立即看到修改后的值。

  3. 有序性:程序执行的顺序按照代码的先后顺序执行。(你以为这是废话?请了解指令重排序)。这三个特性中2,3可以由volatile关键字保证(2.缓存一致性协议,3.禁止指令重排序),1只能由同步方式保证。

同步是解决资源共享的有效手段。当一个线程在操作共享变量的时候,其他线程只能等待。只有当该线程执行完同步代码块后,其他线程才能有机会操作共享资源。通常有如下几种同步方式:

  1. synchorized关键字: 修饰方法或者使用同步代码块。

  2. ReentrantLock重入锁对象: 锁住共享变量的操作。

  3. 使用并发数据结构对象:Atomic系列,Concurrent系列等。

    但是同步的操作,代价较大,我们应该尽可能减少同步操作,是的一个线程能尽快的释放锁,减少其他线程执行的时间。由于等待一个锁的线程只有在获得了这把锁之后,
    才能继续执行所以让持有锁的线程及时释放锁的相当重要的。

    以下情况线程释放锁:

    1. 执行完同步代码块。

    2. 执行同步代码块的过程中,遇见异常,线程死亡,锁被释放。

    3. 执行同步代码块的过程中,执行了锁所属对象的wait(),这个线程会释放锁进入对象等待池。

以下情况线程不会释放锁:

  1. 执行同步代码块的过程中,执行了Thread.sleep(),当前线程放弃CPU开始睡眠进入阻塞状态,但是不会释放锁。

  2. 执行同步代码块的过程中,执行了Thread.yield(),当前线程放弃CPU开始睡眠进入就绪状态,但是不会释放锁。

  3. 执行同步代码块的过程中,其他线程执行了当前线程的suspend()(已废弃,同时废弃的还有:Thread.stop(),Thread.resume()),当前线程被暂停,但是不会释放锁。 死锁两个线程互相等待对方持有的锁,统统进入阻塞状态,jvm不检测也不避免这种情况。

【线程通信

不同的线程需要协作完成工作(一种情况是:线程2需要线程1的执行结果)。

- Object.wait(): 执行该放大的线程释放它持有的该对象的共享锁(前提时必须持有该共享锁),该线程进入对象等待池,等待其他线程将其唤醒。
    
- Object.notify(): 执行该方法的线程随机唤醒对象等待池中的一个线程,并将其装入对象锁池之中。

补充:

1.锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权。但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

2.等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。步骤如下(后面会有代码实例):

   1. t1执行s的一个同步代码块,t1持有s的共享锁,t2在s的锁池中等待。
   
   2. t1在同步代码中执行s.wait(0,t1释放s的共享锁,进入s的等待池。
   
   3. s的锁池中t2获得共享锁执行s的另一同步代码块。
   
   4. t2在同步代码块中执行s.notify(),JVM将t1从s的等待池转入s的锁池。
   
   5. t2完成同步代码,释放锁,t1获得锁继续执行同步代码。        
    

eg:两个线程,一个线程将某个对象的某个成员变量的值加1,而另外一个线程将这个成员变量的值减1.使得该变量的值始终处于[0,2].初始值为0:

【中断阻塞

当一个线程处于阻塞状态时,另一个线程调用阻塞线程的interrupt(),阻塞线程收到InterruptException,并退出阻塞状态,开始进行异常处理。代码:

@Override            
        public void run() {                
            System.out.println("runnable running");                
            try {                   
                 Thread.sleep(1l);                
            } catch (InterruptedException e) {     
                  //-----start异常处理----                    
                e.printStackTrace();                   
                  //-----end异常处理-----                
            }            
        }

【总结

并发编程的知识非常复杂,以上只是一些皮毛,后续还将学习Synchronized,ReentrantLock,Future,FutureTask,Executor,Fork/Join,CompletableFuture,Map-Reduce等相关知识,最后用一个实际项目来完成这部分知识的学习。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
volatile 关键字的使用场景及其原理
一、 Java线程的内存工作模型在当前的Java内存模型下(JVM1.2之后),线程(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fbaike.baidu.com%2Fitem%2F%25E7%25BA%25BF%25E7%25A8%258B)可以把变量保存在本地内存(
Wesley13 Wesley13
3年前
java之jvm
1.JVM内存模型_线程独占:栈,本地方法栈,程序计数器线程共享:堆,方法区_回答以上问题是需回答两个要点:1\.各部分功能2\.是否是线程共享2.JMM与内存可见性JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要
灯灯灯灯 灯灯灯灯
4年前
一次性带你了解清楚Java内存模型!
Java内存模型咳咳咳,能看完的都是人上人。。。。Java虚拟机内部使用JMM(Java内存模型)将内存划分为两个逻辑单元,线程栈(或者叫本地内存)和堆。每一个线程都有属于自己的线程栈,在线程栈中会保存局部变量(也叫做本地变量)、方法中定义的参数和异常处理器的参数(catch中的参数);这些参数和变量都属于线程局部操作,会被隔离,所以不受内存模
红烧土豆泥 红烧土豆泥
3年前
(转载)Java内存区域(运行时数据区域)和内存模型(JMM) - czwbig
转载自:Java内存区域和内存模型是不一样的东西,内存区域是指Jvm运行时将数据分区域存储,强调对内存空间的划分。而内存模型(JavaMemoryModel,简称JMM)是定义了线程和主内存之间的抽象关系,即JMM定义了JVM在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java
Wesley13 Wesley13
3年前
java并发编程实战:第十六章
一、什么是内存模型,为什么要使用它如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果编译器把变量保存在本地寄存器而不是内存中编译器中生成的指令顺序,可以与源代码中的顺序不同处理器采用乱序或并行的方式来执行指令保存在处理器本地缓存中的值,对于其他处理器是不可见在单线程中,只要
Wesley13 Wesley13
3年前
Java 内存模型
什么是Java内存模型?JMM(JavaMemoryModel,Java内存模型),它定义了多线程访问Java内存的规范。简单的说有以下几部分内容:Java内存模型将内存分为主内存和工作内存定义了几个原子操作,用于操作主内存和工作内存中的变量定义了volatile变量的使用规则happensbefor
Wesley13 Wesley13
3年前
Java并发编程之Synchronized
引子目前在Java中存在两种锁机制:synchronized和Lock,今天我们先来介绍一下synchronizedsynchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还保证了共享变量的内存可见性。用法Java中的每个对象都可以作为锁。每一个Object类及其子类
Wesley13 Wesley13
3年前
Java多线程之内存可见性
Java多线程之内存可见性一、Java内存模型介绍什么是JMM?Java内存模型(JavaMemoryModel)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的细节所有的变量都存储在主内存中每个线程都
Wesley13 Wesley13
3年前
JAVA内存模型与线程以及volatile理解
Java内存模型是围绕在并发过程中如何处理原子性、可见性、有序性来建立的。一、主内存与工作内存  Java内存模型主要目标是在虚拟机中将变量存储到内存和从内存中取出变量。这里的变量包括:实例字段、静态字段、构成数组对象的元素;不包括局部变量和方法参数,因为它们是线程私有的。Java内存模型规定了所有变量都存储在主内存,线程的工作内
Wesley13 Wesley13
3年前
Java线程安全总结
浅谈java内存模型 不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内
待兔 待兔
1年前
Java内存的可见性
Java内存的可见性可见性:一个线程对共享变量的修改,能够及时被其它线程看到共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量Java内存模型(JMM):描述了Java程序中各种线程共享变量的访问规则,以及在JVM
CodeZenithPro
CodeZenithPro
Lv1
致富路上请务必身体健康。
文章
3
粉丝
0
获赞
0