Java线程和并发

线程和并发:
在这里插入图片描述
Java线程是抢占式的调度(jvm调度)
并行:指两个或多个事件在同一时刻点同时发生。
并发:指两个或多个事件在同一时间段内发生。重点
单核处理器就不能并行,只能并发 ,通过CPU通过时间片来调度每个线程的执行时间。
多核处理器在同一时间点一起执行线程就是并发。
进程:一个内存中运行的应用程序,独立的内存空间,通讯不方便,一个进程可以有多个线程。
线程:在同一个进程中的多个任务。基本单位。重点
Java程序就至少有主线程和一个垃圾回收线程
多线程的优势:共享内存, 创建线程代价小,占有资源低,java本身对于多线程的支持(jvm支持多线程,内部是jvm来调度多线程)
创建线程:
1.继承Thread类 ,2.实现Runnable接口(也可以使用匿名内部类来创建只使用一次的线程)继承和接口的方法有不一样(操作和共享资源方面有差异)

当我们的多线程启动之后,就交给我们的jvm来调度
Start方法只能调用一次 否则报错
我们在使用多线程访问同一个资源的安全性问题时:1.同步代码块 2.同步方法 3.锁机制
同步代码块和同步方法(synchronized修饰的方法):
synchronized (this) {
//同步代码块 同时只有一个对象可以进去这个代码块 执行完后退出其他的对象才可以进入
//其中的this就是同步锁,表示当前对象
if (num > 0) {
System.out.println(Thread.currentThread().getName() + “吃了第” + num-- + “个苹果”);
}
}
Synchronized 好坏:这个也就是stringbuilder和stringbuffer的区别 stringbuffer的方法上面贴有Synchronized修饰符。提高了安全,降低了性能
1, 保证了多线程并发的同步操作,保证安全性
2, 性能低
有很多的相关都和Synchronized有关:

  1. stringbuilder和stringbuffer(安全)
  2. Arraylist和Vector(安全)
  3. Hashmap和hashtable(安全)
    我们一般用性能高但是不安全的
    我们在写单例模式时:一般推荐使用饿汉式(调用之前就直接创建类)性能高。没有线程安全性问题。我们在使用懒汉式的时候(调用时创建)会有线程安全性问题,所以推荐使用饿汉式。
    Juc包详解 :
    在这里插入图片描述
    并发容器:一般都是采用锁分段技术,对于操作的位置进行同步操作
  4. ConcurrentHashMap(对应Hashmap)(使用分段锁):相比Hashtable进一步提高并发性。
    原理:HashMap,Hashtable与ConcurrentHashMap都是实现的哈希表数据结构,
    Hashtable是对整张哈希表进行锁定,巨大的浪费。ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度进行区别以及如何去锁定。ConcurrentHashMap单独锁住每一个桶(segment).ConcurrentHashMap将哈希表分为16个桶(默认值),诸如get(),put(),remove()等常用操作只锁当前需要用到的桶,而size(),isEmpty()才锁定整张表。原来只能一个线程进入,现在却能同时接受16个写线程并发进入(写线程需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的。 迭代时使用了不同于传统集合的快速失败迭代器(fast-fail iterator)的另一种迭代方式,称为弱一致迭代器,在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时实例化出新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的并发性和扩展性,是性能提升的关键。
    2.CopyOnWriteArrayList(对应arraylist): 利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值 给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
    3.CopyOnWriteArraySet(对应HashSet): 基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
    1. ConcurrentSkipListMap(对应TreeMap): Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。
    2. ConcurrentSkipListSet(TreeSet): 内部基于ConcurrentSkipListMap实现.

并发队列(继承于Queue):
普通的队列:
1.ArrayDeque, (数组双端队列)
2.PriorityQueue, (优先级队列)
3.linkedList(链表队列)

  1. 非阻塞队列(高性能):ConcurrentLinkedQueue和ConcurrentLinkedDueue
    a) ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁(cas)的方式,实现了高并发状态下的高性能和线程安全,通常ConcurrentLinkedQueue性能好于BlockingQueue。 它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。 头是最先加入的,尾是最近加入的,该队列不允许null元素。
  2. 阻塞队列(BlockingQueue):支持两个附加操作的队列,通过加锁的方式实现线程安全的:两个操作:
    1、 在队列为空时,获取元素的线程会等待队列变为非空。
    2、 当队列满时,存储元素的线程会等待队列可用。
    阻塞队列常用于生产者和消费者的场景。
    阻塞队列和普通队列的区别:
    阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素take()的操作将会被阻塞,或者当队列是满时,往队列里添加元素put()的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列.
    • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    • LinkedBlockingDeque:一个由链表结构组成的双向有界阻塞队列。

• PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
• DelayQueue:一个使用延迟时间排序的无界阻塞队列。
• LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
• SynchronousQueue:一个不存储元素的阻塞队列。

具体分类:

  1. SynchronousQueue队列是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。可以看做内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
  2. 有界队列:ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。 有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。 ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
  3. 无界队列:PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。 所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。 另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。
    并发工具类:
  4. CountDownLatch: CountDownLatch的构造函数接受一个int类型的参数作为计数器,通过调用CountDownLatch提供的countdown方法,可以使计数器减1,他提供了一个await方法用来阻塞当前线程,计数器减一,当计数器的值为0,就恢复正常。
    不能被重置。
  5. CyclicBarrier: 可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。可以重置之后循环使用
    1和2的区别是:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。
    CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。
  6. Semaphore信号量:控制并发线程数。Semaphore可以控制同时访问资源的线程个数。内部管理一组许可(数量一定),线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。 Semaphore对象也可以实现互斥锁的功能,并且可以是由一个线程获得了"锁",再由另一个线程释放"锁",这可应用于死锁恢复的一些场合。
  7. Exchanger 交换器:Exchanger用于实现线程间的数据交换。Exchanger提供一个同步点,在同步点上,两个线程使用exchange方法交换彼此数据。
    原子类:(底层就是volatile(可见性)和CAS(原子性)共同作用的结果)
    java.util.concurrent.atomic包下面的类;原子性变量类
    特点:多线程环境下,无锁的进行原子操作。不能绝对保证线程不被阻塞(因cpu原子指令不同,可能需要某种形式的内部锁):
    java.util.concurrent.atomic原子操作类包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。
    在这里插入图片描述
    标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
    数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
    更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
    复合变量类:AtomicMarkableReference,AtomicStampedReference
    可以适当的代替锁,但是锁才是更加安全。
    线程本地存储:防止任务在共享资源上产生冲突。

Lock(接口):
锁的几个重要概念:
一、公平锁/非公平锁
1.公平锁是指按多个线程申请锁的顺序来获取锁。
2.非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
比如:对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS(AbstractQueuedSynchronized 抽象的队列式的同步器 ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask)的来实现线程调度,所以并没有任何办法使其变成公平锁。

二、可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
ReentrantLock、Synchronized都是可重入锁

三、独享锁/共享锁
1.独享锁是指该锁一次只能被一个线程所持有。
2.共享锁是指该锁可被多个线程所持有。
对于ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

四、互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock

五、乐观锁/悲观锁
不是具体类型的锁,而是指看待并发同步的角度
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

六、分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,目的在于细化锁粒度。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

七、偏向锁、轻量级锁、重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

八、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

九、闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。CountDownLatch就是一种灵活的闭锁实现。

十、活锁
LiveLock是一种形式活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发送在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放回到队列开头,因此处理器将被反复调用,并返回相同的结果。

十一、无锁
要保证现场安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
无状态编程。无状态代码有一些共同的特征:不依赖于存储在对上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非无状态的方法等。可以参考Servlet。
线程本地存储。可以参考ThreadLocal
volatile
CAS
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

  1. synchronized的缺陷:
    如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
      1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
    2)线程执行发生异常,此时JVM会让线程自动释放锁。
    如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程只能等待;影响效率。Lock机制就可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),还可以局部锁定,提高效率。还可以知道线程有没有成功的获取锁。Lock需要用户主动释放锁,并且在发生异常时,不会自动释放锁。(否则可能死锁),synchronized不需要。
  2. 常用类和方法:
    Lock接口中的方法:lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()(可中断)是用来获取锁的同时保持对中断的响应。
    可定时的与可轮询的锁获取模式由tryLock()实现。如果不能获得所有需要的锁,那么就会释放已经获得的锁。然后再尝试重新获取。降低活锁的可能,
    Lock lock = …;
    lock.lock();
    try{
    //处理任务
    }catch(Exception ex){

}finally{
lock.unlock(); //释放锁
}

