上一篇:JAVA知识点全总结——(二)JAVA基础知识
多线程状态:开始、可运行、等待、阻塞、结束。开始状体在调用了start方法之后进入可运行状体,可运行状态调用wait、await、park后进入等待状态。等待其他线程调用notify、signal、unpark将其唤醒,这是一种线程间的通信方式。阻塞表示的是某些线程因为没有符合要求无法进入同步区,在之前等待,等条件符合可以进入。中断会中断阻塞和可运行的线程、某些锁的等待状态可以被中断,某些不能。当线程执行完毕后就进入结束状态。
实现Lock的主要类是ReentrantLock,这是一个功能比较复杂的类,主要都功能有1.公平锁;2.尝试获取锁;3.可以中断等待状态;4.读写锁;这些相比较sync锁更多的操作主要是因为他的内部是根据AQS来实现的,所以能够进行更多的操作,本身是一个jdk的类,能够提供更多的方法。
在ReentrantLock中有三个内部类,分别是一个父类sync类和两个子类,这两个子类一个实现了公平锁,另一个是非公平锁。具体的实现方法是在ReentrantLock在将线程插入AQS的时候,非公平锁可以尝试立即抢占资源,而公平锁必须去排队。
使用tryLock的方式加锁,可以设置时间,在超过时间没有获取到锁的情况下可以放弃锁,进入其他逻辑。
ReentrantLock中提供了一个方法可以中断等待中的线程,在sync中是做不到的。
ReentrantLock中有一个ReadAndWriteentrantLock可以做读写锁,读锁不限制个数,写锁排他,其实这类实现的方法都是根据AQS,本身封装成工具便于使用。
AQS是很多线程同步工具的实现类,全称是AbstractQueueSynchronizor,抽象同步队列,虽然说是一个抽象类,但是实际上他实现了所有的方法,我们只需要重写获取和释放两个方法,就能达到不同的效果。AQS维护一个队列,这个队列中存放着线程,还维护一个资源,一般来说是一个计数器,来计数有多少线程此时在使用。新添加的元素放到队尾,队头的线程获取资源,后面的线程等待队头释放自己才能获取,以此循环实现一个队列的概念。非公平锁就是在插入队尾时可以先看一下队头有没有使用,没有使用则直接抢占。
闭锁的概念是当多个线程达到指定地点时,某个约定的线程才会继续进行下去,可以用于统计或者其他功能。实现方法是一个线程await,等待计数器为0时才能进行。其他多个线程完成自己的工作后进行countDown将总数减少1,当计数为0的时候await被唤醒。类似join()。
同步屏障是指n个设置了同步屏障的线程在某一点会全部暂停,等到所有线程都到达这个点时才会继续进行。
我把信号量叫做多入锁,相比较ReentrantLock同时只能一个线程操作,Semaphore可以同时支持n个线程一起操作。可以起到限流的效果。
sync锁全称synchronized,是一种关键字用来为方法和类加锁,不同的位置有不同的作用。用wait和notify进行进程间同步。
死锁是指多个线程同时请求对方的资源被卡死的情况,这种情况要在编程的时候注意会不会发生,如果怀疑自己的线程有死锁情况,可以dump一下Thread文件查看线程运行情况。死锁发生的条件是,每个线程占有资源,资源不共享,不能抢占,不会一段时间后按时释放资源等,造成了死锁情况。解决死锁的方法很多,一种是从逻辑上找到问题,尽量让所有的线程按照同样的顺序请求资源。其他的方法是设置线程共享变量,或者final字段。
我们有很多个任务需要进行,为了执行这些任务我们创建了一些线程,如果每个线程执行完毕之后就释放,再新建就会非常浪费资源。这是个时候我们就想能不能把线程都存放在一起,进行复用,这就是线程池的概念。线程池按照一定的规则维护线程并且当有任务的时候使用这些线程来执行。
创建线程池threadPoolExecutor有一些参数。核心线程数,当前线程池稳定维护的线程数;最大线程数,当前线程池最大可以容纳的线程数;等待时间,等待时间过了而且线程池中的线程超过核心的数量,就不断减少;等待时间单位;阻塞队列,选用不同的阻塞队列存放待执行的任务,大概有Array,Linked,Synchronou的BlockingQueue几种情况;拒绝模式,当队列饱和的时候如何处理其他任务,有直接丢弃,有抛出异常,有丢弃队列中最近的一个任务,先执行新任务,不断重复塞入等操作。
newSingleThreadExecutor->new ThreadPoolExecutor(1, 1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool->new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool->new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue());创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown();
我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换。2)并发不高、任务执行时间长的业务要区分开看: a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
BlockingQueue是队列的一种变种,在入队和出队的时候会上同步锁,并且如果一个线程向BlockingQueue中插入时发现已经满了就会阻塞,一直到有空余为止。如果一个线程读取发现是空的就会阻塞,知道有数据为止。
java中的线程中断是指其他线程调用Thread的interupt方法进行中断请求,致于如何处理这个中断请求是在内部通过捕获这个异常自己逻辑实现的。在stop方法被废弃之后没有方法能够让某个线程立刻停下。
threadLocal是一个线程私有的变量,对于每一个线程都拥有自己的一个ThreadLocal值,原理很简单,是在堆上创建了一个HAshmap,键是自己的ThreadID,而值是要存放的内容。在一些处理不当的情况下,可能会发生内存泄漏的情况。
这个类和ArrayList基本相同,有一些不同点,在于grow的增长速度是2倍而不是1.5倍,而且所有的方法都使用了snyc锁,这样的效率一定是有问题的。那么list有没有好的方法呢?也是有的,比如可以用CopyOfWriteArrayList,或者用concurrentHashMap也可以。
stack是vector的一个子类在vector的基础上增加了入栈和出栈方法。也是一个同步安全的类
这个类是hashmap的同步类,鉴于hashmap在实际应用中的广泛,这个类经常被我们提及,但是他的效率实在是不敢恭维,对每个方法加上sync锁。
conllections中提供了很多同步方法,基本实现也和上面的实现相同,简单粗暴加上sync锁,效率不高。
这个类是一个高效的hashmap同步类,在1.7以前和1.8以后的版本中有着不同的实现方式。在1.7之前会使用segment代替每个桶,segment是一个分段锁,每个线程在使用不同的segment的时候不会发生冲突。这样就用一种锁细化的方式提高了效率。在1.8之后,segment被去掉,因为有更简单的方法,就是在每个桶的第一个节点加锁,也能保证线程的安全性。在进行rehash扩容的时候会把完成的桶设置为forwarding,以此来提示其他线程正在进行扩容,其他线程可以通过此参与到扩容之中。
COW是一种比较常见的手法,如果不需要更新则不加锁,如果需要更新就把之前的内容拷贝出来全部更新后再放回去。这种手法叫做COW能应用于很多地方,比如每天进行一次COW,在一致性要求不高的业务上,是可以接受的。
CAS是一种类似于乐观锁的机制,或者说是:比较交换、if-then。在AQS和Atomic原子类等很多地方广泛应用此种方式进行加锁。计算机能够保证CAS的原子性是很关键的一个点,Unsafe是CAS调用的真正方法,这个类能够对内存进行分配,NIO的分配就基于此类。
AtomicInteger类可以对int数据做原子的加减操作,在内部原子类是通过CAS进行多线程并发处理的。
LongAdder和AtomicInteger没有太多区别,只不过LongAdder采用类似hashmap的方式把自己的long分成了很多cell,每个线程对自己的cell操作,需要合并的时候调用sum方法。
可见性,保证数据对于工作内存是可见最新的数据的。volite保证了可见性和避免了重排序,遵循happen-before语义。
线程间通信有很多种方式,但是大体分为两类,一类是通过共享变量来通信,包括:1.await、notify2.join3.volatile4.cyclicBarrier5.AtomicInteger6.synchronized7.ReentrantLock,Condition。另一类是通过线程间直接通信,包括:Future、PipedInputStream、PipedOutputStream。
JVM有一个“先行发生”(happen—before)的规则,它是内存模型中定义的两项操作之间的偏序关系,如果操作A - HB - B,其意思就是说:A这个操作对于B是可见的。通俗地说:只要A发生了变化,B一定能观察到。 1.volatile规则:volatile变量的写 - HB - volatile变量的读 2.锁规则:解锁(unlock) - HB - 加锁(lock) 3.传递性:A - HB - B,B - HB - C,A - HB - C 4.线程的start()方法 - HB - 它的每一个动作
下一篇:JAVA知识点全总结——(四)数据库