基础_多线程

1. 创建线程的方法:

(1)继承Thread类

(2)实现Runnable接口或Callable,优点是可继承多个接口,更加灵活,能减少程序之间的耦合度;可实现多个线程共享一个target,共同处理一份共享资源;

1. Runnable接口或Callable接口区别:

Runnable接口中的run()方法的返回值是void;而Callable接口中的call()方法是有返回值和抛出异常,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果;即Callable+Future/FutureTask却可以获取多线程运行的结果。

1. 保证线程顺序执行方法

a. 使用线程的join方法,该方法的作用是“等待线程执行结束”,即join()方法后面的代码块都要等待现场执行结束后才能执行 ,如在t2中执行t1.join();---- 貌似不管用

b. 通过SingleThreadExecutor,即单线程执行器,本质是个队列;SingleThreadExecutor的工作线程只有一个,其他队列中的线程都处于休眠,也就是sleep状态,当这个worker线程做完事了,也就是run方法运行结束,就又从队列中拿出一个休眠线程(sleep)出来唤醒(notify),这样依次把队列中的所有线程处理完毕,这样并没有结束,如果线程池中没有待处理的线程,线程池一直会等待,等待下一次任务的提交,除非把线程池给shutdown掉,这样线程池的生命周期才算完毕。

c. 通过标志变量+wait()/notifyall或condition来配合实现线程通信控制;

详见:https://blog.csdn.net/eene894777/article/details/74942485 保证三个线程依次按顺序执行

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

http://www.zhimengzhe.com/bianchengjiaocheng/qitabiancheng/293596.html --- java中一个线程等待另一个线程执行完后再执行

2. 线程安全:

(1)可以使用同步代码块synchronized、同步方法synchronized,本质是对当前对象加同步监视器;

可以使用Lock,包括:读写锁、可重入锁、可重入读写锁,需要显式释放锁,相比同步更灵活;

(2)释放同步监视器情况:代码结束、遇到return\exception\wait,不释放情况:Thread.sleep(),Thread.yield();

3. 线程通信:

(1)使用Object提供的wait()\notify()\notifyAll()三个方法,必须由同步监视对象来调用;-> 使用前提是synchronized,具备同步监视对象;

(2)使用Lock对象Condition实例,即lock.newCondition(),然后调用其3个方法:await/signal/signalAll(); ----> 非synchronized的情况;

3. 线程池:

(1)4中静态创建线程池的方法:调用Executors的4个静态方法

newFixedThreadPool 、newSingleThreadExecutor、newCachedThreadPool 、newScheduledThreadPool、自定线程池(如spring框架里的ThreadPoolTaskExecutor)

自定义线程池:配置核心线程数和最大线程数不同,任务队列使用有界队列LinkedBlockingQueue

阿里规约建议线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资激耗尽的风险。说明:Executors各个静态方法的弊端:

1) newFixedThreadPool / newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

}

public LinkedBlockingQueue() {

this(Integer.MAX_VALUE);

}

2) newCachedThreadPool / newScheduledThreadPool:

主要问題是线程数最大数是Integer.MAX_VALUE,可能会创建数置非常多的线程,甚至OOM。

public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue<Runnable>());

}

(2)创建线程池通常的参数:核心线程数、最大线程数、keepalived时间、时间单位、线程池工厂、工作队列、拒绝/饱和策略、

工作队列类型:无界队列(LinkedBlockingQueue-固定和单线程池使用)、有界队列(ArrayBlockingQueue或有界的LinkedBlockingQueue-自定义线程池使用)、直接提交(SynchronousQueue-缓存线程池使用)

拒绝/饱和策略:中止策略(默认策略-丢弃任务并抛异常)、抛弃策略(抛弃任务不抛异常)、抛弃最旧策略(丢弃队列最前面的任务)、调用者运行策略(调用线程中处理)

threadFactory有两种选择:(1)DefaultThreadFactory,将创建一个同线程组且默认优先级的线程;(2)PrivilegedThreadFactory,使用访问权限创建一个权限控制的线程。ThreadPoolExecutor默认采用DefaultThreadFactory

https://blog.csdn.net/kai_wei_zhang/article/details/8207586 --- 线程工厂 ThreadFactory源码解读 好!!!

(3)线程池原理和分配过程:

当有新任务要执行时,先判断当前运行线程数如果小于核心线程数,则马上创建线程并运行任务(详细过程为:用线程工厂创建一个新线程,设为worker属性,worker里面启动线程进行执行,并一直循环从任务队列里面取未执行任务进行执行);如果大于等于核心线程数,则将任务推入任务队列;如果队列满了,则创建非核心线程运行任务;如果队列满且正在运行线程数大于等于最大线程数,那么就执行4中拒绝策略;

