【JAVA】多线程进阶

文章目录

        • 1.锁的种类
        • 2.synchronized
          • ⑴原理
          • ⑵注意事项
        • 3.ReentrantLock
          • ⑴概述
          • ⑵锁的基本概念
          • ⑶ReentrantLock和Synchronized对比
          • ⑷ReentrantLock的用法
        • 4.ConcurrentHashMap
          • ⑴实现原理
          • ⑵与hashtable比较
        • 5.ThreadLocal
          • 1.原理
          • 2.注意事项
          • 3.ThreadLocalMap为什么设计成弱引用
          • 4.InheritableThreadLocal
        • 6.volatile
          • ⑴目的
          • ⑵原理
          • ⑶volatile和synchronized的对比
          • ⑷volatile的非原子性
          • ⑸指令重排序
          • ⑹使用场景
        • 7.CyclicBarrier
        • 8.CountDownLatch
        • 9.Semaphore
        • 10.atmoic
        • 11.LockSupport
          • ⑴构造函数
          • ⑵它是不可重入的
          • ⑶主要用途
          • ⑷正确用法
          • ⑸与wait和notify的区别
        • 12.Exchanger
        • 13.ReentrantReadWriteLock
          • ⑴概述
          • ⑵特点
          • ⑶升降级
        • 14.线程池
          • ⑴为什么使用线程池
          • ⑵构造函数
          • ⑶使用原理
          • ⑷常用方法
          • ⑸常见线程池
            • ①可缓存线程池CachedThreadPool
            • ②定长线程池FixedThreadPool()
            • ③定时线程池ScheduledThreadPool()
            • ④单线程化的线程池SingleThreadExecutor()

1.锁的种类

  • 自旋锁 ,自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
  • 阻塞锁 被阻塞的线程,不会争夺锁。
  • 可重入锁 多次进入改锁的域
  • 读写锁
  • 互斥锁 锁本身就是互斥的
  • 悲观锁 不相信,这里会是安全的,必须全部上锁
  • 乐观锁 相信,这里是安全的。
  • 公平锁 有优先级的锁
  • 非公平锁 无优先级的锁
  • 偏向锁 无竞争不锁,有竞争挂起,转为轻量锁
  • 对象锁 锁住对象
  • 线程锁
  • 锁粗化 多锁变成一个,自己处理
  • 轻量级锁 CAS 实现
  • 锁消除 偏向锁就是锁消除的一种
  • 锁膨胀 jvm实现,锁粗化
  • 信号量 使用阻塞锁 实现的一种策略
  • 排它锁:X锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

2.synchronized

⑴原理
  • 代码块同步
    代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
    根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

  • 方法同步
    相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上和代码块同步没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

⑵注意事项
  • synchronized取得的都是对象锁
  • A线程持有object对象的Lock锁,B线程可以调用object对象的非synchronized方法,但不可以调用object任何的synchronized方法
  • synchronized是可重入锁
  • 出现异常,锁自动释放
  • static方法加synchronized是属于类的,非static方法是属于对象的

3.ReentrantLock

⑴概述

首先我们看两点Synchronized的局限性:

  • 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞
  • 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

JDK1.5之后发布,加入了Doug Lea实现的concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。

⑵锁的基本概念
  • 可重入锁
    如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁。也可以这样说:同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
  • 可中断锁
    如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
  • 公平锁
    先到先得。
⑶ReentrantLock和Synchronized对比
  • 相同点
    • 都是可重入锁
  • 不同点
    • synchronized是关键字,reentrantLock是类
    • synchronized是通过JVM的字节码实现,reentrantLock通过CAS实现
    • synchronized的加锁和释放锁是自动的,ReetrantLock需要手动加锁和释放锁
    • synchronized是不可中断的,ReetrantLock可中断的
    • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁
    • ReetrantLock的tryLock可以设置超时机制
⑷ReentrantLock的用法
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();  // 和synchronized的wait/notify一个意思

一定要在finally里面释放锁

4.ConcurrentHashMap

