JUC-并发基础

1 并发基础概念

1.1 并发和并行

他们都可以表示你两个或者多个任务一起执行,但是偏重点不同,并发偏重于多个任务交替执行,并行是真正意义上的同时执行。 并发和并行都能表示两个或多个任务一起执行,但是并发偏重于任务交替执行,多个任务之间很可能是串行的,而并行是真正意义上的"同时执行"。

并发三要素:原子性、可见性、有序性

1.2 java的内存模型(JMM)及基础

java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。可以避免像c++等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同,比如有些c/c++程序可能在windows平台运行正常,而在linux平台却运行有问题。

1.2.1 原子性

对象的状态

对象的状态是指存储在状态变量(例如实例和静态域)中的数据。对象状态还可能包含其他依赖对象的域。

竞态条件

在并发编程中,由于不恰当的执行时序而出现不正确的结果。

发生条件:当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。最常见的竞态条件就是:先检查后执行(在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效,从而导致各种问题)。

复合操作:包含一组必须以原子方式执行的操作以确保线程安全性。

在线程安全中,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,但某个变量更新时,需要早同一个原子操作中对其他变量同时更新。要保证一致性就需要在单个原子操作中更新所有相关的状态变量。

由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽律不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock和unlock也可以满足需求。lock和unlock虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令monitorenter和monitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性。

内置锁

同步代码块包括两个部分:一部分是作为锁的对象的引用,一个作为由这个锁保护的代码块。

synchronized(lock) {
//访问或修改由锁保护的共享状态
}
重入概念

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获取一个已经由他自己持有的锁,那么这个请求就会成功。

1.2.2 可见性

可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。
除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。

1.2.3 有序性

有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句值得是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。
保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。

总体来看,synchronized对三种特性都有支持,虽然简单,但是如果无控制的滥用对性能就会产生较大影响。

指令重排原则先行发生原则

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏同步的多线程程序中,想要堆内存操作的执行顺序进行判断,几乎是不可能得出正确的结论的。

  • 一个线程内保证语义的串行性
  • volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:解锁必然发生在随后的枷、加锁前
  • 传递性:A先于B,B先于C,那么a必然先于c
  • 线程的start方法先于他的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断先于被中断的代码
  • 对象的构造函数执行,结束先于finalize()方法

1.2.4 工作内存与主内存交互

下图为物理机的图 -对应,线程、工作内存、主内存。

工作内存和主存交互.jpg

主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。

工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
这里需要说明一下:主内存、工作内存与java内存区域中的java堆、虚拟机栈、方法区并不是一个层次的内存划分。这两者是基本上是没有关系的,本文只是为了便于理解,做的类比

下图为jvm抽象出的图

jmvstorage.jpg

1.2.5 八种原子操作

java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的,每种操作必须是原子性的(double和long类型在某些平台有例外)。

java虚拟机中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。

  1. lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  2. unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  3. read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  4. load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  5. use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  6. assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  7. store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  8. write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

对于这8中操作,虚拟机也规定了一系列规则,在执行这8中操作的时候必须遵循如下的规则:

  • 不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
  • 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
  • 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
  • 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
  • 一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
  • 对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
  • 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
  • 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作

1.2.5 volatile修饰规则

关键字volatile可以说是java虚拟机中提供的最轻量级的同步机制。他的正确使用方式包括:确保他们自身状态的可见性,确保他们所引用的对象的状态的可见性,以及表示一些重要的程序生命周期事件的发生。比如 用于标注,同步机制可以确保原子性和可见性,volatile只可以确保可见性。缓存一致性。

使用条件:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

假定T表示一个线程,V和W分别表示两个volatile修饰的变量,那么在进行read、load、use、assign、store和write操作的时候需要满足如下规则:

  • 在线程T的工作内存中,每次使用变量V之前必须从主内存去重新获取最新的值,用于保证线程T能看得见其他线程对变量V的最新的修改后的值。
  • 在线程T的工作内存中,每次修改变量V之后必须立刻同步回主内存,用于保证线程T对变量V的修改能立刻被其他线程看到。
  • 说在同一个线程内部,被volatile修饰的变量不会被指令重排序,保证代码的执行顺序和程序的顺序相同。

