手记

Java——线程回顾汇总:同步/生产者消费者模式/定时调度


  一个进程可以产生多个线程。同多个进程可以共享操作系统的某些资源一样,同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。

      1. 一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。

      2. 一个进程可拥有多个并行的(concurrent)线程。

      3. 一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。

      4. 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。

      5. 线程的启动、中断、消亡,消耗的资源非常少。

线程和进程的区别

      1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。

      2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。

      3. 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。

      4. 多进程: 在操作系统中能同时运行多个任务(程序)。

      5. 多线程: 在同一应用程序中有多个顺序流同时执行。

      6. 线程是进程的一部分,所以线程有的时候被称为轻量级进程。

      7. 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。

      8.  系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。

创建线程的方式

1、 继承Thread类方式的多线程

        • 优势:可以继承其它类,多线程可共享同一个Runnable对象

        • 劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方

        法

2、实现Runnable接口方式的多线程

        • 优势:可以继承其它类,多线程可共享同一个Runnable对象

        • 劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方

        法

3、实现Callable接口

    与实行Runnable相比, Callable功能更强大些

    • 方法不同,可以有返回值,支持泛型的返回值

    • 可以抛出异常

    • 需要借助FutureTask,比如获取返回结果

    Future接口

        可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

        FutrueTask是Futrue接口的唯一的实现类

        FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为

Future得到Callable的返回值

线程控制方法

•  join ()

     阻塞指定线程等到另一个线程完成以后再继续执行

•  sleep ()

    使线程停止运行一段时间,将处于阻塞状态

    如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!

•  yield ()

    让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态

    如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!

•  setDaemon ()

    可以将指定的线程设置成后台线程

    创建后台线程的线程结束时,后台线程也随之消亡

    只能在线程启动之前把它设为后台线程

•  interrupt()

    并没有直接中断线程,而是需要被中断线程自己处理

•  stop()

    结束线程,不推荐使用

线程状态

QQ拼音截图20181210192701.png

QQ拼音截图20181210192723.png

    一个线程对象在它的生命周期内,需要经历5个状态。

新生状态(New)

      用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

就绪状态(Runnable)

      处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:

      1. 新建线程:调用start()方法,进入就绪状态;

      2. 阻塞线程:阻塞解除,进入就绪状态;

      3. 运行线程:调用yield()方法,直接进入就绪状态;

      4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

运行状态(Running)

      在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

阻塞状态(Blocked)

      阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

      1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

      2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。

      3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

      4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

        

        阻塞分为三种:

                BLOCKED 

                被阻塞等待监视器锁定的线程处于此状态。 处于阻塞状态的线程正在等待监视器锁定进入同步块/方法,或者在调用Object.wait后重新输入同步的块/方法。 

                

                WAITING 

                正在等待另一个线程执行特定动作的线程处于此状态。 等待线程的线程状态 由于调用以下方法之一,线程处于等待状态: 

                        Object.wait没有超时 

                        Thread.join没有超时 

                        LockSupport.park 

                

                TIMED_WAITING 

                正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 

                        具有指定等待时间的等待线程的线程状态。 线程处于定时等待状态,因为在指定的正等待时间内调用以下方法之一: 

                        Thread.sleep 

                        Object.wait与超时 

                        Thread.join与超时 

                        LockSupport.parkNanos 

                        LockSupport.parkUntil 

死亡状态(Terminated)

      死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。

      当一个线程进入死亡状态以后,就不能再回到其它状态了。

终止线程的典型方式      

        终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。

public class TestThreadCiycle implements Runnable {

    String name;

    boolean live = true;// 标记变量,表示线程是否可中止;

    public TestThreadCiycle(String name) {

        super();

        this.name = name;

    }

    public void run() {

        int i = 0;

        //当live的值是true时,继续线程体;false则结束循环,继而终止线程体;

        while (live) {

            System.out.println(name + (i++));

        }

    }