⑴实现原理
  • JDK1.8之前
    使用的是分段锁的概念,把一个大的MAP拆分成几个类似hashtable结构(Segment),使用可重入锁ReentrantLock,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中,只有在同一个分段内才存在竞态关系。试想,原来只能一个线程进入,现在却能同时16个(默认是分16段)写线程进入(写线程才需要锁定,而读线程并不锁定,只有在求size等操作时才需要锁定整个表),并发性的提升是显而易见的。
    ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性
  • JDK1.8舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。另外,底层数据结构改变为采用数组+链表/红黑树的数据形式。
⑵与hashtable比较
  • ⑴ HashTable的线程安全使用的是一个单独的全部Map范围的锁,ConcurrentHashMap抛弃了HashTable的单锁机制,使用了锁分离技术,使得多个修改操作能够并发进行,只有进行SIZE()操作时ConcurrentHashMap会锁住整张表。
  • ⑵ HashTable的put和get方法都是同步方法, 而ConcurrentHashMap的get方法多数情况都不用锁,put方法需要锁。
  • ⑶但是ConcurrentHashMap不能替代HashTable,因为两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。

5.ThreadLocal

1.原理

每个线程对象都有一个ThreadLocalMap类(类似于Map结构)的成员变量,KEY就是设置的当前的ThreadLocal这个对象,值就是对应的VALUE。

2.注意事项
  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,尤其是线程池线程会出现复用很容易出现业务问题。
3.ThreadLocalMap为什么设计成弱引用

设计成弱引用的目的是为了更好地对ThreadLocal进行回收,当我们在代码中将ThreadLocal的强引用置为null后,这时候Entry中的ThreadLocal理应被回收了,但是如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,这就是将其设置成弱引用的目的。

4.InheritableThreadLocal

用InheritableThreadLocal可以让子线程从父线程取得值,也可以进行更改,但是需要注意:当子线程取得值的同时,主线程将
InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。

6.volatile

⑴目的

保证变量内存可见性,防止局部重排序

⑵原理

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
①它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
②它会强制将对缓存的修改操作立即写入主存;
③如果是写操作,它会导致其他CPU中对应的缓存行无效

⑶volatile和synchronized的对比
  • volatile只能修饰变量,性能好。synchronized可以修饰代码块,方法。
  • volatile不会出现阻塞,synchronized会出现阻塞
  • volatile保持数据可见性,不支持原子性,synchronized保证原子性,也间接保证可见性
  • volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程访问资源的同步性
⑷volatile的非原子性

先看四条语句:

x = 10;         //语句1   原子性
y = x;         //语句2    非原子性
x++;           //语句3    非原子性
x = x + 1;     //语句4    非原子性

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
我们看一个例子:

public class Test {
    public volatile int inc = 0;
 
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

这里的输出并非是10000而是一个小于10000的数。

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

⑸指令重排序

JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑

先看下面的代码:

线程A:
content = initContent();    //(1)
isInit = true;              //(2)
线程B
while (isInit) {            //(3)
    content.operation();    //(4)
}

对于线程A,代码(1)和代码(2)是不存在数据依赖性的,尽管代码(3)依赖于代码(2)的结果,但是由于代码(2)和代码(3)处于不同的线程之间,所以JVM可以不考虑线程B而对线程A中的代码(1)和代码(2)进行重排序,那么假设线程A中被重排序为如下顺序:

线程A:
isInit = true;              //(2)
content = initContent();    //(1)

对于线程B,则可能在执行代码(4)时,content并没有被初始化,而造成程序错误。那么应该如何保证绝对的代码(2) happens-before 代码(3)呢?没错,仍然可以使用volatile关键字。

volatile关键字除了之前提到的保证变量的内存可见性之外,另外一个重要的作用便是局部阻止重排序的发生,即保证被volatile关键字修饰的变量编译后的顺序与 也即是说如果对isInit使用了volatile关键字修饰,那么在线程A中,就能保证绝对的代码(1) happens-before 代码(2),也便不会出现因为重排序而可能造成的异常。

⑹使用场景

volatile关键字在某些情况下性能要优于synchronized,但是需要保证操作是原子性操作。

7.CyclicBarrier

它主要的方法就是一个:await()。await() 方法没被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此 CyclicBarrier 上面阻塞的线程开始运行。在这之后,如果再次调用 await() 方法,计数就又会变成 N-1,新一轮重新开始,这便是 Cyclic 的含义所在。

CyclicBarrier 的使用并不难,但需要主要它所相关的异常。除了常见的异常,CyclicBarrier.await() 方法会抛出一个独有的 BrokenBarrierException。这个异常发生在当某个线程在等待本 CyclicBarrier 时被中断或超时或被重置时,其它同样在这个 CyclicBarrier 上等待的线程便会受到 BrokenBarrierException。意思就是说,同志们,别等了,有个小伙伴已经挂了,咱们如果继续等有可能会一直等下去,所有各回各家吧。

CyclicBarrier.await() 方法带有返回值,可以加超时时间,用来表示当前线程是第几个到达这个 Barrier 的线程。

和 CountDownLatch 一样,CyclicBarrier 同样可以可以在构造函数中设定总计数值。与 CountDownLatch 不同的是,CyclicBarrier 的构造函数还可以接受一个 Runnable,会在 CyclicBarrier 被释放时执行。

package dxc2;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//经典
public class CyclicBarrierTest {
	  public static void main(String[] args) throws IOException, InterruptedException {  
	        //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去  
	        //Waits until all parties have invoked await on this barrier.   
	       // CyclicBarrier barrier = new CyclicBarrier(3);  
	        final int count = 3;  
	        final CyclicBarrier barrier = new CyclicBarrier(count, new Runnable() { 
	        	//数量到达屏障数量执行
	            @Override  
	            public void run() {  
	                System.out.println("go go go!");  
	            }  
	        });  
	        ExecutorService executor = Executors.newFixedThreadPool(3);  
	        executor.submit(new Thread(new Runner(barrier, "1号选手")));  
	        executor.submit(new Thread(new Runner(barrier, "2号选手")));  
	        executor.submit(new Thread(new Runner(barrier, "3号选手")));  
	  
	        executor.shutdown();  
	    }  
	}  
	  
