《多线程》

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35781178/article/details/88680552

《一》

线程的概念:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

栈:将线程的局部变量放在栈里面,这些局部变量是该线程私有的。
堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,主要存放使用new 操作创建的对象实例。

方法区则用来存放JVM加载的类,常量及静态变量等信息,也是线程共享的。

创建线程有三种方式,分别是:
1.继承Thread 类,重写run方法;
2.实现Runnable 接口的run方法;
3.使用FutureTask方式。

3种创建线程的优缺点
创建方式 优点 缺点
继承thread类 在run()方法内获取当前线程直接使用this 就可以了,不必使用Thread.currentThread()方法


1.java 是单继承,如果继承了thread类,就不能再继承其他类;

2.任何与代码没有分离,当多个线程执行一样的任务时,需要多份任务代码,而Runable则没有这个限制。
3.没有返回值;

实现Runnable接口 多个线程执行一样的任务时,只写一份代码就可以了。实现接口的方式比继承的方式更灵活,也能减少程序之间的耦合度,面向接口也是设计模式的6大原则的核心。 1.没有返回值;
实现Callable接口 返回结果  

总结:
1.使用继承方式的好处是方便传参,你可以再子类里面添加成员变量,通过set 方法设置参数或通过构造函数进行传递。java 单继承,多实现。

2.如果使用Runnable接口,则只能使用主线程里面被声明为final 的变量。

3.callable 可以得到返回值

多线程的作用:

1.发挥多核CPU 的优势

2.防止阻塞

3.便于建模。

start()方法和run()方法的区别:

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

线程通知和等待
java 的object 类是所有类的父类,java 将所有类都需要的方法放到了object 类里面。

1.wait()函数:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事才返回:
 1.其他线程调用了该共享对象的notify()或notifyAll()方法;
 2.其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回

注意:如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时,调用线程会抛出IIIegalMonitorStateException异常。

一个线程如何获取一个共享变量的监视器锁:

1.执行synchronized 同步代码块时,使用该共享变量作为参数。
  Synchronized(共享变量){
  //to do
  }

2.调用该共享变量的方法,并且该方法使用了synchronized修改。
synchronized void add(int a,int b){
  //to do 
  }

虚假唤醒: 一个线程可以从挂起状态变成可以运行状态(唤醒),即使该线程没有被其他线程调用notify(),notifyAll()方法进行通知,或被中断,或等待超时。

synchronized(obj){
  while(条件不满足){
    obj.wait();
  }
}

wait(long timeout)函数:该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms 时间内被其他线程调用该共享变量的notify()或notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout 设置为0 则和wait 方法效果一样,因为wait方法内部就是调用了wait(0)。

注意:如果在调用该函数时,传递了一个负值,则会抛出iiiegalArgumentException异常。

notify()函数: 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程时随机的。

nofifyAll()函数:nofifyAll()方法会唤醒所有在该共享变量上由于调用wait 系列方法而被挂起的线程。

让出CPU执行权的yield 方法

thread 类中有一个静态的yield 方法,当一个线程调用yield方法时,实际上就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。

操作系统是为每个线程分配一个时间片来占有CPU,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度。而当一个线程调用了Thread类的静态方法yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

    当一个线程调用yield 方法时,当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU 执行权。

 总结:sleep 与yileld 方法的区别在于,当线程调用sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而yield 方法,线程只是让出自己剩余的时间片,并没有阻塞挂起,二是处于就绪状态,线程调度器下一次调度时就有可能调到当前线程执行。

线程中断:java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,二是被中断的线程根据中断状态自行处理。

void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法类设置线程A的中断标志为true 并立即返回。设置标志仅仅是设置标志,线程A 实际上并没有中断,它回继续往下执行。如果线程A 因为调用了wait,join, sleep()方法而被阻塞挂起,这时候若线程B 调用线程A 的interrupt()方法,线程A 会在调用这些方法 的 地方抛出interrupedException异常返回。

boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。

Boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。和isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。并且该方法是static方法,可以通过Thread类直接调用。

理解线程上下文切换:在多线程编程中,线程个数一般都大于CPU个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配才哟ing了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换。在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

现场上下文切换时机有:当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

thread local:多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。

同步的措施一般是加锁。当创建一个变量后,每个线程对其进行访问的时候使用ThreadLocal 就可以做这件事情。ThreadLocal 是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个Threadlocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。

Thread 类中有一个threadLocals 和 一个inheritableThreadLocals ,它们都是ThreadLocalMap 类型的变量,而ThreadLocalMap 是一个定制化的HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLoal 的set 或者get时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal 实例里面,二是存放在调用线程的threadLocals 变量里面。threadLocal类型的本地变量存放在具体的线程内部空间中。ThreadLocal 就是一个工具壳,它通过set 方法将value 值存入调用线程的threadLocals 里面并存放起来。通过remove 移除。

sleep 和wait总结:sleep 和wait 都可以用来放弃CPU一定的事件,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 会放弃这个对象的监视器。

总结:每个线程内部都有一个名为threadLocals 的成员变量,该变量的类型为hashMap ,其中key为我们定义的ThreadLocal 变量的this引用,value 则为我们使用set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals 中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后记得调用ThreadLocal 的remove 方法删除对应线程的threadLocals 中的本地变量。

inheritableThreadLocal类: inheritableThreadLocal 继承threadLocal ,提供 了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

《二:并发线程的其他基础知识》

多线程并发编程:

并发:同一个时间内多个任务同时都在执行,并且都没有执行结束。【一个人喂两娃,轮换着喂,表面上两个孩子都在吃饭】

并行:在单位时间内多个任务同时执行。【2个大人喂两个小孩】

总结:多线程编程实践中,线程的个数往往多于CPU的个数,所以一般称多线程并发而不是并行编程。

为什么进行多线程并发编程:多核CPU意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但随着对应系统性能和吞吐量的要求提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

Java 中线程安全问题:

共享资源:该资源被多个线程所持有 [多个线程都可以访问该资源]。

线程安全问题就是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见结果的问题。

注解:当多个线程都只是读取共享资源,而不去修改,是不会存在线程安全问题的。只有当至少一个线程修改共享资源,而其他线程在读取共享资源时,才会有安全问题。

synchronized 关键字 :synchronized 是Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁称为内部锁,也叫监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出一场后或者在同步块内调用了该内置锁资源的wait 系列方法时释放该内置锁。内置锁是排他锁,当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

注解:Java 中的线程是与操作系统的原生线程一 一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,很耗时,而synchronized 会导致上下文切换。

volatile 关键字:[使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。] ,Java 提供了一种弱形式的同步:volatile 。volatile 关键字保证对一个变量的更新对其他线程马上可见。当一个变量被声明喂volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,二是会把值刷回主内存。当其他线程读取该共享变量时,会从主内存重获最新值,而不是使用当前线程的工作内存中的值。

注解:当线程写入了volatile 变量值时就等价于线程退出了synchronized 同步块;读取volatile变量值时就相当于进入了同步块。使用synchronized 锁会阻塞挂起,导致上下文切换和重新调度的开销,volatile 只能保证共享变量的可见性,并不保证原子性操作。

Java 中的CAS 操作:Compare and Swap ,它是jdk 提供的非阻塞原子性操作,通过硬件保证了 更新操作的原子性。JDK 里面的Unsafe 类提供了一系列的CompareAndSwap * 方法。

JDK 中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。

JDK 的Unsafe 类提供了硬件级别的原子性操作, Unsafe 类中的方法都是native 方法,它们使用JNI的方式访问本地C++实现库。

伪共享的产生原因:多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。

避免伪共享:

1.JDK 8之前一般都是通过字节填充的方式来避免该问题,就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一样缓存行中。

2.JDK 8 提供了sun.misc.Contended 注解,用来解决伪共享问题。

公平锁和非公平锁:

公平锁:ReentrantLock reentrantLock = new ReentrantLock(true);

非公平锁:ReentrantLock reentrantLock = new ReentrantLock(true);,如果不传参数,则默认是非公平锁。

注解:在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

独占锁和共享锁:根据锁只能被单个线程持有 还是能被多个线程共同持有,锁分为独占锁和共享锁。

独占锁:保证任何时候都只有一个线程能得到锁,ReentrantLock 就是以独占方式实现的。

共享锁:可以同时由多个线程持有,例如ReadWriteLock 读写锁,它允许一个资源可以被多线程同时进行读操作。

总结:独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,限制了并发行,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。共享锁则是一种乐观锁,它放开了加锁的条件,允许多个线程同时进行读操作。

《三:Java 并发包中ThreadLocalRandom》

Random: 在单线程中每问题,在多线程环境中使用,多线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这回降低并发性能,而ThreadLocalRandom 可以解决这一问题。

public static void main(String[] args) {
    //获取一个随机数生成器
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (int i = 0;i< 10;++i){
        //包含0 不包含5
        System.out.println(random.nextInt(5));
    }
}

注解:ThreadLocalRandom.current(),获取一个随机数生成器。ThreadLocal 通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步,ThreadLocalRandom也是同理。这样就不会存在竞争问题了,也会大大提高并发性能。

《四》

JUC 包提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大的提高。JUC 包中有AtomicInteger,AtomicLong,AtomicBoolean等原子性操作类,它们原理相似,,AtomicLong是原子性递增或递减类,其内部使用Unsafe 来实现。

总结:AtomicLong 通过CAS 提供了非阻塞的原子性操作,相比使用阻塞算法的同步器性能得到进一步的提升,但是在高并发大量线程会同时去竞争更新同一个原子变量,但同时只有一个线程的CAS 操作会成功,这就造成了大量线程竞争失败后会通过无限制循环不断进行自旋尝试CAS的操作,所以浪费CPU资源。java8 通过 LongAdder 用来克服在高并发下使用AtomicLong的缺点。

注解:LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下cell数组就是null)和一个基值变量base,由于cells 占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,惰性加载。当一开始判断cell 数组是null并且并发线程较少时,所有的累加操作都是对base 变量进行的。cell 类型时AtomicLong 的一个改进,用来减少缓存的争用,也就是解决伪共享问题。cell 的构造很简单,其内部维护一个被声明为volatile 的变量,这里声明为volatile 是因为线程操作value变量时没有使用锁,为了保证变量的内存可见性这里将其声明为volatile 的。另外cas 函数通过操纵,保证了当前线程更新时被分配的cell 元素中value 值的原子性。另外,@sun.misc.Contended 修改是为了避免伪共享。

总结:Java8 中的新增的LongAdder 原子性操作类,该类通过内部cells 数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对cells 数组里面的元素进行并行的操作。数组元素cell 使用 @sun.misc.Contended 注解进行修饰,这避免了cells 数组内多个原子变量被放入同一个缓存行,也就是避免了伪共享,这对性能也是一个提升。

并发包中的并发List 只有CopyOnWriteArrayList,CopyOnWriteArrayList 是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)进行的,也称为写时复制策略。每个CopyOnWriteArrayList 对象里面有一个array 数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对array 进行修改。ReentrantLock 是独占锁。

《五》Java 并发包中并发list

并发包中的并发List 只有CopyOnWriteArrayList,CopyOnWriteArrayList 是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数据(快照)上进行的,也就是使用了写时复制策略。每个CopyOnWriteArrayList 对象里面有一个array 数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对array 进行修改。
CopyOnWriteArrayList 中用来添加元素的函数有add(E e),add(int index,E element),addIfAbsent(E e),addAllAbsent(Collection<? extends E> c) 等。

CopyOnWriteArrayList 使用写时复制的策略来保证list 的一致性,而获取-修改-写入散步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个实践只有一个线程能对list 数组进行修改。另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。