总结上面三条规则,前面两条可以概括为:volatile类型的变量保证对所有线程的可见性。第三条为:volatile类型的变量禁止指令重排序优化

  • valatile类型的变量保证对所有线程的可见性

    可见性是指当一个线程修改了这个变量的值,新值(修改后的值)对于其他线程来说是立即可以得知的。vola>tile类型的变量每次值被修改了就立即同步回主内存,每次使用时就需要从主内存重新读取值。返回到前面对普通变量的规则中,并没有要求这一点,所以普通变量的值是不会立即对所有线程可见的。volatile的规则,保证了read、load、use的顺序和连续行,同理assign、store、write也是顺序和连续的。也就是这几个动作是原子性的,但是对变量的修改,或者对变量的运算,却不能保证是原子性的。如果对变量的修改是分为多个步骤的,那么多个线程同时从主内存拿到的值是最新的,但是经过多步运算后回写到主内存的值是有可能存在覆盖情况发生的。

  • volatile变量禁止指令重排序优化

    普通的变量仅仅会保证在该方法执行的过程中,所有依赖赋值结果的地方都能获取到正确结果,但不能保证变量赋值的操作顺序和程序代码的顺序一致。因为在一个线程的方法执行过程中无法感知到这一点,这也就是java内存模型中描述的所谓的“线程内部表现为串行的语义”。也就是在单线程内部,我们看到的或者感知到的结果和代码顺序是一致的,即使代码的执行顺序和代码顺序不一致,但是在需要赋值的时候结果也是正确的,所以看起来就是串行的。但实际结果有可能代码的执行顺序和代码顺序是不一致的。这在多线程中就会出现问题。

2. 线程补充

2.1 线程中断

thread中三个关于中断的方法

public void interrupt();  // 中断线程
public static boolean interrupted() //判断是否被中断
public boolean isInterrupted()  //判断是否被中断

如果想要一个线程在中断时执行一些操作,那么需要在对应的run方法中设置相应的代码处理.

thread.sleep()方法会让当前线程休眠若干时间,可能抛出interruptexception异常需要捕获

2.2 等待wait和通知notify

当一个对象示例调用wait()方法之后,当前线程就会在这个对象上等待.

比如线程A调用obj.wait()方法,那么线程A就会进入一个object对象的等待队列,这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象.当object.notifyy()被调用时,他就会从整个队列中随机唤醒一个线程.

还有一个唤醒方法 notifyAll().他会唤醒这个等待队列中所有的等待的线程,而不是随机选择一个.

**注:**wait方法并不是可以随便的调用的,他必须包含在对应的synchronized语句中,无论是wait或者notify都需要首先获得目标对象的一个监视器这两个方法结束后再释放监视器.

wait与sleep的区别
leep()是使线程暂时停止执行一段时间的方法,wait()方法也是一种使线程暂停执行的方法,例如,当线程交互时,如果线程对一个同步对象X发出一个()调用请求,那么,该线程会暂停执行,被调用对象进入等待状态,知道被唤醒或等待时间超时。

具体而言,sleep()方法与wait()方法的区别主要表现在一下几个方面:

(1)原理不同,sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动“苏醒”。例如,当线程执行报时功能时,每一秒钟打印一个时间,那么此时就需要在打印方法前面加上一个Sleep()方法,以便让自己每隔1s执行一次,该过程如同闹钟一样,而wait()方法是object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其它线程调用notify()方法(或notifyALL方法)时才“醒过来”,不过,开发人员也可以给它指定一个时间,自动“醒”过来。与wait()方法配套的方法还有notify()方法和notifyALL()方法。

(2)对锁的处理机制不同,由于sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用sleep()方法并不会释放锁, 而wait()方法则不同,当调用wan()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。举个简单的例子,如果小明拿遥控器的期间,可以用自己的sleep()方法每隔10min调一次频道,而在这10min里,遥控器还在他的手里。

(3)使用的区域不同。由于wait()方法的特殊意义,因此,它必须放在同步控制方法或同步语句块中使用,而 sleep()方法则可以放在任何地方使用。
sleep()方法必须捕获异常,而wait()、notify()以及notifyALL()不需要捕获异常,在sleep()的过程中,有可能被其他对象调用它的interrupt(),产生interruptException异常。
由于sleep不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法
wait与sleep的区别原文