	class Runner implements Runnable {  
	    // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)  
	    private CyclicBarrier barrier;  
	  
	    private String name;  
	  
	    public Runner(CyclicBarrier barrier, String name) {  
	        super();  
	        this.barrier = barrier;  
	        this.name = name;  
	    }  
	  @Override  
	    public void run() {  
	        try {  
	            Thread.sleep(100 * (new Random()).nextInt(8));  
	            System.out.println(name + " 准备好了...");  
	            // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。  
	            barrier.await();  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        } catch (BrokenBarrierException e) {  
	            e.printStackTrace();  
	        }  
	        System.out.println(name + " 起跑!");  
	    }  
}

输出:

1号选手 准备好了...
2号选手 准备好了...
3号选手 准备好了...
go go go!
3号选手 起跑!
1号选手 起跑!
2号选手 起跑!

8.CountDownLatch

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。CountDownLatch最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。

package dxc2;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchTest {

    // 模拟了100米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。
    public static void main(String[] args) throws InterruptedException {

        // 开始的倒数锁 
        final CountDownLatch begin = new CountDownLatch(1);  

        // 结束的倒数锁 
        final CountDownLatch end = new CountDownLatch(10);  

        // 十名选手 
        final ExecutorService exec = Executors.newFixedThreadPool(10);  

        for (int index = 0; index < 10; index++) {
            final int NO = index + 1;  
            Runnable run = new Runnable() {
                public void run() {  
                    try {  
                        // 如果当前计数为零,则此方法立即返回。
                        // 等待
                        begin.await();   //阻塞1
                      
                       /* Thread.sleep((long) (Math.random() * 10000));  */
                        Thread.sleep((long) (0));  
                        System.out.println("No." + NO + " arrived");  
                    } catch (InterruptedException e) {  
                    } finally {  
                        // 每个选手到达终点时,end就减一
                        end.countDown();  //减到0打开阻塞2
                    }  
                }  
            };  
            exec.submit(run);
        }  
        System.out.println("Game Start");  
        // begin减一,打开阻塞1,开始游戏 
        begin.countDown();  
        // 等待end变为0,即所有选手到达终点
        end.await();     //阻塞2
        System.out.println("Game Over");  
        exec.shutdown();  
    }
}

输出:

Game Start
No.2 arrived
No.5 arrived
No.7 arrived
No.3 arrived
No.8 arrived
No.1 arrived
No.4 arrived
No.6 arrived
No.9 arrived
No.10 arrived
Game Over
  • CyclicBarrier和CountDownLatch的区别
    • CountDownLatch减计数,CyclicBarrier加计数。
    • CountDownLatch是一次性的,CyclicBarrier可以重用。
    • CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。
      CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着。
    • CountDownLatch基于AQS;CyclicBarrier基于锁和Condition。本质上都是依赖于volatile和CAS实现的。

9.Semaphore

是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问.
Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控.

package dxc2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreTest {
	public static void main(String[] args) {  
        // 线程池 
        ExecutorService exec = Executors.newCachedThreadPool();  
        // 只能5个线程同时访问 
        final Semaphore semp = new Semaphore(5);  
        // 模拟20个客户端访问 
        for (int index = 0; index < 20; index++) {
            final int NO = index;  
            Runnable run = new Runnable() {  
                public void run() {  
                    try {  
                        // 获取许可 
                        semp.acquire();  
                        System.out.println("Accessing: " + NO);  
                        Thread.sleep((long) (Math.random() * 1000));  
                        // 访问完后,释放 ,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后线程一直阻塞
                        //semp.release();  
                    } catch (InterruptedException e) {  
                    }  
                }  
            };  
            exec.execute(run);  
        }  
        // 退出线程池 
        exec.shutdown();  
    }  
}

输出:

Accessing: 2
Accessing: 6
Accessing: 3
Accessing: 14
Accessing: 10

10.atmoic

从JDK1.5开始引入了java.util.concurrent.atomic包,方便程序员在多线程情况下进行无锁的原子操作,但是由于CPU的架构不同,提供的原子指令不一样,也有可能需要某种形式的内部锁,所以也不能完全保证线程不被阻塞!
在atmoic包里一共有12个类,四种原子更新方式:
⑴原子更新基本类型

