java多线程基础

1 基本概念 

      同步(synchronous)异步(asynchronous)

      并发(concurrency)并行(paralleism) 区别:并发是一个cup分时执行不同部分,并行是多个cpu线程同时执行。

      临界区 :公共资源(共享数据)。当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。每个资源一次只能有一个线程用,一旦临界区资源被占用,其他线程想使用必须等待。建立临界区用sychronized或者lock。

      阻塞(blocking) 非阻塞(non-blocking):多线程之间相互影响。阻塞:一个线程抢占临界区后其他线程挂起等待。非阻塞允许多线程进入临界区,分为无障碍,无锁,无等待。

             (1)无障碍 最弱的非阻塞调度,可自由出入临界区,宽进严出。如果无竞争,则操作;如果有竞争发现数据冲突,会重试新的数据直到无冲突,每次数据都可看作一个快照。

               (2)   无锁  前提是无障碍,保证一个线程可以胜出。cas。

             (3) 无等待  前提是无锁,至少一个能进能出,要求所有线程都必须在有限步内完成,也就是无饥饿。   

死锁(deadlock) 饥饿(stavation) 活锁(livelock)

2 java线程状态

        (1)启动 start方法。 java多线程调用方法有两种,一种重写run函数,一种传入runnable,为什么呢?thread类的start方法,调用的是run方法,run方法里,调用线程自身的一个runnable实例target的run方法,所以自己写线程,要么重写run,要么传入新的target实例。

        (2)终止  Thread.stop(),不推荐使用,释放当前所有锁,由于太暴力,有可能导致数据不一致。

        (3)中断  Thread.interrupt() 中断,isInterrupted() 判断是否中断。一般来说,从主线程调用中断函数,在线程中判断是否中断,如果中断执行相应操作如停止工作。中断相当于对线程发一个信号,修改线程标志位。

                 * 注意,使用线程sleep的时候,会抛出一个InterruptedException,是因为sleep的时候,无法响应interrupt中断,所以一旦中断标志位设置,就会在sleep中抛出异常,可以在catch中操作中断行为,方便立即响应。sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。

        (4)挂起(suspend)和继续执行(resume)。挂起不会释放锁,容易死锁不推荐使用,例如在不同线程里想resume一个线程,但resume和suspend顺序不固定,可能永远suspend。

        (5)等待线程结束(join) 谦让(yeild)。join会等待线程结束,用法类似countDownLatch.wait(),传入时间则只等待固定时间。  join的本质是 while(isAlive()){wait(0)}。

              yeild会让线程暂停,让出cpu资源,让同优先级线程一起竞争(cpu时间片),但不能指定多少时间,也不释放锁。

          * 守护线程  运维性质的工作,比如垃圾回收线程,jit线程。当一个java应用内只有守护线程时,虚拟机就会自然退出。setDaemon(ture) 可以把某线程设置为守护线程,但需要在start()之前设置。

          *线程优先级  线程上调用setPriority()设置优先级,优先级枚举例如Thread.MAX_PRIORITY,高优先级有更高概率抢资源,但不一定。

3 线程同步操作

      (1)synchronized

                对给定对象加锁,进入同步代码块前要获得给定对象的锁。

               给实例方法加锁,相当于对当前实例加锁,进入同步代码块之前要获得当前实例的锁。

               给静态方法加锁,给当前类加锁,进入同步代码块前要获得当前类的锁。 

      (2)wait notify  

               持有了某个对象的锁,才能调用这个对象的wait,如sychronized(object){object.wait();},否则报异常,会使当前线程释放锁,同样是等待,给sleep相反;

               同样,只有获得某个对象的锁,才能调用notify,notify唤醒的线程会重新争抢锁资源,如果调用notify的时候代码块不释放锁,那么唤醒的线程由于争抢不到资源还是无法执行。注意 notify只能随机唤醒一个等待的线程,nofityall可以唤醒所有等待线程。     

     (3)多线程内存模型

            原子性 指一个不可中断的操作,多线程也不会进行干扰。i++就不是原子操作

            有序性  java线程会对指令可能进行冲排序,不影响实际逻辑,可以节省cpu时钟周期。

            可见性 但一个线程修改了共享变量的值,其他线程是否能够立即知道这个修改。       

     *volatile 只是保证可见性,并不能保证原子性。使用volatile关键字会强制将修改的值立即写入主存;volatile会在变量上加一个lock前缀指令,相当于内存屏障,防止冲排序,lock前后内容不会混起来冲排序,lock之前内容必须是都执行完的,在对变量进行写操作的时候,会把结果立刻存入内存,并让所有的高速缓存(线程内存)的该变量副本无效。volatile防止重排序的功能实例如下 

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
 sleep()
}
doSomethingwithconfig(context);
有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

下图内存模型

       

     volatile不能保证原子性,多线程如果拿到同一个值同时修改,结果也可能相互覆盖。

  

      *happen-before规则

            程序顺序规则: 保证语义串行性,重排序与否不影响结果

            volatile规则:volatile变量写先发于读,保证可见性。

            锁规则:解锁(unlock)必然发生在随后加锁(lock)前。

            传递性:a先于b,b先于c,a先于c。

            线程start()优先于每一个动作;所有操作先于终结(join) 。

            线程中断(interrupt)先于被中断线程的代码。

            对象构造函数执行结束先于finalize()方法。

4 关键字原理 

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

      *Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

      *wait方法的使用必须在同步的范围内,否则就会抛出IllegalMonitorStateException异常。调用wait方法后,线程是会释放对monitor对象的所有权。

  *sleep暂停期间一直持有monitor对象锁,其他线程不能进入。

原文地址:https://www.cnblogs.com/lkdirk/p/6627192.html