ReentrantLock(可重入独占式锁:类):公平锁和非公平锁两种实现方式。synchronized也是可重入锁 。可重入和不可重入的概念是这样的:当一个线程获得了当前实例的锁,并进入方法A,这个线程在没有释放这把锁的时候,能否再次进入方法A呢?

可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法A(方法A递归)。内部有一个计数器。
不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。
ReadWriteLock(接口)维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
最多支持 65535 个递归写入锁和 65535 个读取锁。试图超出这些限制将导致锁方法抛出 Error。有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
ReentrantReadWriteLock是上面接口的实现类(里面还有其他的方法):
注意:如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
LockSupport:是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。
Condition:的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
AbstractQueuedSynchronizer(AQS): 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架(很多的同步器都可以通过AQS简单高效的构建出来)。该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作:
java.util.concurrent.locks.AbstractQueuedSynchronizer.getState()
java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int, int)
子类推荐被定义为自定义同步装置的内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干acquire之类的方法来供使用。该同步器即可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。
Executor框架:
New thread()的缺点:每次neYUUUw Thread()耗费性能 ,调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。 不利于扩展,比如如定时执行、定期执行、线程中断。
采用线程池的优点:重用存在的线程,减少对象创建、消亡的开销,性能佳
可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
提供定时执行、定期执行、单线程、并发数控制等功能。
executor框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
我们发现在juc类库中,一般避免对thread对象的直接操作,而是使用executor.
在这里插入图片描述
管理thead对象:不再显式的创建thead对象
Executor:一个接口,其定义了一个接收Runnable对象的方法executor.内部采用线程池机制。
ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法(运行,关闭,已终止三种状态),以及可跟踪一个或多个异步任务执行状况返回Future的方法.
Executors工具类:
Executors提供四种线程池,newFixedThreadPool(固定数量)、newCachedThreadPool(可据实际情况调整线程数量)、newSingleThreadExecutor(只有一个。确保任务串行执行)、newScheduledThreadPool(固定长度,且以延时或定时的方式来执行,类似于Timer)。
用来创建线程池:返回一个executorservice对象等操作。
Callable接口:
实现具有返回值的任务。Call方法。必须使用executorservice.submit()方法调用。
返回值放在future中,
Future 表示异步计算的结果。
Thread.Sleep()任务终止执行给定的时间。
优先级只表示频率,不是绝对的。
Thread.yield().让步,表示可以退让cpu资源。也不是绝对的。
后台线程:线程启动之前,调用setDaemon()方法设置。程序结束与否只和非后台线程有关。后台线程创建的线程也是后台线程。
如果普通的线程异常:会直接抛给控制台,无法捕获。我们使用:
Thread.UncaughtExceptionHandler,:当 Thread 因未捕获的异常而突然终止时,调用处理程序的接口。
ThreadPoolExecutor(线程池类):
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
线程池的创建:

public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
threadFactory:线程工厂:用于线程池创建线程。默认创建一个新的,非守护线程,不包含任何的配置信息。可以通过制定一个线程工厂,来配置创建的线程
handler:超出线程范围和队列容量的任务的处理程序
其中包含四种饱和策略:

  1. AbortPolicy(终止策略)默认。将抛出未检查的RejectedExcution-Exception,调用者可以捕获这个异常,然后根据需求写自己的代码,
  2. DiscardPolicy(抛弃策略) 直接抛弃该任务
  3. DiscardOldestPolicy(抛弃最旧策略) 会抛弃下一个将要执行的任务,如果是优先级队列,就抛弃优先级最高的。所以一般不和优先级队列一起使用。
  4. CallerRunPolicy(调用者运行)该策略不会抛弃任务,也不会抛出异常,而是将任务的运行回退到任务调用者,在提交任务的线程中执行该任务。
    ScheduledExecutorService接口(可安排在给定的延迟后运行或定期执行的命令。)和ScheduledThreadPoolExecutor类实现(安排在给定的延迟后运行命令,或者定期执行命令。):
    Future和Fork/Join(java7新特性):
    Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

Fork(切分)/join(合并): 可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。充分利用线程进行并行计算,并减少了线程间的竞争,
详情请看:
fork/jion详解
线程的四种状态:
新建,就绪,阻塞,死亡。
一个线程被阻塞的原因:
1.调用sleep()进入休眠.
2.调用wait()线程挂起
3.任务在等待某个输入/输出(I/O)
4,试图在某个对象上调用同步控制方法,但是对象锁不可用,因为另一个任务获取了该锁,
Thread.interrupt()中断此线程。实际上给线程设置一个中断标志,线程仍会继续运行。
Interrupted()判断是否被中断。
线程之间之间的协作:wait和notify,notifyall.方法。
死锁:任务之间相互等待的连续循环。哲学家就餐问题。