《六》Java 并发包中锁原理剖析

locksupport 工具类:jdk 中的rt.jar 包中lock support 是个工具类,他的主要作用就是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。lock support 类与每个使用他的线程都会关联一个许可证,在默认情况下调用lock support 类的方法的线程是不持有许可证的。lock support是使用unsafe 类实现的。

1.void park(Thread thread)方法:如果调用park 方法的线程已经拿到了与lock support 关联的许可证,则调用lock support.park()时会马上返回,否则调用线程会被禁止参与雕成的调度,也就是会被阻塞挂起。

public static void main(String[] args) {
    /**
     * 只有begin 没有end,线程被挂起,因为默认情况下调用线程是不持有许可证的。
     */
    System.out.println("begin support");
    LockSupport.park();
    System.out.println("end support");
}

在其他线程调用unpark()方法并且将当前线程作为参数时,调用park 方法而被阻塞的线程会返回,另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。所以调用park方法时最好也使用循环条件判断方式。

2.void unpark(Thread thread)方法:当一个线程调用unpark 时,如果参数thread线程没有持有thread 与lock support 类关联的许可证,则让thread 线程持有。如果thread 之前因调用park()而被挂起,调用unpark()后,该线程会被唤醒。如果thread 之前没有调用park,则调用unpark 后,再调用park 方法,会立刻返回。

public static void main(String[] args) {
    /**
     * 只有begin 没有end,线程被挂起,因为默认情况下调用线程是不持有许可证的。
     */
    System.out.println("begin support");
    //使当前线程获取到许可证
    LockSupport.unpark(Thread.currentThread());
    //再次调用park方法
    LockSupport.park();
    System.out.println("end support");
}
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("child thread begin park");
            //调用park()方法,挂起自己
            LockSupport.park();
            System.out.println("child thread end park");
        }
    } );
    //启动子线程   主线程休眠1s
    thread.start();
    Thread.sleep( 1000 );
    System.out.println("main thread begin unpark");
    //调用unpark 方法让thread 线程持有许可证,然后park方法返回
    LockSupport.unpark( thread );
}
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("child thread begin park");
            while (!Thread.currentThread().isInterrupted()){
                //调用park()方法,挂起自己,只有被中断才会退出循环
                LockSupport.park();
            }
            System.out.println("child thread end park");
        }
    } );
    //启动子线程   主线程休眠1s
    thread.start();
    Thread.sleep( 1000 );
    System.out.println("main thread begin unpark");
    //中断子线程
    thread.interrupt();
}

