- 线程的分类
-
用户线程:执行完后才可以停下,用户创建的线程默认为用户线程
-
守护线程:保护用户线程,Java虚拟机不会因为守护线程是否执行完毕而等待
1.线程的创建方式:
-
继承Thread类
-
实现Runnnable接口
-
实现Callable接口通过FutureTask包装器来创建Thread线程,实现call()方法
注: 在面向对象中尽量多用实现少用继承,因为在Java中存在单继承的局限性。
2.线程基本方法:
Jion()(等待其他线程终止):在当前线程调用一个线程的join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再有阻塞状态变为就绪状态,等待cpu。
Yield()(线程让步):会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性竞争到CPU时间片。
Interrupt()(线程中断):给这线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身不会因此而改变状态。当调用sleep()而使线程处于time-wating状态,这时调用interrupt()方法,会抛出InterruptException,从而是线程提前结束。
currentThread():返回代码段正在被那个线程调用。而且当调用start()方法时,返回的是线程名字,而调用run()方法时,返回的是main线程。(currentThread()方法是返回当前正在被哪个线程调用,而this是指向当前对象)
isAlive()方法:是判断当前的线程是否处于活动状态。活动状态:即线程已经启动但未终止。
getId()方法:是取得线程唯一的标识。
sleep()方法:是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)是指this.currentThread()暂时指定的时间,让出cpu给其他线程,但是他的监控状态依然存在,指定的时间一到,就会自行恢复运行状态。
wait()方法:会释放当前的锁,让出cpu,进入等待状态,只有当notify()/notifyAll()被执行时,才会唤醒处于等待状态的线程。(属于Object类)
start():是方法是通知“线程规划器”,此线程已经准备就绪,等待调用线程对象的run()方法。(执行start方法的顺序不代表线程启动的顺序)
run():包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。Run方法运行结束,此线程终止,然后CPU再调度其他线程。
notify(): 只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
notifyAll(): 会唤醒所有等待(对象的)线程
3.线程池
线程池创建线程的优点:1.降低资源的消耗 2.提高响应的速度 3.方便管理
java提供的四种线程池:
1.newSingleThreadExecutor:创建一个单线程的线程池。如果这个唯一的线程因为异常结束,那么会有一个新的线程来代替它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统能够创建的最大线程大小。
4.newScheduledThreadPool:创建一个无线大小的线程池。此线程池支持定时以及周期性执行任务的需求。
实现了Executor接口的ThreadPoolExecutor:
参数:核心线程池大小(corePoolSize);最大线程的数量(maximumPoolSize);超时释放(keepAliveTime);超时单位(Unit);阻塞队列(workqueue);线程工厂(threadFactory);拒绝策略(Handler)。
处理流程:当可执行的任务小于核心线程池数的时候,每加一个任务就新建一个线程;当可执行的任务数达到核心线程池数的时候,新任务会放到任务队列中去;当任务队列数量满了,并且最大线程数大于核心线程池数,新任务就新建线程;当需要执行的任务总数超过任务队列加最大线程数时,就实现拒绝策略。
阻塞队列常用实现:
ArrayBlockingQueue:基于数组结构的有界队列,此队列按照FIFO原则对任务进行排序;
LinkedBlockingQueue:基于链表结构的无界队列,此队列按FIFO原则对任务进行排序;
SynchronousQueue:直接将任务提交给线程而不是将它加入到队列,实际上此队列是空的,每个插入操作都需要等上一个调用移除操作;
PriorityBlockingQueue:具有优先级的队列的有界队列,可以自定义优先级,默认是按自然排序。
拒绝策略:实现RejectedExcutionHandler接口
AbortPolicy(中止策略,抛出异常,默认的策略)
CallerRunsPolicy(由调用线程处理该任务)
DiscardOldestPolicy(丢弃队列最前面的任务,然后重新提交被拒绝的任务)
DiscardPolicy(线程队列已满,则后续提交的任务都会被丢弃)
线程池的使用准则:
1.不要对那些同步等待其他任务结果的任务排队;
2.理解任务,要有效的调整线程池的大小,需要理解正在排队的任务以及它们正在什么。线程不要太多或者太少;
4.同步集合
(1)CopyOnWrite思想,写入时复制:
写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。实现类有:CopyOnWriteArrayList集合以及CopyOnWriteArraySet
//CopyOnWriteArrayList 的 add() ,在访问的时候,拷贝出来一个副本, //先操作这个副本,再把现有的数据替换为这个副本; //然后其get()方法就是普通的无锁访问; public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
优点:对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。
缺点:数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题。
(2)ConcurrentHashMap:采用分段锁方式解决线程安全问题,并且效率高。
ConcurrentHashMap实现原理:ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。
HashTable:底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable
HashMap:底层数组+链表实现,可以存储null键和null值,线程不安全
(3)BlockingQueue:阻塞队列
ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。 ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行。
LinkedBlockingQueue:基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
LinkedBlockingDeque:是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可从队列的头和尾同时操作(插入或删除),并且,该阻塞队列赛是线程安全的。并且可指定队列的容量,默认是Integer.MAX_VALUE。
BlockingQueue常用的方法:
add(e) 将元素e加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则抛出异常;
offer(anObject): 表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
offer(E o, long timeout, TimeUnit unit): 可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
put(anObject): 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
element() 获取队首元素,如果队列为空,则抛出异常。
drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
(4)信号量Semaphore
Semaphore是一个计数信号量,本质是一个共享锁,信号量维护了一个信号量许可集,线程通过调用aquire()方法来获取信号量的许可,当信号量中有可用的许可时,线程能获取该许可;
否则线程等待,直到有可用的许可证为止。线程可以通过relesae来释放它所持有的信号量许可。
前三个是立刻输出,后两个等待2秒后输出。
(5)循环栅栏CyclicBarrier
CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到达到某个公共屏障点。因为该barriera在释放等待线程后可以重用。
(6)闭锁Countownatch
CountDownLatch也是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到满足条件。
创建了一个数量为5的CountDownlactch对象,任务创建,启动5个work线程,然后调用await让主线程进入等待状态,5个Work线程执行完都会调用countDown(0函数,5个完成之后,主线程就会被唤醒。
CountDownLatch的计数无法被重置,CyclicBarrier的计数可以被重置。
非线程安全:
是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被改,值不同的情况,进而影响程序的执行。多个线程在执行同一个变量进行并发访问时,会产生脏读。
5.同步锁
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
Synchronized锁(同步锁)关键字
实现原理:Synchronized可以保证变量或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性;一般是一个自旋操作,线程如果没有拿到这把锁,那么就会不断尝试,直到拿到为止,而且是多个线程一起抢。
缺点:线程释放锁只有两种情况:获得锁的线程执行完了该代码块,然后线程释放对锁的占有;线程执行发生异常时,jvm会让线程自动释放锁。如果是其他原因线程被阻塞,但是没释放锁,其他线程只能等待,会影响执行效率。或者如果多个线程只是进行读操作,但是当一个线程在读时,其他线程也只能等待。
只提高了一致性,但是并发性大大降低
Volatile关键字:
能保证可见性,不能保证原子性,禁止指令重排
可见性:即线程修改数据后会通知到所有线程,所有线程都知道该数据被修改
原子性:全部执行完成。
禁止指令重排:即在底层计算机汇编语言的指令执行顺序不会被改变
原理:1.确保指令重排时不会把其后边的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,即在执行到内存屏障这句指令时,在它的前面的操作已经全部完成。2.它会强制将对缓存的修改操作立即写入主存。如果是写操作,它会导致其他cpu中对应的缓存行无效。
2.应用场景:对变量的写操作不依赖当前值,该变量无包含在具有其他变量的不变式。
3.jMM(内存模型):主内存+工作内存
ReentrantLock:(互斥锁)
ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。ReentrantLock是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成,这些方法就来源于父接口Lock,而synchronized是JVM的关键字。lock()方法是获取锁,tryLock()是尝试非阻塞的获取锁,unlock()是释放锁
ReentrantLock的基本方法:
lock() : 获取锁;
tryLock() : 尝试获取锁;
tryLock(long timeout,TimeUnit unit) : 尝试获取锁,如果到了指定时间还获取不到,显示超时;
unLock() : 释放锁;
newCondition(): 获取锁的Condition
Condition是为线程提供了一个含义,需要和某种形式的锁与一个条件关联起来,等待提供一个条件的主要属性是:以原子方式释放相关的锁,并挂起当前线程,就像wait()方法一样。
await() : 线程等待
await(int time,TimeUnit unit) : 线程等待特定时间,超时则为超时
signal(): 随即唤醒某个等待线程
signalAll() : 唤醒所有等待的线程
Synchronized 和 Lock的区别
- Synchronized 内置的Java关键字, Lock是一个Java类
- Synchronized 无法判断获取锁的状态, Lock可以判断是否获取到了锁
- Synchronized 会自动释放锁, Lock必须要手动释放锁,如果不释放锁,死锁!
- Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
- Synchronized 可重入锁,不可以中断的,非公平锁;Lock,可重入锁,可以判断锁,非公平锁(可自己设置)
- Synchronized 适合锁少量的代码同步问题, Lock适合锁大量的同步代码!
AQS: 队列同步器AbstractQueuedSynchronizer(简称同步器),是用来构建锁或其他同步组件的基础框架,通过内置的FIFO队列来完成获取线程的排队工作,AQS主要利用硬件原语指令(CAS compare-and-swap),来实现轻量级多线程同步机制,并且不会引起CPU上文切换和调度,同时提供内存可见性和原子化更新保证(线程安全的三要素:原子性、可见性、顺序性)。
CAS: (比较并交换)
就是compare and swap 或者compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存与之前取到的值是否相等,若相等,则用新值,若失败则重试,一般情况是一个自旋操作,即不断的重试。(atomicInteger里的CompareAndSet()方法,比较并交换)
CAS的缺点:1.循环时间长开销很大;2.只能保证一个共享变量的原子操作
3.引出来ABA问题
ABA问题:狸猫换太子,即线程1,2同时得到数据A,但是线程1可能执行5秒,线程2只执行两秒,线程2将A改为B,线程1还没有执行完,线程2再将B改回A,此时线程1返回后会以为数据没有改动。
原子引用类(AtomicReference)CompareAndSet(预期值,新值)方法