线程之间通信的方式:

  1. syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
    如果调用某个对象的wait()方法,当前线程必须拥有这个对象的monitor(即锁),因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。如果当前线程没有这个对象的锁就调用wait()方法,则会抛出IllegalMonitorStateException.调用某个对象的wait()方法,相当于让当前线程交出(释放)此对象的monitor,然后进入等待状态,等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁);调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
  2. ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
    Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
    Condition是个接口,基本的方法就是await()和signal()方法;
    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
    调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
    Conditon中的await()对应Object的wait();
    Condition中的signal()对应Object的notify();
    Condition中的signalAll()对应Object的notifyAll()。
  3. 管道通信就是使用java.io.PipedInputStream 和java.io.PipedOutputStream。。PipedWriter和PipedReader进行通信。
  4. 锁机制和synchronized关键字的同步机制。和volatile一样,是“共享内存”式的方式
  5. 利用CyclicBarrierAPI。其实还是lock和condition机制
  6. 利用BlockingQueue;
    备注:其实对于线程间的通讯和协作我们可以分成两个大的方向:
    共享内存机制和消息通信机制
    并发模型 通信机制 同步机制 例子

共享内存 线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。 同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。 锁,synchronized,volatile
等大多数
消息传递(actor) 线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。 由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 管道。

