6

【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等相关知识,最后用一个实际项目来完成这部分知识的学习。


极品公子
221 声望43 粉丝

山不向我走来,我便向山走去。