【多线程】多线程-简单

0.概念

进程:资源分配的基本单位

线程:处理及任务调度和实行的基本单位

进程是系统进行资源分配和资源调度的独立单位,每一个进程都有它自己的内存空间和系统资源。进程实现多处理机环境下的进程调度、分配、切换时,都有较大的时间和空间开销,为了提高效率,较少处理机的空转时间和调度切换时间,用线程取代进程的调度功能。

一个进程包括多个线程,进程的内存空间是共享的,每个线程都可以使用这些共享内存。一个线程使用某些共享内存时,其他线程必须等待。

1.Thread类主要方法

执行
start()和run():
  • start()方法在java.lang.Thread类中定义;run()方法在java.lang.Runnable接口中定义,必须在实现类中重写。
  • 当程序调用start()方法时,会创建一个新线程,然后执行run()方法。但是如果我们直接调用run()方法,则不会创建新的线程,run()方法将作为当前调用线程本身的常规方法调用执行,并且不会发生多线程。
  • start()方法不能多次调用,否则抛出java.lang.IllegalStateException;而,run()方法可以进行多次调用,因为它只是一种正常的方法调用。
中断
interrupted()

判断是否中断,清除中断标志位

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
isInterrupted :

本地方法,判断是否中断,是否清除标志位

private native boolean isInterrupted(boolean ClearInterrupted);
interrupt() :

中断线程,添加中断标志位

public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

线程间协作
join() :

如果线程A执行了threadB.join(),其含义为:当前线程A会等待线程B终止后再继续执行。

public final void join() throws InterruptedException {
        join(0);
    }

join(long)

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}


睡眠
sleep(long) :

Thread的静态方法,使当前线程按指定时间休眠

public static native void sleep(long millis) throws InterruptedException;
Thread的sleep()与Object.wait()区别:
  • sleep()方法是Thread的静态本地方法;wait()方法是Object类的实例本地方法。
  • waite()和notify()因为会对对象的“锁标志”进行操作,所以wait(),notify(),notifyAll()方法必须在同步方法或同步代码块中调用,也就是必须获取到对象锁;而sleep()方法可以在任何地方使用。
  • 当一个线程执行到wait()方法时,它会进入到当前对象的线程等待池,同时会释放对象的锁,使其他线程能够访问;可以通过notify,notifyAll方法来唤醒等待的线程而sleep()只是会让调用线程进入睡眠状态,让出CPU资源给其他线程,并不会释放掉对象锁。等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。

【如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B因被调用Object#wait(),Object#wait(long, int), 或者线程本身的join(), join(long),sleep()处于阻塞状态中,则线程B会立刻抛出InterruptedException,而且线程的阻塞状态将会被清除。在catch() {} 中直接return即可安全地结束线程。】

yield():

yield()方法是停止当前线程,与sleep()方法交出处理器资源让所有线程去竞争不同的是,yield()方法让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。(Java通过int型的priority属性控制优先级,1~10)

public static native void yield();

2.ThreadLocal

2.1ThreadLocal与Thread

  • 每个Thread线程内部都有一个Map;
  • Map里面存储线程本地对象(key:即ThreadLocal)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值

2.2ThreadLocal类几个核心方法:

public T get()
public void set(T value)
public void remove()
protected T initialValue() { }
  • get()方法用于获取当前线程的变量副本

  • set()方法用户保存当前线程的变量副本

  • initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,用于为当前线程初始变量副本值

  • remove()方法移除当前线程的副本变量

  1. 首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,key为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
  2. 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
  3. 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

2.3ThreadLocal的应用场景

最常见的ThreadLocal的使用场景为用来解决数据库连接、Session管理等。

Session管理:
 1 private static final ThreadLocal threadSession = new ThreadLocal();
 2  
 3 public static Session getSession() throws InfrastructureException {
 4     Session s = (Session) threadSession.get();
 5     try {
 6         if (s == null) {
 7             s = getSessionFactory().openSession();
 8             threadSession.set(s);
 9         }
10     } catch (HibernateException ex) {
11         throw new InfrastructureException(ex);
12     }
13     return s;
14 }

可以看到,在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际上放到当前线程ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap,而threadSession作为这个值的Key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际上是先取得当前线程的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量。显然,其它线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。

数据库连接:
public class DataSourceHolder {
    /**
     * 线程本地环境
     */
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

    /**
     * 设置数据源:每次切换数据源都是将本地线程ThreadLocal类型的dataSources作为key,String类型的dataSource参数作为value存入ThreadLocalMap类型的本地线程map集合中
     */
    public static void setDataSource(String dataSource) {
        dataSources.set(dataSource);
    }

    /**
     * 获取数据源:获取数据源即用当前本地线程map中的key:dataSources来找到对应的value,即数据源
     */
    public static String getDataSource() {
        return (String) dataSources.get();
    }

    /**
     * 清除数据源:删除本地线程集合中的键值对
     */
    public static void clearDataSource() {
        dataSources.remove();
    }
}

当对同一个线程调用的多个方法中共享了某一个变量(如上面的session和数据源),可以采用ThreadLocal。

ThreadLocal并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果。ThreadLocal最大的用处是用来把实例变量共享成全局变量,在线程的任何方法中都可以访问到该实例变量

ThreadLocal类型变量为何声明为静态static?

Java中每个线程都有与之关联的Thread对象,Thread对象中有一个ThreadLocal.ThreadLocalMap类型的成员变量,该变量是一个Hash表,所以每个线程都单独维护这样一个Hash表,当ThreadLocal类型对象调用 set()方法时,这个set()方法会使用当前线程维护的Hash表,把自己(ThreadLocal)作为Key,相应的值作为value插入到Hash表中。由于每个线程维护的Hash表是独立的,因此在不同的Hash表中,key值即使相同也是没问题的