线程池原理:线程池工厂本质是享元模式,负责线程的创建和管理;???

(4)ExecutorService 的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了 shutdown()方法时,便进入关闭状态,此时意味着 ExecutorService 不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用 shutdown()方法,ExecutorService 会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

参见:

http://wiki.jikexueyuan.com/project/java-concurrency/executor.html 并发新特性—Executor 框架与线程池 -- 好

https://blog.csdn.net/he90227/article/details/52576452 --理解线程池的原理 -- 好!!!!

https://www.jianshu.com/p/5df6e38e4362 -- 当面试官问线程池时,你应该知道些什么?

https://www.cnblogs.com/CESC4/p/8041198.html --- 谈谈连接池、线程池技术原理 -- 好

https://www.cnblogs.com/studyLog-share/p/5286290.html java并发:线程池、饱和策略、定制、扩展 -- 好

3. 定时线程池或定时任务

ScheduledExecutorService比Timer更安全,功能更强大,后面会有一篇单独进行对比;

https://www.cnblogs.com/zhujiabin/p/5404771.html 四种常用线程池

http://www.cnblogs.com/0201zcr/p/4703061.html java Timer(定时调用、实现固定时间执行)

4. 并发相关

(1) CountDownLatch:

本质是个计数器,可以实现一个线程等待其他指定个数的线程都执行完后再执行,每个其他线程里会进行减数;

详见:https://www.cnblogs.com/bqcoder/p/6089101.html --- CountDownLatch使用场景及分析

https://www.cnblogs.com/uodut/p/6830939.html -- java并发之同步辅助类(Semphore、CountDownLatch、CyclicBarrier、Phaser)

(2)CyclicBarrier和CountDownLatch的区别:

简介:CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。

a.CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行,即:等各线程到达集合点后交给后面的线程做汇总;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行,即:实现一个线程等待其他指定个数的线程都执行完后再执行;

c. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了;

b. CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务;

(5) 并发线程池的线程数处理:

a. 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

b.并发不高、任务执行时间长的业务要区分开看:

假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

5. 锁相关:

(1)死锁:

当两个线程互相等待对方持有的锁资源时会导致死锁;

避免死锁的方法:a. 按照相同的顺序加锁; b.获取锁时加一个超时时间; c.死锁检测:死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁和锁超时也不可行的场景。通过对访问锁的线程、访问的锁、已获取的锁作标记然后检测是否存在互相等待对方释放锁的场景,如果存在则死锁发生了。此时应该:让一方的线程释放所有锁,随机等待然后重试;更好的方案是给一部分线程设置随机的优先级,让优先级低的那些线程回退,从而避免死锁。

(2)锁类型:

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

可重入锁/不可重入锁:可重入锁(最大优势是可避免死锁);

公平锁/非公平锁:公平锁就是先等待的线程先获得锁,非公平锁是上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

排他锁/共享锁:

无锁状态/偏向锁/轻量级锁/重量级锁:指锁的状态,为了对synchronized同步关键字进行优化而产生的的锁机制,是通过在对象头中的Mark Word字段来进行标识的;--偏向锁(是指一段同步代码一直被一个线程所访问,即对象的锁偏向某个线程,那么下次该线程会自动获取锁,降低获取锁的代价;适用于单线程或锁几乎无竞争的场景),--轻量级锁(是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,该线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;优点是响应快,缺点是消耗CPU);重量级锁(是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低;优点是不消耗CPU,缺点是响应慢,适用于高吞吐量);

详见:https://blog.csdn.net/L_BestCoder/article/details/79296130 偏向锁、轻量级锁、重量级锁、自旋锁原理讲解 -- 好!!

乐观锁和悲观锁:--乐观锁(每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁),--悲观锁(还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了)---总结:悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。 悲观锁在Java中的使用,就是利用各种锁。 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新

(3)具体锁:

Synchronized: 本质是可重入锁,排他锁,阻塞锁;JDK1.6之前的Synchronized是重量级锁,本质是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK1.6之后引入了“偏向锁”和“轻量级锁”,即通过对象头里面的两位来标识锁状态,实现无锁 --> 偏向锁 --> 轻量级 --> 重量级

ReentranctLock: 本质是可重入锁,排它锁,一定程度避免死锁,原理是CAS+自旋,本质是state+队列,先CAS获取锁,失败则加入队列,自旋检测前驱是否释放锁;由于内部使用了CAS,比JVM的Synchronized性能要好点;