    public void terminate() {

        live = false;

    }

 

    public static void main(String[] args) {

        TestThreadCiycle ttc = new TestThreadCiycle("线程A:");

        Thread t1 = new Thread(ttc);// 新生状态

        t1.start();// 就绪状态

        for (int i = 0; i < 100; i++) {

            System.out.println("主线程" + i);

        }

        ttc.terminate();

        System.out.println("ttc stop!");

    }

}

暂停线程执行sleep/yield

      暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:

      1. sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。

      2. yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

线程的联合join()

      理解为插队

        线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。

获取线程基本信息的方法

image.png

线程的优先级

      1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。

      2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。

    Thread.MIN_PRIORITY = 1

        Thread.MAX_PRIORITY = 10

        Thread.NORM_PRIORITY = 5

      3. 使用下列方法获得或设置线程对象的优先级。

         int getPriority();

         void setPriority(int newPriority);

      注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

守护线程/后台线程

            setDaemon ()

            • 可以将指定的线程设置成后台线程

            • 创建后台线程的线程结束时,后台线程也随之消亡

            • 只能在线程启动之前把它设为后台线程

线程同步

     由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。

      由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

synchronized 方法

      通过在方法声明中加入 synchronized关键字来声明,语法如下:

public  synchronized  void accessVal(int newVal);

      synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

synchronized块

      synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。

      Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

      synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:

            synchronized(syncObject) { 

               //允许访问控制的代码 

               }

Lock锁

    JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活

    java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁

定语义。

    ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。

注意:如果同步代码有异常,要将unlock()写入finally语句块

• Lock和synchronized的区别

    1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁

    2.Lock只有代码块锁,synchronized有代码块锁和方法锁

    3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

• 优先使用顺序:

    Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

 

死锁的概念

      “死锁”指的是:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

 

死锁的解决方法

      死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

线程并发协作(生产者/消费者模式)——管程法

      多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

Ø 什么是生产者?

      生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是消费者?

      消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是缓冲区?

      消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

image.png

      缓冲区是实现并发的核心,缓冲区的设置有3个好处:

Ø 实现线程的并发协作

      有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。

Ø 解耦了生产者和消费者

      生产者不需要和消费者直接打交道。

Ø 解决忙闲不均,提高效率

      生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。

线程并发协作总结:

      线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:

      1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

      2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。

      3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

      4. 在生产者消费者问题中,仅有synchronized是不够的。

        · synchronized可阻止并发更新同一个共享资源,实现了同步;

        · synchronized不能用来实现不同线程之间的消息传递(通信)。

      5. 那线程是通过哪些方法来进行消息传递(通信)的呢?

image.png

 6. 以上方法均是java.lang.Object类的方法;

      都只能在同步方法或者同步代码块中使用,否则会抛出异常。

image.png

线程并发协作(生产者/消费者模式)——信号灯法

通过标志位来标记。

image.png

任务定时调度

      通过Timer和Timetask,我们可以实现定时启动某个线程。

java.util.Timer

      在这种实现方式中,Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。

java.util.TimerTask

      TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。

在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。

    在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现。

实际开发中,我们可以使用开源框架quanz,更加方便的实现任务定时调度。实际上,quanz底层原理也就是这个。

JMM中的happens-before规则

image.png

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

比如i++这句代码

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

  这个代码在单线程中运行任何问题,但在多线程中运行就会有问题。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。

  比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

  可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

  最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

  也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

  为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1)通过在总线加LOCK#锁的方式

  2)通过缓存一致性协议

  这2种方式都是硬件层面上提供的方式。

  在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

  但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

  所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

212219343783699.jpg

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before规则

happens-before是JMM最核心的概念,理解happens-before是理解JMM的关键。该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。

按照官方的说法:

当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有 HB 关系,则会产生数据竞争问题。 要想保证操作 B 的线程看到操作 A 的结果(无论 A 和 B 是否在一个线程),那么在 A 和 B 之间必须满足 HB 原则,如果没有,将有可能导致重排序。 当缺少 HB 关系时,就可能出现重排序问题。

