Java并发(一):线程基本点详解

    xiaoxiao2026-02-27  9

    目录

    JVM内存模型   1)原子性    2)可见性    3)有序性voliate线程Java线程Synchronized的作用与原理ReentrantLockThreadLocal

    Java内存模型(Java Memory Model,JMM)        Java内存模型本身只是一种抽象的概念,它声明了一系列规则或规范(主要是声明了主内存与工作内存交互的一些细节),来解决Java在硬件层面上内存访问的差异,也是Java得以跨平台的原因之一。   从内存模型的角度出发,Java中内存可分为主内存和线程工作内存两种,所有变量都存储在主内存中,而线程工作内存主要存储线程私有数据。 主内存是共享内存区域,所有线程都可以访问,但线程并不能直操作主内存中的变量,线程如果想操作主内中的变量,需要先将变量从主内存中拷贝到自己的工作内存中,操作完成后再将变量刷回主内存,只有当数据刷回主内存后,最新的结果对其它线程才是可见的(对于volitile变量同样如此),也就说线程之间的通信(这里指可见性)都必须通过主内存来完成。比如下图   注:这里的主内存与工作内存与堆、栈并不是同一层次上的划分,出发角度不一样,前者是为了解决内存访问差异而划分的,后者代表程序运行时内存的划分。但从变量存储的区域上来看,主内存可对应理解成堆,存储对象但不包括存储局部变量及方法参数,而工作内存则可以对应理解成栈,存储线程局部变量及方法参数等。

    Java内存模型的特征   可见性:可见性是指当线程修改共享变量时,其它线程何时可以看到修改后的信息。上面已经说过,只有当数据刷回主内存之后,对其它线程才是可见的。所以这种可见性不一定是及时的,线程A对共享变量的修改会先反映在自己的工作内存中,然后再写回主内存中,如果在修改完但还没有回写的情况下,线程B去获取此时得到的仍然是旧值。针对这种情况,Java提供了volatile关键字,来保证并发修改下变量的可见性,关于volatile稍后说明。   原子性:表示对共享数据某个操作的原子性,由于整个操作的原子性,所以在保证原子性的同时也保证了可见性。java中可以简单的通过synchronized、lock等来达到,synchronized也稍后说明。   有序性:出于优化的考虑,JVM有可能会对指令进行重排序,所以指令的执行顺序并不一定等于代码的编写顺序,但JVM保证最终逻辑上的有序性。比如int a = 3; int b = 4; int c = a + b;  执行时JVM并不保证a一定比b先定义,但从逻辑上会保证,c一定在a,b定义之后才会执行。对于被volatile修饰的变量,jvm不会对其进行重排序。

    voliate关键字的作用   对volatile变量的操作同样也遵从内存间的交互规范,与普通变量的区别在于:

    对volatile变量的修改会保证及时(同时)反映到线程工作内存和主内存中,而且在每次使用volatile变量前线程都会先从主内存中获取最新值。这点就保证了对volatile变量修改的及时可见。除可见性外,对于volatile变量jvm不会进行指令重排,指令的执行顺序和程序中的顺序严格一致,如volatile int a =3; int b = 4;JVM会保证a在b之前定义。再比如在JDK1.5之前双重check的懒汉式单例也需要用到volatile来确保对象可以正常使用,原因是,对象创建一般会经历内存分配—>初始化—>返回对象引用等一系列操作。但由于对象引用在内存分配之后就已经确定了,与对象是否初始化并没有逻辑上的关系,所以如果指令重排的话,对象就有可能在尚未初始化完成就提前返回了,即内存分配—>返回对象引用—>初始化,使用的时候,就有可能出现问题。volatile虽然保证对变量修改的及时可见,但并不能保证操作的原子性。 /** 并发情况下,及时获取对该值的修改 */ volatile boolean isStop = false; public void execute(){ while(isStop){ ... } } public void toStop(){ isStop = true; }

    线程简介   线程可以分为内核线程(KLT)和用户线程(UT)两种,内核线程由操作系统分配,程序一般不直接调用,而是通过调用系统为内核线程提供的接口—轻量级进程(LWP),去调用内核线程,轻量级进程与内核线程是一对一的关系,每个轻量级进程都是一个独立的调度单元。Java线程模型就是基于操作系统原生线程模型来实现的,在window和unix下,可以理解为下图:  用户线程建立在用户空间的线程库上,创建、销毁、同步、调度等操作都在用户态中完成,不需要内核参与,因此它的操作相对来更快速而且消耗更低,但难点在于由于没有内核线程的支持,需要手动切换、调度等操作,实现起来非常复杂,目前基于纯用户线程的设计已经很少用了,某些情况下两者会配合使用。  Java线程简介 线程状态:java线程主要有5种状态new、runnable、blocked、wait/timed_waiting(有限wait)、terminated(执行完毕),通过getState()可获取当前状态。 优先级:优先级从1-10(默认5),新线程优先级默认继承于父线程。 这里的优先级只是Java中的划分,并不对应底层系统的优先级。

    守护线程:守护线程在后台运行,当程序中全是守护线程时,jvm会自动退出。在守护线程中应该永远不要去访问如文件、数据库资源这些操作,因为守护线程很可能在任何地方被中断。

    sleep(millis):让当前线程休眠一段时间,休眠时间是由参数设定,线程状态由runnable转为timed_waiting,休眠期间释放cpu资源但不释放监视器。在时间到达或线程被中断前,该线程都不会再被调起。在waiting期间如果该线程被任何其它线程中断,则该方法抛出异常,并且清除中断标记。 wait()/wait(millis)、notify()/notifyAll():线程状态由runnable转为waiting/timed_waiting,与sleep()的区别在于它等待时间到达或其它线程notify,且在waiting期间释放cpu资源释放锁,同样受线程中断影响。这两组方法位于根类Object中,两者配合实现一种等待—唤醒的线程协作方式,常用于生产—消费者模式下。

    join()/join(millis):等待被调用线程中止,当前线程状态由runnable转为waiting/timed_waiting,所以waiting过程中同样受中断影响。如下:f2等待f1执行完成,main线程又等待f2执行完成。

    public static void main(String[] args) throws InterruptedException { Thread f1 = new Thread(()->{ try { Thread.sleep(10000); System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) {} }); Thread f2 = new Thread(()->{ try { f1.join(); System.out.println("f1[end], f2[start]"); } catch (InterruptedException e) {} }); f1.start(); f2.start(); f2.join(); System.out.println("over"); }

     yield():使正在运行的线程释放cpu资源重新回到可执行状态,线程状态由runnable(正在执行)到 runnable(可执行),所以线程有可能马上又被执行,由于不会进入wait状态,所以yield()方法并不受线程中断的影响。此外yield()方法只能使同优先级或者高优先级的线程得到运行的机会。 interrupt():中断线程,本身只是设置线程的中断标记而已,但中断会使处于waiting期间的线程抛出一个中断异常InterruptedException,原因是由于线程处于waiting期间,无法被调起执行,不能自己检测中断,所以JVM会通过抛出异常的方式来唤醒线程。通过中断/处理中断异常/检测中断状态也是一种比较常见的线程间通信的协作方式。stop():终止线程会释放它已经锁定的所有监视器,很容易造成的问题就是数据不一致性。这就相当于一个DB事务中途退出,对于DB而言事务中途退出会导致事务全部回滚,这没什么问题,但在Java程序中,原子性操作的中途退出并没有回滚一说,而是会产生并发问题,所以数据不一致性的问题就很可能产生。即使要终止线程也可以通过上方说的中断方式,来安全退出。线程挂起suspend()与恢复resume():这两个方法在Java中被标记为过时的不再使用的方法,原因是因为通过suspend()挂起的线程,不会释放锁,直到在其它线程中调用resume()恢复,且继续运行完成才会释放锁。因此有可能造成两种情况:一就是如果调用resume()的线程也需要获取相同锁,由于原线程并未释放,那么就会造成死锁。二如果resume()在suspend()之前被执行,则原线程永远也无法被恢复。与wait()/notify()的区别在于情况一,wait会释放锁所以不会造成死锁情况,情况二的话wait也不可避免。

    public void testDeadlock() throws InterruptedException{ final Object lock = new Object(); Thread t = new Thread(()->{ synchronized (lock) { //lock try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("----t.running"); } }); Thread suspend = new Thread(()->{ t.suspend(); System.out.println("----t.suspend"); }); Thread resume = new Thread(()->{ synchronized (lock) { t.resume(); System.out.println("----t.resume"); } }); t.start(); Thread.sleep(100); suspend.start(); //suspend Thread.sleep(100); resume.start(); //resume t.join(); suspend.join(); resume.join(); System.out.println("----over"); }

    线程异常处理:除了对线程内部的代码进行try...catch外,也可以设置异常处理程序

    @Test public void testUncaughtExceptionHandler() throws InterruptedException{ Thread th = new Thread(()->{ int i = 1/0; }); th.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("由于没有捕获异常,所以执行自定义的处理方式" + e.getMessage()); } }); th.start(); }

    Synchronized块的作用与原理   synchronized块可以保证操作的原子性和可见性,synchronized底层通过监视器(或内置锁)实现,监视器在jvm中的模型可分为:入口区、持有者、等待区三部分。

     

      synchronized块的进入和退出分别对应指令monitorenter、monitorexit,且synchronized同步块对同一线程是可重入的,重入通过为内置锁关联一个计数器和持有者线程来实现,计数器为0时即表示监视器未被任何占用。当某线程请求获取到一个未被占用的锁时,JVM将记录锁的持有者,并计数+1,如果同一线程再次请求获取这个锁,计数器依次+1;持有线程每退出一个监视块时,计数器-1,当计数器为0时,则会释放锁。即每执行一个monitorenter对应计数+1,monitorexit对应计数-1。

    当一个新的线程想要获取该监听器,但该监视器已被其它线程占用时,新线程将会进入入口区。持有者代表当前持有监视器的线程,持有者线程可以多次获取监视器而不会被自己阻塞。等待区是指已进入监视区域但中途由于某种原因(比如等待I/O操作、等待队列消费或生产)而放弃持有监视器的线程。 

    ReentrantLock重入锁   reentrantLock与synchronized很相似,在JDK1.5之前,前者性能更好,1.6起两者性能就差不多了,甚至在往后的JVM会更倾向于synchronized进行改进,所以建议在用synchronized能解决的情况下就使用synchronized。   目前来看,两者主要是表示形式及功能丰富上存在一些差异(reentrantLock以api的方式进行调用,lock()、unlock()通常使用配合 try...finally使用,后者以原生层面上的互斥锁表现),相对原生的synchronized,reentrantLock提供了一些更高级的功能,如获取可中断,设置非/公平锁、条件等(synchronized 是一个非公平性锁)。

    如下是JDK中自带的一个经典使用示例(生产者-消费者模式)

    获取可中断:指在获取锁的过程中,可以响应中断操作。如lockInterruptibly()、 tryLock(long timeout,TimeUnit unit)公平锁:通过new ReentrantLock(true)设置,多线程并发情况下,公平锁倾向于优先将锁分配给等待时间最长的线程。但对于无参的tryLock()公平锁不起作用,只要当前锁可用就被进行分配,而不管其它线程的等待。多个条件Condition :Condition配合lock一起使用,来实现多条件的锁与唤醒操作。其实它就相当于Object中的wait/notify,只不过使用wait()/notify()时要求当前线程必须先持有对象锁,而condition的使用则没有这个限制,所以更加灵活。 final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } }

     

    转载请注明原文地址: https://ju.6miu.com/read-1307426.html
    最新回复(0)