线程的那点事

线程安全概念

1.1 当多线程访问某一个类后者方法时,都能表现出正确的行为,那么这个类就是线程安全的。

synchronized:可以在任意对象或者方法上加锁,而加锁的这段代码称之为互斥区或者临界区。

当多个线程访问run方法时,以排队的方式(按照cpu分配的先后顺序而定)进行处理,一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized中的内容,拿不到锁这个线程就会不断尝试获得这把锁,直到拿到为止,而且是多个线程同时争夺这把锁(也就是锁竞争)。

1.2 synchronized取得锁都是对象锁,而不是把一段代码当锁,所以哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,两个对象,就获得两把不同的锁,他们互不影响。

有一种情况是当synchronized修饰的方法加上static后,就会成为类级别的锁,表示锁定.class类(独占.class类)

1.3 对象锁的同步异步

同步:其目的就是为了线程安全,其实对于线程安全还要满足两个特性:
原子性

可见性

1.4 脏读

1.5 锁重入

synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到了一个对象的锁后,再次请求此对象可以再次得到该对象的锁。

扫描二维码关注公众号,回复: 2632766 查看本文章

1.6 死锁

线程间通信

线程是操作系统中独立的个体,但这些个体如果不经过特殊处理就不能成为整体,线程间的通信就是成为整体的必用方式之一。可以使用wait/notify来实现线程间的通信(他们是object的方法)
1 wait和notify方法必须配合synchronized关键字使用

2 wait释放锁,notify不释放锁

2.2 模拟BlockingQueue

顾名思义他是一个阻塞队列,实现LinkedBlockingQueue的put和take

put:把obj放入队列中,如果队列没有空间,则调用此方法的线程被阻塞,知道队列有了空间再继续

take:取走队列收个位置的对象,若队列为空,阻断进入等待状态,知道有新的元素被加入队列。

2.3 ThreadLocal:线程局部变量,是一种线程间并发访问变量的解决方案,与synchronized加锁的方式不同,ThreadLocal完全不提供锁,而使用空间换时间的概念,为每个线程提供独立的副本,保证线程间的安全。

从性能上说,ThreadLocal不具备有绝对的优势,在并发不是很高时,加锁的性能会更好,但作为一套完全无锁的线程解决方案,在高并发和锁竞争激烈的场景,使用ThreadLocal可以比较好的减少锁竞争的问题。

容器

3.1 同步类容器都是线程安全的,但某些场景下需要加锁来保证复合操作,复合类操作如:迭代、跳转、以及条件运算。这些复合操作在多线程并发的修改内容时,可能会出现意外情况,原因是当迭代时并发的修改了容器中的内容。

同步类容器如:HashTable,其底层无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只有一个线程访问容器的状态。这很明显不满足今天高并发的需求,在保证线程安全的同时,也要保证性能问题。

3.2 并发类容器

jdk5以后提供了多种并发类容器来代替同步类容器而改善性能。同步类容器的状态都是串行化的。并发类容器是专门针对并发设计的,使用ConcurrentHashmap来代替传统的hashtable,添加了一些常见复合操作的支持。以及使用CopyOnWriteArrayList代替Voctor。

3.2.1 ConcurrentMap接口下有两个重要的实现:
ConcurrentHashMap

ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)

CocurrentHashMap内部使用段(Segment)来表示不同的部分,每个段其实就是一个小的HashTable,他们都有自己的锁,只要多个修改操作发生在不同的段上,他们就可以并发进行。其实就是把HashMap分成了16个段(Segment),也就是最高支持16个线程的并发修改,这也是在多线程并发场景时,降低锁竞争的一种方式,并且代码中大多共享变量使用了Volatile关键字声明,目的是第一时间获取被修改的内容,性能非常好。

3.2.2 CopyOnWrite容器

简称COW,有两种CopyOnWriteArrayList,CopyOnWriteArraySet。CopyOnWrite容器即写时复制的容器,通俗的理解就是当我们往容器中添加一个元素时,不直接往容器中添加,而实现将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完毕后,在将原容器的引用,指向新的容器。这样做的好处是我们可以对COW容器并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写是不同的容器。