两个操作间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before仅仅要求前一个操作对后一个操作可见。appens-before原则和一般意义上的时间先后是不同的。

HB规则

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

锁定规则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。

volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

其中,传递规则至关重要如何熟练的使用传递规则是实现同步的关键。

然后,再换个角度解释 HB:当一个操作 A HB 操作 B,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

同时,如果 操作 B HB 操作 C,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

而实现可见性的原理则是 cache protocol 和 memory barrier。通过缓存一致性协议和内存屏障实现可见性。

double pi = 3.14;   // A

double r = 1.0;     // B

double area = pi * r * r;  // C

  上述存在三个happens-before关系:

    A happens-before B

    B happens-before C

    A happens-before C

  在者三个happens-before关系中2和3是必须的,1是不必要的。因此JMM把happens-before要求禁止的重排序分了下面两类

      1.会改变程序执行结果的重排序

      2.不会改变程序执行结果的重排序

  JMM对这两种不同性质的重排序,采用了不同的策略,如下:

      1.对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序

      2.对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)

volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序——有序性

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

ThreadLocal

 ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

线程共享变量缓存如下:

    Thread.ThreadLocalMap<ThreadLocal, Object>;

        1、Thread: 当前线程,可以通过Thread.currentThread()获取。

        2、ThreadLocal:我们的static ThreadLocal变量。

        3、Object: 当前线程共享变量。

    我们调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前ThreadLocal获取当前线程共享变量Object。

    ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,局部变量只有自己线程看得见,不影响其他线程。

    ThreadLocal 能放一个线程级别的变量,本身能够被多个线程共享使用,并且有能达到线程安全。所以,TreadLocal就是在多线程环境下保证成员变量的安全,常用方法是get/set/initialValue方法。

JDK建议把ThreadLocal定义为private static

public class ThreadLocalTest01 {

    //private static ThreadLocal<Integer> threadLocal = new ThreadLocal<> ();

    //更改初始化值

    /*private static ThreadLocal<Integer> threadLocal = new ThreadLocal<> () {

        protected Integer initialValue() {

            return 200;

        }; 

    };*/

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()-> 200);

    public static void main(String[] args) {

        //获取值

        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());       

        //设置值

        threadLocal.set(99);

        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());

        

        new Thread(new MyRun()).start();

        new Thread(new MyRun()).start();

    }   

    public static  class MyRun implements Runnable{

        public void run() {

            threadLocal.set((int)(Math.random()*99));

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());       

        }

    }

    

}

每个线程自身的数据,更改不会影响其他线程

public class TestThreadLocal {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()-> 1);

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName());

        for(int i=0;i<2;i++) {

            new Thread(new MyRun()).start();

        }

    }   

    public static  class MyRun implements Runnable{

        public MyRun() {

            threadLocal.set(-100);

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    

        }

        public void run() {

            Integer left =threadLocal.get();

            System.out.println(Thread.currentThread().getName()+"得到了-->"+left);     

            threadLocal.set(left -1);

            System.out.println(Thread.currentThread().getName()+"还剩下-->"+threadLocal.get());    

        }

    }

}

结果:

main

main-->-100

main-->-100

Thread-0得到了-->1

Thread-0还剩下-->0

Thread-1得到了-->1

Thread-1还剩下-->0

InheritableThreadLocal

使用ThreadLocal不能继承父线程的ThreadLocal的内容,而使用InheritableThreadLocal时可以做到的,这就可以很好的在父子线程之间传递数据了。

对比如下:

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        threadLocal.set(2);

        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());   

        

        new Thread(()->{

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());   

            threadLocal.set(200);

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());   

        }) .start();

    }

main-->2

Thread-0-->null