synchronized语句示例

 public class SimpleWN {
        final static Object object = new Object();

        public static class T1 extends  Thread{
            public void run() {
                synchronized (object){
                    System.out.println(System.currentTimeMillis()+":T1 start! ");
                    System.out.println(System.currentTimeMillis()+":T1 wait for object");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(System.currentTimeMillis()+":T1 end! ");
                }
            }
        }

        public static class T2 extends Thread{
            public void run() {
                synchronized (object){
                    System.out.println(System.currentTimeMillis()+":T2 start! notify one thread");
                    object.notify();
                    System.out.println(System.currentTimeMillis()+":T2 end! ");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        public static void main(String[] args) {
            Thread t1 = new T1();
            Thread t2 = new T2();
            t1.start();
            t2.start();
        }
    }

上述代码中,开启了T1和T2两个线程。T1执行了object.wait()方法。执行wait()方法前,T1先申请object的对象锁。

因此,在执行object.wait()时,它持有的是object的对象锁的。wait()方法执行后,T1会进行等待,并释放object对象锁。

T2在执行notity()方法之前也会先获得object的对象锁,这里为了让结果明显,特意在notity()方法执行之后,让T2休眠2秒,

这样做可以更明显地说明,T1在得到notity()方法通知后,还是会先尝试重新获得object的对象锁。

执行结果:

1570675715571:T1 start! 
1570675715571:T1 wait for object
1570675715571:T2 start! notify one thread
1570675715571:T2 end! 
1570675715571:T1 end! 

在T2通知T1继续执行后,T1并不能立即继续执行,而是要等待T2释放object的锁,并重新成功获得锁后,才能继续执行;Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间,除wait()方法可以被唤醒外,另外一个主要区别就是**wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。
**

挂起suspend和继续执行resume线程

不推荐使用suspend方法挂起线程,因为挂起线程会使线程暂停但是并不会施放任何锁资源,此时其他线程想要访问被它暂用的锁时,都会被牵连,导致无法继续运行.直到对对应的线程上进行resume()操作,被挂起的线程才能继续.必须包含在对应的synchronized语句中.

2.3 线程组

  • 后面所创建的线程基于下面这个类
public class ThreadRunnable implements Runnable{
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

线程创建的构造器有一种是线程组的方式.

 //创建一个名为privateGroup线程组
        ThreadGroup group = new ThreadGroup("privateGroup");
        //最后一个参数是为了给创建的线程命名,第一个参数为创建的线程所属的组.
        Thread thread = new Thread(group, new ThreadRunnable(), "t1");
        Thread thread1 = new Thread(group, new ThreadRunnable(), "t2");
        thread.start();
        thread1.start();
        //输出当前线程组线程活动总数
        System.out.println(group.activeCount());
        //打印线程组中所有的线程信息
        group.list();
        /*输出结果
        * 2
	java.lang.ThreadGroup[name=privateGroup,maxpri=10]
    Thread[t1,5,privateGroup]
    Thread[t2,5,privateGroup]
	线程run的结果
        * 
        * */

注:线程组的一个方法stop()可以强制停止该组的所有线程,

2.4 守护线程

所谓守护线程即在后台默默完成一些系统性的服务,比如垃圾回收线程,与之对应的就是用户线程即系统工作的线程,当守护线程要守护的对象不存在时,整个应用程序就会结束.

deamonT.setDaemon(true);设置为守护线程

如下:守护的为main主线程,延时2s后主线程结束,守护线程随后结束,如果不设置守护线程那么主线程结束后,该线程依旧运行

public class Daemon {
    public static class DeamonT extends Thread{
        public void run(){
                while (true){
                    System.out.println("i'm alive");
                    try{
                        Thread.sleep(500);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread deamonT = new DeamonT();
        deamonT.setDaemon(true);
        deamonT.start();
        Thread.sleep(2000);
    }
}

这里需要注意,守护线程要在线程启动前进行设置,否则会爆出异常

2.5 线程的优先级

即线程运行的优先等级,在jdk中内置三个静态等级,等级范围1~10,数字越大等级越高

在线程启动前通过setPriority(int newPriority)方法设置等级

/**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

3. 线程安全和synchronized

关键词synchronized的作用是实现线程,对同步的代码加锁,使得每一次,只能有一个线程进入同步块.

synchronized的三种用法

  • **指定枷锁对象:**对给定对象加锁,进入同步代码块前要获得给定对象锁

给定一个特定对象创建线程,被synchronized包裹的代码会在每次请求运行之前请求instancd的锁

public class Synchronized01 implements Runnable{
    static Synchronized01 instancd = new Synchronized01();
    static int i=0;
    @Override
    public void run() {
        for (int j=0;j<1000;j++) {
            synchronized (instancd){
                i++;
        }
    }
}
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instancd);
        Thread t2 = new Thread(instancd);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
  • 直接作用域实例方法: 相当于对当前实例加锁,进入同步代码前要获得当前实例的锁

与上述方法的不同在于,此方法只会获取创建此线程所使用的的对象,如下为insanced对象,而上面的方法可以指定锁对象.

public class Synchronized01 implements Runnable{
    static Synchronized01 instancd = new Synchronized01();
    static int i=0;
    public synchronized void  instance(){
        for (int j=0;j<1000;j++) {
                i++;
        }
    }
    @Override
    public void run() {
        instance();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instancd);
        Thread t2 = new Thread(instancd);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
  • 直接作用于静态方法:相当于对当前类进行加锁,进入同步代码块前要获取当前类的锁

将对应方法改为静态,会请求获取当前类的锁而不是实例的锁.

public class Synchronized01 implements Runnable{
    static Synchronized01 instancd = new Synchronized01();
    static int i=0;
    public static synchronized void  instance(){
        for (int j=0;j<1000;j++) {
                i++;
        }
    }
    @Override
    public void run() {
        instance();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instancd);
        Thread t2 = new Thread(instancd);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值