队列(Queue)

4.1 ConcurrentLinkedQueue:一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue,他是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则,头是最先加入的,尾是最近加入的,该队列不允许NULL元素。

add(),offer()都是加入元素的方法(在ConcurrentLinkedQueue中这两个方法没有任何区别)

poll(),peek()都是去取头元素节点,区别在于前者会删除元素,后者不会。

阻塞队列:

ArrayBlockingQueue,LinkedBlockingQueue(可转变为无界队列),SynchronousQueue(同步Queue),PriorityBlockingQueue(优先级Queue)

模式

5.1 Futrue

Futrue优点类似于商品订单,比如网购时,购买商品,提交订单,当订单处理完成后,在家里等待商品送货上门即可。也比较类似于Ajax,页面异步处理,无需一直等待请求结果,可以继续浏览页面。

5.2 Master-Worker模式

Master-Worker模式是常用的并行计算模式。他的核心思想是系统由两类进程协作工作:Master和Worker进程。Master负责接收和分配任务,Worker负责处理子任务。当各个Worker子进程处理完成后,会将结果返回给Master,由Master作总结归纳。其好处就是可以将一个大任务分解成若干个小任务,并行执行,从而提供系统的吞吐量。

5.3 生产者消费者

生产者和消费者也是一个非常经典的多线程模式,我们在时间开发中应用非常广泛的思想理念。在生产消费模式中,通常有两类线程,即若干个生产者和消费者的线程。生产者负责提交用户请求,消费者线程负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。

线程池

为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效的进行线程控制。他们都在,java.util.concurrent包中,是JDK并发包的核心。其中有一个比较重要的类Executor:他扮演着线程工厂的角色,我们通过Executors可以创建具有特定功能的线程池。

Executors创建线程池的方法:

    newCachedThreadPool()方法:返回一个可根据实际使用情况调整线程个数的线程池。不限制最大线程数量,若有空闲的线程则执行任务,若无任务则不创建线程,并且每个空闲线程默认会在60秒后自动回收。

    newFixedThreadPool()方法:返回一个固定数量的线程池,该方法的线程数始终不变。当有一个任务提交时,若线程池中有线程空闲,则立即执行,若没有空闲线程,则被暂缓在一个任务队列中等待有空闲的线程去执行。

    newSingleThreadExecutor()方法:创建只有一个线程的线程池。若有空闲线程则立即执行,若没有,则被暂缓在任务队列中。

    newScheduledThreadPool()方法:该方法返回一个ScheduledExecutorService对象,具有定时器的功能。

    自定义线程池:若Executors工厂类无法满足我们的需求,可以自己去常见自定义线程池,其实Executors工厂类中所有的创建线程池方法均是用ThreadPoolExecutor这个类,这个类可以自定义线程,构造方法如下:

  

new ThreadPoolExecutor(int corePoolSize,                                //当前核心线程数(当线程刚new出来时线程池中线程个数)
                               int maximumPoolSize,                                 //最大线程数
                               long keepAliveTime,                                    //保持存活的时间(空闲时间)
                               TimeUnit unit,                                              //空闲时间单位
                               BlockingQueue<Runnable> workQueue    //任务队列
                               ThreadFactory threadFactory,                     //
                               RejectedExecutionHandler handler             //拒绝执行的任务

                )    

自定义线程池使用详细:

指定构造方法的队列是什么类型的比较关键:

    使用有界队列时,若有新的任务需要执行,如果线程池中实际线程数小于corePoolSize,则优先创建线程;若大于corePoolSize则将该任务加入任务队列中;若队列已满,则在线程总数不大于maxPoolSize的前提下创建新的线程;若线程数大于maxPoolSize则执行拒绝策略,或其他自定义方式。

    使用无界队列时:LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的问题。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务;当达到corePoolSize后就不再增加新的线程。若后续还有新任务到来,而又没有空闲的线程资源,则任务直接进入任务队列等待。若任务创建与执行的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

    JKD拒绝策略:

        AbortPolicy:直接抛出异常系统正常工作

        CallerRunPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。

        DiscardOldestPolicy:丢弃最早的一个任务,尝试再次提交当前任务。

        DiscardPolicy:丢弃无法处理的任务,不给予任何处理。

        其实这四种拒绝策略都比较暴力并不推荐。        

        推荐:如果需要自定义拒绝策略,可以实现RejectedExecutionHandler接口。