如果把ThreadLocal对象声明为非静态的,则当包含ThreadLocal类声明的类去产生一个实例时,都会产生一个ThreadLocal的新对象,这是毫无意义的,只是增加了内存消耗

3.Java内存模型(JMM)

3.1模型结构

CPU的处理速度和主存的读写速度不是一个量级的(CPU的处理速度快很多),为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。

在这里插入图片描述

图中的线程A和线程B之间要完成通信的话,要经历如下两步:

  • 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;
  • 线程B从主存中读取最新的共享变量。
3.2主内存与工作内存关系

所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本。

线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。

在这里插入图片描述

3.3内存间交互操作

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。

在这里插入图片描述

  • 1、lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  • 2、unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • 3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
  • 4、load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本;
  • 5、use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  • 6、assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • 7、store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
  • 8、write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
3.4内存模型三大特性
2.4.1原子性

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
  counter++这并不是一个原子操作,包含了三个步骤:
    1.读取变量counter的值;
    2.对counter加一;
    3.将新值赋值给变量counter。

3.4.2可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
  主要有三种实现可见性的方式:

  • volatile,通过在指令中添加lock指令,以实现内存可见性。
  • synchronized,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
3.4.3有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
  volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
  synchronized 关键字同样可以保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

3.5重排序

编写的程序都要经过优化后(编译器和处理器会对我们的程序进行优化以提高运行效率)才会被运行,优化分为很多种,其中有一种优化叫做重排序,重排序需要遵守as-if-serial规则和happens-before规则。重排序的目的是为了性能

3.5.1 重排序分类

一般重排序可以分为如下三种:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
3.5.2 重排序过程

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3M1aE0ER-1606825039586)(…/图片库/20201129155434929.png)]
  1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是DCL(双重检验锁)问题。
  针对编译器重排序,Java内存模型(JMM)的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

3.5.3数据依赖性

编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。所以有数据依赖性的语句不能进行重排序。

3.6 as-if-serial规则和happens-before规则
3.6.1 as-if-serial规则

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高性能),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
  为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

3.6.2happens-before规则
  • 1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。尽管两个操作在不同的线程中执行。
  • 2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

具体的happens-before规则:

  • 1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 2、监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • 3、volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • 4、start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • 5、join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 6、程序中断规则:对线程interrupt()方法的调用先行于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。(意思就是先interrupt()中断线程,之后才可以interrupted()判断是否中断)
  • 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
3.6.3as-if-serial规则和happens-before规则的区别
  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

4.并发关键字

4.1 synchronized

synchronized最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性

Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

synchronized可使用在代码块和方法中,根据synchronized用的位置可以有这些使用场景:

在这里插入图片描述

4.1.1 synchronized修饰方法和同步代码块的区别

synchronized修饰实例方法或静态方法都是通过标识ACC_SYNCHRONIZED实现同步。而同步代码块是是采用monitorenter、monitorexit两个指令来实现同步。

在这里插入图片描述

在这里插入图片描述

4.1.2 底层如何实现?

synchronized修饰方法,实现同步是隐式的。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如有,则需要先获取监视器锁,然后才开始执行方法。方法执行之后再释放监视器锁。在线程执行方法的时候,有另外线程也来请求执行该方法,会因为无法获取监视器锁而被阻断。

synchronized修饰同步代码块是是采用monitorenter、monitorexit两个指令来实现同步。在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0的时候,锁就会被释放。如果获取对象锁失败,那当前线程就要阻塞等待。直到对象锁被另外一个线程释放为止。monitorenter、monitorexit这两个字节码指令都需要一个reference类型的参数来明确要锁定和解锁的对象。例如使用synchronized (this)和synchronized (test.class)。

4.1.3 synchronized分别修饰在实例方法和静态方法时,多线程并发时会竞争锁

synchronized使每个线程依次排队操作共享变量,因为线程执行数据操作时使用了同步代码块,这样就能保证每个线程所获得共享变量的值都是当前最新的值,如果不使用同步的话,就可能会出现A线程累加后,而B线程做累加操作有可能是使用原来的就值,即“脏值”,导致最终的计算结果不正确。而使用Synchronized就可保证每个线程都是操作的最新值。

在这里插入图片描述

执行结果:表明多线程之间同步成功,每个线程读取到的count都是当前最新的数据。

在这里插入图片描述

如果此时将count3改为count2,多线程并发时会竞争锁,就会导致计算结果错误。

在这里插入图片描述

在这里插入图片描述

4.2 volatile

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

volatile可见性实现原理
  1. 在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。

  2. Lock前缀的指令会引起处理器缓存写回内存;

  3. 一个处理器的缓存回写到内存会导致其他处理器该内存地址缓存的数据无效;

  4. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

参考:

Java并发编程基础知识

让你彻底理解Synchronized

Thread Local

synchronized修饰代码块与方法的区别

====================================

说一下为什么使用wait()方法时,一般是需要while循环而不是if?

​ while会一直执行循环,直到条件满足,执行条件才会继续往下执行。if只会执行一次判断条件。我们知道用notify() 和notifyAll()可以唤醒线程,一般我们常用的是notifyAll(),因为notify(),只会随机唤醒一个睡眠线程,并不一定是我们想要唤醒的线程。如果使用的是notifyAll(),唤醒所有的线程,那你怎么知道他想唤醒的是某个正在等待的wait()线程呢,如果用while()方法,就会再次判断条件是不是成立,满足执行条件了,就会接着执行,而if会直接唤醒wait()方法,继续往下执行,根本不管这个notifyAll()是不是想唤醒的是自己还是别人,可能此时if的条件根本没成立。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值