Thread-0-->200

    private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        threadLocal.set(2);

        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());  

        

        new Thread(()->{

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());   

            threadLocal.set(200);

            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());   

        }) .start();

    }

main-->2

Thread-0-->2

Thread-0-->200

线程池

线程池能够对线程进行统一分配,调优和监控: 

- 降低资源消耗(线程无限制地创建,然后使用完毕后销毁) 

- 提高响应速度(减少了创建新线程的时间) 

- 提高线程的可管理性:避免线程无限制创建、从而销耗系统资源,降低系统稳定性,甚至内存溢出或者CPU耗尽。

线程池的应用场合

    • 需要大量线程,并且完成任务的时间端

    • 对性能要求苛刻

    • 接受突发性的大量请求

参考详见 https://blog.csdn.net/programmer_at/article/details/79799267

可重入锁

Lock就是可重入锁的实现

Java多线程的wait()方法和notify()方法

        这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出IllegalMonitorStateException异常,所以这两个方法必须在同步块代码里面调用。

wait():阻塞当前线程

notify():唤起被wait()阻塞的线程

所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。

public class ReLockTest {

    Lock lock = new Lock();

    public void a() throws InterruptedException {

        lock.lock();

        doSomething();

        lock.unlock();

    }

    //不可重入 

    public void doSomething() throws InterruptedException {

        lock.lock();

        //...................

        lock.unlock();

    }

    public static void main(String[] args) throws InterruptedException {

        ReLockTest test = new ReLockTest();

        test.a();

        test.doSomething();

    }

}

// 不可重入锁  不能连续使用锁

class Lock{

    //是否占用

    private boolean isLocked = false;

    //使用锁

    public synchronized void lock() throws InterruptedException {

        while(isLocked) {

            wait();

        }

        isLocked = true;

    }

    //释放锁

    public synchronized void unlock() {

        isLocked = false;

        notify();

    }

}

当前线程执行a()方法首先获取lock,接下来执行doSomething()方法就无法执行doSomething()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。

可重入锁

所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。

public class ReLockTest {

    ReLock lock = new ReLock();

    public void a() throws InterruptedException {

        lock.lock();

        System.out.println(lock.getHoldCount());

        doSomething();

        lock.unlock();

        System.out.println(lock.getHoldCount());

    }

    //不可重入

    public void doSomething() throws InterruptedException {

        lock.lock();

        System.out.println(lock.getHoldCount());

        //...................

        lock.unlock();

        System.out.println(lock.getHoldCount());

    }

    public static void main(String[] args) throws InterruptedException {

        ReLockTest test = new ReLockTest();

        test.a();           

        Thread.sleep(1000);     

        System.out.println(test.lock.getHoldCount());

    }

}

// 可重入锁 + 计数器

class ReLock{

    //是否占用

    private boolean isLocked = false;

    private Thread lockedBy = null; //存储线程

    private int holdCount = 0;

    //使用锁

    public synchronized void lock() throws InterruptedException {

        Thread t = Thread.currentThread();

        while(isLocked && lockedBy != t) {

            wait();

        }

        

        isLocked = true;

        lockedBy = t;

        holdCount ++;

    }

    //释放锁

    public synchronized void unlock() {

        if(Thread.currentThread() == lockedBy) {

            holdCount --;

            if(holdCount ==0) {

                isLocked = false;

                notify();

                lockedBy = null;

            }       

        }       

    }

    public int getHoldCount() {

        return holdCount;

    }

}

第一个线程调用a()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量holdCount并记录lockBy为第一个线程。接着第一个线程进入doSomething()方法,由于同一线程,所以不会进入while而挂起,接着增量holdCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将holdCount递减为0,才将标记为isLocked设置为false。

可重入锁的概念和设计思想大体如此,Java中的可重入锁ReentrantLock设计思路也是这样

©著作权归作者所有:来自51CTO博客作者huingsn的原创作品,如需转载,请注明出处,否则将追究法律责任


0人推荐
随时随地看视频
慕课网APP