锁机制(更广泛的同步操作)
Lock提供了synchronized更广泛和更强大的操作 体现面向对象(java5开始)
Lock l = …;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
进入(锁)-》执行代码-》 离开(解锁)
java5在提供lock机制的同时提供了codition接口,解决lock机制的通讯控制和互斥
现获取锁,在使用condition
总的来说:lock机制取代了synchronized机制 而condition接口就取代了其中的wait,notify,notifyall方法
多线程容易造成死锁。互相等待的情况,不同于阻塞。没有解决,只能避免。
线程的六种状态:
Wait和sleep方法:
Sleep让我们的线程放弃cpu,休眠一段时间。但是不会释放同步锁。多用于模拟网络延迟等。
但是wait方法会释放同步锁。
线程睡眠(sleep) 线程联合(join)
后台线程:我们创建的默认是前台线程。设置daemon=true是后台线程 ,在start之前设置。后台线程随着前台线程的结束就结束了。前台线程不会,
线程的优先级:高低只和获得执行机会的次数有关系,不一定谁先执行,默认都一样是5.(优先级是1-10)我们一般就使用这三个值(1,5,10)
线程的礼让:当前线程愿意让出自己的cpu资源。但是调度器可以忽略该提示,调用之后线程就绪但是可能cpu又把他重新调度回来。很少使用,
Sleep和yield的区别:
都能使当前线程放弃cpu,sleep放弃之后不考虑优先级。但是yield指挥让给优先级大于等于他的。Sleep-》计时等待 。yield->就绪。
定时器:timer类,可以定时执行某任务java.util包
线程组:我们可以在创建线程的时候就设置先线程组,没有的话。如果线程A创建了B ,没设置B就加入A所在的线程组。线程组里面的操作就是批量操作里面的线程。
ThreadLocal:本地线程变量.。
其他的一些概念:
线程封闭:数据共享需要同步操作,如果仅在单线程内访问数据,就不需要同步,这就是线程封闭。例如:局部向量和ThreadLocal类.
ThreadLocal类其实不能解决线程共享变量的问题,他就是看作保存了一个map.每一个线程都对应一个值,其他线程不能访问,就是一个保存线程本地变量。
发布一个对象是使该对象能够在当前作用域之外的代码中去使用。
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象。
安全发布对象:

  1. 在静态初始化函数中初始化一个对象的引用
  2. 将对象引用保存在volatile类型或者原子类的对象中
  3. 将对象的引用类型保存在某个正确构造对象的final类型域中
  4. 将对象引用保存在锁保护的域中。
    一些共享对象的安全措施
  5. 线程封闭:
  6. 只读共享:读写锁
  7. 线程安全共享:在内部实现同步,
  8. 保护对象:只能通过持有锁来访问。
    Collections.synchronizedxxxx()。该方法实现线程安全的方式是:将他们的状态封装起来,并对每个公有的方法进行同步处理。每次只有一个线程能够访问容器的状态。
    即使是线程安全的类(hashtable vector)。在符合操作中应当注意并发修改问题。注意同步机制。
    锁是一个对象。线程去获取对象,就相当于看这个锁对象有没有被持有。
    发生死锁(两个线程都持有一个锁并且都想要获取对方的锁。)的几种情况:
    1. 锁顺序死锁:因为获得锁的顺序不同,相同就不会死锁。
    2. 动态的锁顺序死锁:转账实例。虽然加锁顺序在方法中是一致的,但是由于多线程同时操作。发生死锁。一个线程A向B转账,一个线程B向A转账。锁的顺序取决于参数的传递顺序。可以使用加时赛锁和对象的HashCode().确保加锁顺序的一致性。
    3. 在协作对象之间发生死锁:如果在持有锁时调用一个外部方法。那么将出现活跃性问题,因为外部方法可能会获取其他的锁。或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
    4. 资源死锁:某些任务需要等待其他任务的结果,那么很有可能产生死锁。所以有界线程池/资源池和相互依赖的任务不能一起使用。
    Synchronized:同步,线程安全,保证原子性,可见性和禁止指令重排序。互斥锁,可重入锁。非公平锁。synchronized 用的 锁 是 存在 对象头 中。
    对其中锁的讨论: 即使是两个不同的代码段,都要锁同一个对象,
  9. 修饰静态方法,锁住的是当前类的Class对象(每个类只有一个class对象): 同一个类不能同时访问。
    a) 同一类的不同对象,调用同一个静态同步方法,会等待锁释放.
    b) 同一类的不同对象,调用不同一个静态同步方法,会等待锁释放
    c) 同一对象,一个调用静态同步方法,一个调用非同步方法,不用等待锁释放.
  10. 普通的方法,锁的是当前实例。同一个对象不能同时访问。
    a) 一个对象调用同一同步方法会等待锁释放
    b) 一个对象调用不同同步方法会等待锁释放
    c) 同一对象,一个调用同步方法,另一个调用非同步方法不需要等待释放锁。
  11. 对于同步代码块。锁的是括号里面配置的对象。
    a) 当锁定的是this时。与修饰普通方法一样。锁住的是当前对象。
    b) 当锁定是其他对象,例如是string,那么在string是同一string,也就是地址相同时,才会竞争,不同时会并发执行。锁住的是其他对象,也就是其同一个对象会产生竞争。
    内置锁的实现原理:
    代码 块 同步 是 使用 monitorenter 和 monitorexit 指令 实现 的,而方法同步 是使用另外一种方式实现的 ,细节在JVM规范里并没有详细说明。但是方法的同步同样可以使用这两个指令来实现。
    在同步代码块的开始处插入指令monitorenter,结束和异常处插入monitorexit。任何 对象 都有一个monitor与之关联,当且一个monitor 被 持有 后, 它将 处于 锁定 状态。 线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权。即尝试获得对象的锁。
    Volatile:保证可见性和禁止指令重排序。不保证原子性。可见性:线程每次都是读到最新的数据。底层编译时会生成一个lock指令。
    实现原理:
    两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,Thread-A写了变量i,那么:
    Thread-A发出LOCK#指令
    发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
    Thread-A向主存回写最新修改的i
    Thread-B读取变量i,那么:
    Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值
    有一个类似于内存屏障的功能,保证了指令执行的顺序。禁止重排序。

ThreadLocal(线程本地存储):
ThreadLocal存放在堆内存中,ThreadLocalMap存放在本地线程缓冲(线程隔离)中
ThreadLocal内部维护了一个ThreadLocalMap内部类。使用 WeakReferences 作为键(弱键),为了能够及时的GC(避免内存泄漏). 一个线程对应一个ThreadLocalMap 。get()就是当前线程获取自己的ThreadLocalMap,根据ThreadLocal对象作为key,去获取存储于ThreadLocalMap中的值。然后返回。
在这里插入图片描述
一个线程Thread中存在一个ThreadLocalMap,ThreadLocalMap中的key对应ThreadLocal,在此处可见Map可以存储多个key即(ThreadLocal)。另外Value就对应着在ThreadLocal中存储的Value。

猜你喜欢

转载自blog.csdn.net/qq_41703795/article/details/93305855