3.void parkNannos(long nanos)方法: 和park 方法类似,如果调用park 方法的线程已经拿到了与lock support 关联的许可证,则调用lockSupport.parkNanos(long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos 实践后修改为自动返回。

注:park 方法还支持带有 blocker 参数的方法void park(Object blocker)方法,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。

4.park(Object blocker)方法

5.void parkNanos(Object blocker,long nanos)方法

6.void parkUntil(Object blocker,long deadline)方法

抽象同步队列AQS概述:

AQS -锁的底层支持:abstractQueuedSynchronizer 抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。[AQS 是一个FIFO双向队列,其内部通过节点head 和tail 记录队首和队尾元素,队列元素的类型为node,其中node 中的thread变量用来存放进入AQS队列里面的线程;node 节点内部的shared 用来标记该线程是获取共享资源时被阻塞挂起后放入AQS 队列的,exclusive 用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus 记录当前线程等待状态,可以为cancelled(取消),signal(唤醒),condition(等待),propagate(释放共享资源时需要通知其他节点);prev 记录当前节点的前驱节点,next记录当前节点的后续节点。]

AQS 内部类ConditionObject ,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量。

独占锁ReentrantLock :可重入锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被放入该锁的AQS阻塞队列里面。

获取锁:void lock()方法:当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

void unlock()方法:尝试释放锁,如果当前线程持有该锁,则调用该方法会让线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则抛出IIIegaMonitorStateException异常。

public class ReentrantLockList {

    //线程不安全的list
    private ArrayList<String> array = new ArrayList<>();
    //独占锁
    private volatile ReentrantLock lock = new ReentrantLock(  );
    //添加元素
    public void add(String em){
        lock.lock();
        try{
            array.add( em );
        }finally {
            lock.unlock();
        }
    }
    //删除元素
    public void remove(String em){
        lock.lock();
        try{
            array.remove( em );
        }finally {
            lock.unlock();
        }
    }
    //获取数据
    public String get(int index){
        lock.lock();
        try{
          return   array.get( index );
        }finally {
            lock.unlock();
        }
    }
}

注解:ReentrantLock的底层是使用AQS实现的可冲入独占锁。某时只能有一个线程获取该锁。解决线程安全问题使用ReentrantLock就可以,但是ReentrantLock是独占锁,实际中会有写少读多的场景,ReentrantLock满足不了这个需求。ReentrantReadWriteLock采取读写分离的策略,允许多个线程可以同时获取读锁。
public class ReentrantReadWriteLockList {

    //线程不安全的list
    private ArrayList<String> array = new ArrayList<>();
    //独占锁
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(  );
    private final Lock readLock = lock.readLock();
    private final Lock writeLock =lock.writeLock();
    //添加元素
    public void add(String em){
        writeLock.lock();
        try{
            array.add( em );
        }finally {
            writeLock.unlock();
        }
    }
    //删除元素
    public void remove(String em){
        writeLock.lock();
        try{
            array.remove( em );
        }finally {
            writeLock.unlock();
        }
    }
    //获取数据
    public String get(int index){
        readLock.lock();
        try{
            return   array.get( index );
        }finally {
            readLock.unlock();
        }
    }
}

JDK8中新增的StampedLock 锁:StampedLock 是并发包JDK8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的函数时,会返回一个long 型的变量,该变量代表了锁的状态。其中try 系列获取锁的函数,当获取锁失败后会返回为0的stamp 值,当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp 值。

写锁:writeLock

悲观读锁:readLock

乐观读锁:tryOptimisticRead

《七:Java 并发包中并发队列》 

ConcurrentLinkedQueue :是线程安全的无界非阻塞队列,其底层数据结构使用单项链表实现,对于入队和出队操作使用CAS来实现线程安全。ConcurrentLinkedQueue内部的队列使用单项链表方式实现,其中有两个volatile 类型的Node 节点分别用来存放队列的首尾节点。

1.offer 操作:offer 操作是在队列末尾添加一个元素,如果传递的参数是null则抛出NPE异常,否则由于ConcurrentLinkedQueue是无界队列,该方法一直会返回true。另外,由于使用CAS无阻塞算法,因此该方法不会阻塞挂起调用线程。

Java中如何获取到线程Dump 文件:

死循环,死锁,阻塞,页面打开慢等问题,打线程dump 是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

1.获取到线程的pid ,可以通过使用jps 命令,在Linux 下,可以使用 ps-ef | grep java

2.打印线程堆栈,可以通过使用jstack pid 命令,在Linux 下还可以使用kill -3 pid

3.thread 类提供了一个getStackTrace()方法也可以获取线程堆栈。

生产者消费者模型的作用是什么:

1.通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

2.解耦,解耦意味着生产者和消费者之间的联系少,联系越少越可以独立发展而不需要受到相互的制约。

wiat ,notity ,notifyAll 都必须在同步块中被调用,因为是JDK强制的,因为他们在调用之前必须先获取对象的锁。

synchronized 和ReentrantLock 的区别:

1.synchronized 是关键字;而ReentrantLock 是类,本质区别。

2.它比synchronized 提供了更多更灵活的特性,可以被继承,可以有方法,可以有各种各样的类变量。

3.ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁;ReentrantLock可以获取各种锁的信息;ReentrantLock 可以灵活的实现多路通知。Lock 必须在finally 里面释放。

猜你喜欢

转载自blog.csdn.net/qq_35781178/article/details/88680552