关于execute方法和submit方法:

    execute方法:没有返回值,只是执行线程任务。

    submit方法:会有一个Future的返回值,该类具有get()方法,当get结果为null时说明该线程执行完毕。

6.2 

CoutDwonLacth使用:用于一个线程等待多个线程的处理结果

经常用于监听某些初始化操作,主线程可以通过await()方法直接进入阻塞状态,其他线程进行初始化,初始化完毕后通过countdown()唤醒主线程继续执行操作。

CyclicBarrier使用:用于将多个线程同时执行
假设一个场景:每个线程代表一个运动员,当运动员都准备好后才一起出发,只要有一个人没有准备好就一起等待。

6.3 

Semaphore信号量非常适用于高并发访问,在新系统上线之前,要对系统的访问量进行评估。

相关概念:

PV:网站的总访问量,页面浏览量,或者点击量,用户每发起一次请求就会记录一次。
UV:访问网站的一台电脑客户端为一个访客,一般来讲,0点到24点之内相同的IP客户端只记录一次
QPS:即每秒查询数,qps很大程度上代表了业务的繁忙程度,每次请求的背后可能代表着多次的磁盘IO。通过QPS可以非常直观地了解当前网站的访问繁忙程度,一旦当前QPS超过预警阀值,可以考虑扩容,根据前期压测,和后期运维才能得到准确阀值。
RT:及请求的响应时间,这个指标直接说明用户的体验情况。

一般来讲对于系统的峰值评估采用2/8定律,即80%的请求会在20%的时间内到达,这样我们可以根据系统对应的PV计算出峰值QPS。

峰值QPS = (总PV * 80%)/(60 * 60 * 24 *20%)

然后再将总的峰值QPS除以单台机器所能承受的QPS值,就是所需要的服务器数量

以上不包括类似于秒杀,双十一这种大型促销活动的情况。

Semaphore可以控制系统的流量,拿到信号量的就进入,否则就等待。

其实分布式环境中更多还是使用redis来进行限流防刷。

Lock锁

(在1.8之前synchronized性能不如lock,8以后synchronized做了优化,性能已经差不多了,主要还是灵活性)

锁等待与锁通知

在使用synchronized时,如果需要多线程建进行协作工作则需要object的wait()和nitify()进行配合工作。

同样在使用lock时,可以使用一个新的等待通知的类,他就是condition,这个condition一定是针对具体某一把锁的,也就是只有在锁的基础上才能产生condition。

一把锁可以产生多个condition,这就是lock的灵活之处

重入锁:ReentrantLock

在需要同步的代码部分加上锁定,但不要忘记最后一定要释放锁,不然会造成锁永远无法释放,其他线程永远无法进来的情况。

公平锁:默认是非公平锁,公平锁意思是哪个代码先调用就先上锁,非公平锁是按照cpu分配来随机上锁。非公平锁效率要高于公平锁。

lock用法:

tryLock() 尝试获得锁,获得结果用true/false返回。
isFair() 是否是公平锁
isLocked() 是否已经锁定
getHoldCount() 获得调用该lock的次数
getQueueLength() 返回正在等待获取此锁的线程数
hasQueuedThread(Thread thread) 查询指定的线程是否正在等待此锁
hasQueuedThreads() 查询正在等待此锁的线程

读写锁:

ReentrantLockReadWriteLock

读写锁核心就是实现读写分离的锁,在高并发访问下,尤其是读多写少的情况下,性能远远优于重入锁。

读写锁本质分为两个锁,读锁和写锁。读锁下可以同时并发访问,但在写锁情况下,只能一个个顺序访问。读读共享,写写互斥,读写互斥。

猜你喜欢

转载自blog.csdn.net/Romantic_321/article/details/81088851