ReentrantReadWriteLock:共享锁,实现了读写的分离, 读锁是共享的,写锁是独占的 ,读和读之间不会互斥;

ConcurrentHashmap: 用分段锁segment,继承ReentrantLock,加锁过程通过hashcode确定所在段,对该分段加锁;默认并发度是16;

AtomaticInteger: 本质是乐观锁,原理是使用CAS自旋实现原子操作;

Semaphore作用:Semaphore就是一个信号量,它的作用是 限制某段代码块的并发数 。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

CAS锁:

高并发CAS修改两个变量:

总结:

参考:

https://mp.weixin.qq.com/s/xrCPJ9HLt-_7NdW6GVCQgQ JAVA 同步实现原理 --- 好!!!

http://www.importnew.com/22078.html --- AtomicInteger源码分析——基于CAS的乐观锁实现

http://baijiahao.baidu.com/s?id=1595507849295508073&wfr=spider&for=pc 编程中的14种锁,你知道几个?

https://blog.csdn.net/a314773862/article/details/54095819 各种锁简介

https://blog.csdn.net/yanyan19880509/article/details/52345422 --- 可重入锁实现原理 -- 好!!

https://blog.csdn.net/u010900754/article/details/77504958 --- 可重入锁详细原理 -- 好!!!

https://www.cnblogs.com/qifengshi/p/6831055.html --- java中锁分类 -- 好!!!

https://blog.csdn.net/zqz_zqz/article/details/70233767 -- 理解锁的基础知识 -- 好!!

https://blog.csdn.net/sinat_28028941/article/details/53539775 -- CAS原理

(4)synchronized和ReentrantLock的区别: 相同点是都是阻塞锁

ReentrantLock可以对获取锁的等待时间tryLock(xxx)进行设置,这样就避免了死锁;

ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。公平锁就是先等待的线程先获得锁。

JDK 1.5中synchronized还有很大的优化余地(ReenTrantLock内部使用了CAS,比JVM的Synchronized性能要好点)。JDK 1.6 中加入了很多针对锁的优化措施(如偏向锁、轻量级锁等),synchronized与ReentrantLock性能方面基本持平。虚拟机在未来的改进中更偏向于原生的synchronized。

ReenTrantLock可以灵活地实现多路通知,ReentrantLock可以同时绑定多个Condition对象(只需多次调用newCondition方法即可),用来实现分组唤醒需要唤醒的线程们;而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程,synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件。但如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。

synchronized可以自动加锁和释放锁,ReentrantLock必须手动释放;

synchronized是关键字,是JVM实现的;ReentrantLock是类,JDK实现的;

详见:https://blog.csdn.net/zheng548/article/details/54426947 synchronized和锁(ReentrantLock) 区别

6. FutureTask

表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

7. 其他线程相关:

(1)servlet是否是线程安全的?不是,因为serlvet是单例模式,如果多个请求同时访问同一个servlet,会并行调用servlet的service()方法,里面如果有实例变量或全局变量不安全;

(2) StringBuilder和StringBuffer实现了相同的接口,执行效率也一样,但StringBuffer是线程安全的,效率稍微低点;String是常量值,线程安全,但加减字符串时执行效率低;

8. Violatile

并发编程中有三个问题:原子性,可见性和有序性。Violatile能保证可见性和禁止指令重排序;使用Violatile修饰的变量在汇编阶段,会多出一条lock前缀指令,lock前缀指令实际上相当于一个内存屏障,作用有3个:

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
  2. 它会强制将对缓存的修改操作立即写入主存。
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

通常处理器和内存之间都有几级缓存来提高处理速度,处理器先将内存中的数据读取到内部缓存后再进行操作,但是对于缓存写会内存的时机则无法得知,因此在一个处理器里修改的变量值,不一定能及时写会缓存,这种变量修改对其他处理器变得“不可见”了。但是,使用Volatile修饰的变量,在写操作的时候,会强制将这个变量所在缓存行的数据写回到内存中,但即使写回到内存,其他处理器也有可能使用内部的缓存数据,从而导致变量不一致,所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,如果过期,就会将该缓存行设置成无效状态,下次要使用就会重新从内存中读取

https://mp.weixin.qq.com/s/UhCg0m7KJOZ1gQHukm3PDg 面试系列-volatile关键字详解;

其他参考:

http://www.importnew.com/12773.html ----- Java线程面试题 Top 50

猜你喜欢

转载自blog.csdn.net/zxb448126/article/details/81208552