  • AtomicBoolean:原子更新布尔类型。
  • AtomicInteger:原子更新整型。
  • AtomicLong:原子更新长整型。
    ⑵原子更新数组
  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。
    ⑶原子更新引用
  • AtomicReference:原子更新引用类型。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
    ⑷原子更新字段
  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。

举例:
AtomicInteger:

int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
boolean compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
package dxc1;
 
import java.util.concurrent.atomic.AtomicInteger;
 
public class Test123 {
	static AtomicInteger ai = new AtomicInteger(1);
 
	public static void main(String[] args) {
		System.out.println(ai.getAndIncrement());
		System.out.println(ai.get());
	}
}

输出:

1
2

11.LockSupport

⑴构造函数
// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
static Object getBlocker(Thread t)
// 为了线程调度,禁用当前线程,除非许可可用。
static void park()
// 为了线程调度,在许可可用之前禁用当前线程。
static void park(Object blocker)
// 为了线程调度禁用当前线程,最多等待指定的等待时间(纳秒),除非许可可用。
static void parkNanos(long nanos)
// 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
static void parkNanos(Object blocker, long nanos)
// 为了线程调度,在指定的时限前禁用当前线程(毫秒),除非许可可用。
static void parkUntil(long deadline)
// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(Object blocker, long deadline)
// 如果给定线程的许可尚不可用,则使其可用。
static void unpark(Thread thread)
⑵它是不可重入的
package testSpringMVC;
 
import java.util.concurrent.locks.LockSupport;
 
public class Test {
public static void main(String[] args) {
	 Thread thread = Thread.currentThread();  
     
	    LockSupport.unpark(thread);  
	    LockSupport.unpark(thread); 
	    System.out.println("a");  
	    LockSupport.park();  
	    System.out.println("b");  
	    LockSupport.park();  
	    System.out.println("c");  
}
}

输出:

a
b
⑶主要用途

park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。

当前线程需要唤醒另一个线程,但是只确定它会进入阻塞,但不确定它是否已经进入阻塞,因此不管是否已经进入阻塞,还是准备进入阻塞,都将发放一个通行准许。

⑷正确用法

把LockSupport视为一个sleep()来用,只是sleep()是定时唤醒,LockSupport既可以定时唤醒,也可以由其它线程唤醒。

⑸与wait和notify的区别

主要的区别应该说是它们面向的对象不同。阻塞和唤醒是对于线程来说的,LockSupport的park/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“线程”的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。

package testSpringMVC;
 
import java.util.concurrent.locks.LockSupport;
 
public class Test {
public static void main(String[] args) {
	 Thread thread1 = new Thread(){
	        public void run(){
	            System.out.println("子线程1");
	            LockSupport.park();
	            System.out.println("子线程1 -> 已通行!");
	        }
	    };
		 Thread thread2 = new Thread(){
		        public void run(){
		            System.out.println("子线程 2");
		            LockSupport.park();
		            System.out.println("子线程 2-> 已通行!");
		        }
		    };
		    thread1.start();
		    thread2.start();
		    LockSupport.unpark(thread1);
		    
}
}

12.Exchanger

Exchanger 类表示一种会合点,两个线程可以在这里交换对象。两个线程各自调用 exchange 方法进行交换,当线程 A 调用 Exchange 对象的 exchange 方法后,它会陷入阻塞状态,直到线程B也调用了 exchange 方法,然后以线程安全的方式交换数据,之后线程A和B继续运行。

exchange 方法有两个重载实现,在交换数据的时候还可以设置超时时间。如果一个线程在超时时间内没有其他线程与之交换数据,就会抛出 TimeoutException 超时异常。如果没有设置超时时间,则会一直等待。

13.ReentrantReadWriteLock

⑴概述

读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。

⑵特点
  • ⑴写锁可以降级为读锁,但是读锁不能升级为写锁。
  • ⑵不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
  • ⑶WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
  • ⑷默认构造方法为非公平模式 ,开发者也可以通过指定fair为true设置为公平模式 。
⑶升降级
  • 读锁里面加写锁,会导致死锁
  • 写锁里面是可以加读锁的,这就是锁的降级

14.线程池

⑴为什么使用线程池
  • 避免创建和销毁线程的系统开销
  • 控制最大并发数,防止阻塞
  • 对线程进行简单管理,如延迟执行
⑵构造函数

线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,ThreadPoolExecutor提供四个构造函数


//五个参数的构造函数public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)
//六个参数的构造函数-1public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)
//六个参数的构造函数-2public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)
//七个参数的构造函数public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)  
  • ①corePoolSize 核心线程,默认情况下会一直存在于线程池中,如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中.
  • ②int maximumPoolSize 该线程池中线程总数最大值,就是正在运行的线程数最大值
  • ③long keepAliveTime 非核心线程闲置超时时长,一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉,如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么它也会起作用,直到核心线程为0.
  • ④TimeUnit unit keepAliveTime的单位
  • ⑤BlockingQueue workQueue 该线程池中的任务队列:维护着等待执行的Runnable对象
  • ⑥ThreadFactory threadFactory 创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上。
  • RejectedExecutionHandler handler 有四种策略
  • AbortPolicy抛出RejectedExecutionException异常。
  • DiscardOldestPolicy将队列最老的任务干掉
  • DiscardPolicy也是丢弃任务,但是不抛出异常
  • CallerRunsPolicy即不用线程池中的线程执行,而是交给调用方来执行
⑶使用原理
  • ①线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  • ②线程数量达到了corePools,则将任务移入队列等待
  • ③队列已满,新建线程(非核心线程)执行任务
  • ④队列已满,总线程数又达到了maximumPoolSize,就会采取拒绝策略
⑷常用方法
  • execute() //可以向线程池提交一个任务,交由线程池处理
  • submit() //也是用来向线程池提交任务,和execute()比可以返回任务执行的结果,实际上就是调用execute()方法,利用Future获取结果
  • shutdown() //关闭线程池
  • shutdownNow() //关闭线程池
⑸常见线程池
①可缓存线程池CachedThreadPool
  • 线程数无限制
  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  • 闲置线程60s会销毁
②定长线程池FixedThreadPool()
  • 核心线程数=最大线程数,并且线程池的线程不会因为闲置超时被销毁,队列无限长,直到outOfMemory
③定时线程池ScheduledThreadPool()
  • 设置核心线程数,最大线程数是Integer.MAX_VALUE,其实也是个无限大小的线程池。
  • 支持延迟及周期性任务执行。
④单线程化的线程池SingleThreadExecutor()
  • 有且仅有一个工作线程执行任务,所有任务按照指定顺序执行,即遵循队列的入队出队规则

你可能感兴趣的:(JAVA多线程技术,JAVA基础)