先了解线程的生命周期,上图。线程的生命周期从一个新的线程产生到结束中间会经历很是多的状况,大致上以下图,多线程环境下咱们主要是再running的时候采起线程的保护措施,从而使多线程环境下,让线程进入阻塞的状态。这种保护思想其实就是排他了,到最后都得一个个来,不管式任务仍是内存互不干扰,便达到线程安全了。java
线程的生命周期数据库
到了jdk8,内存模型已经有了至关的改变了,下图是小编学习了几篇优秀的博文学习,根据本身的理解绘制出来的,请多指教。编程
jdk8内存模型缓存
独立内存空间安全
从图中能够看出线程安全的区域是在栈空间,每一个线程会有独立的栈空间,从而也解释了为何方法内是线程安全的,而全局变量这些是线程不安全的,由于这些都在堆区。服务器
共享内存空间多线程
堆空间,和MateSpace是被全部线程共享的,所以在处理多线程问题的时候,其实主要是处理这两个空间的内容。共享区域在不加任何保护的状况下对其操做,会有异常结果。并发
package com.example.demo; import org.junit.Test; /** * Project <demo-project> * Created by jorgezhong on 2018/8/31 16:01. */ public class ThreadDemo { @Test public void extendThreadTest() { ExtendThread extendThread = new ExtendThread(); extendThread.start(); } class ExtendThread extends Thread { @Override public void run() { // TODO: 2018/8/31 } } }
@Test public void runnableThreadTest(){ RunnableThread runnableThread = new RunnableThread(); Thread thread = new Thread(runnableThread); thread.start(); } class RunnableThread implements Runnable{ @Override public void run() { // TODO: 2018/8/31 } }
@Test public void callableThreadTest(){ CallableThread callableThread = new CallableThread(); FutureTask<String> stringFutureTask = new FutureTask<>(callableThread); Thread thread = new Thread(stringFutureTask); thread.start(); } /** * 这种实现是由返回值的 */ class CallableThread implements Callable<String>{ @Override public String call() { // TODO: 2018/8/31 return ""; } }
补充:Fulture和Callable(Future模式)框架
首先,这两东西都在java.util.concurrent下,java自己就未多线程环境考虑了不少。看看下面的UML图,RunnableFuturej继承了Future和Runnable接口,将Future引入Runnable中,而且提供了默认实现FutureTask。RunnbleCallable和Future补充解决了两个问题,一个是多线程阻塞解决方案,另外一个则是返回值问题。咱们知道Runnable和Thread定义的run()是没有返回值的。并且当线程遇到IO阻塞的时候,只能等待,该线程没法作任何事情。Callable和Fulture分别解决了这两个问题。Callable提供了返回值的调用,而Fulture提供了多线程异步的机制。异步
Callable没什么好说的,例子如上面代码,就是多了个泛型的返回值,方法变成了call而已。Future就比较复杂了。FultureTask的构造方法接受Runnable或者Callable,也就是说Runnable和Callable的实例均可以使用Fulture来完成异步获取阻塞返回值的操做。
uml java fulture m
Future只有5个方法
Future模式缺陷
Fulture比较简单,基本上只经过两种方式:查看状态和等待完成。要么去查看一下是否是完成了,要么就等待完成,而线程和线程之间的通讯只有经过等待唤醒机制来完成。原来的Fulture功能太弱,以致于google的Guava和Netty这些牛逼的框架都是从新去实现以拓展功能。而java8引入了实现了CompletionStage接口的CompletableFuture。能够说是极大的扩展了Future的功能。吸取了Guava的长处。
关于CompletableFuture和的具体内容,后续再写一篇详细介绍。结合java8的Stream API CompletionStage接口定义不少流式编程的方法,咱们能够进行流式编程,这很是适用于多线程编程。CompletableFuture实现了该接口,并拓展了本身的方法。对比Fulture多了几十个方法。大体能够分为同步的和异步的两种类型。而做业的时候,能够切入任务某一时刻,好比说完成后作什么。还能够组合CompletionStage,也就是进行线程之间的协调做业。
咱们能够看到java线程池相关的包,他们之间的关系以下图。
java uml thread
java uml thread m
从uml类图能够看出(图片有点大,放大一下把),整个线程池构成实际上是这样的:
Executor
封装了线程的实现Executor
的子接口 ExecutorService
定义了管理 Executor
的一系列方法。 ThreadPoolExecutor
实现了 ExecutorService
,定义了一系列处理多线程的内容,好比线程工程和保存线程任务的队列ScheduledExecutorService
扩展了 ExecutorService
,增长了定时任务调度的功能。 ScheduledThreadPoolExecutor
实现了 ScheduledExecutorService
,同时继承 ThreadPoolExecutor
的功能Executors
静态类,包含了生成各类ExecutorService的方法。从接口的组成能够看出,Executor、ExecutorService和ScheduledThreadPoolExecutor三个接口定义了线程池的基础功能。能够理解为他们三个就是线程池。
那么整个线程池是围绕两个默认实现ThreadPoolExecutor和ScheduledThreadPoolExecutor类来操做的。
至于操做,我发现java还蛮贴心的,默认实现的线程池只区分了可定时调度和不可定时调度的。实在是太过于灵活了,本身使用的话要配置一大堆参数,我想个线程池而已,给我搞这么多配置表示很麻烦,只须要关心是否是定时的,只考虑我分配多少线程给线程池就行了。所以有了Executors
Executors操做两个默认的实现类,封装了了大量线程池的默认配置,并提供了如下几种线程池给咱们,咱们只须要管线少部分必要的配置便可。
ExecutorService pool = Executors.newSingleThreadExecutor(); //提交实现到线程池 pool.submit(() -> { // TODO: 2018/8/31 do something });
ExecutorService pool = Executors.newCachedThreadPool(); //提交实现到线程池 pool.submit(() -> { // TODO: 2018/8/31 do something });
//参数为线程数 ExecutorService pool = Executors.newFixedThreadPool(8); //提交实现到线程池 pool.submit(() -> { // TODO: 2018/8/31 do something });
//参数为线程数 ScheduledExecutorService pool = Executors.newScheduledThreadPool(8); /* * 提交到线程池 * 参数1:Runnable * 参数2:初始延迟时间 * 参数3:间隔时间 * 参数4:时间单位 */ pool.scheduleAtFixedRate(() -> { // TODO: 2018/8/31 do something }, 1000, 2000, TimeUnit.MILLISECONDS);
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor(); //参数少了初始延迟时间 pool.schedule(() -> { // TODO: 2018/8/31 do something }, 1000, TimeUnit.MILLISECONDS);
一、 考虑业务类型
除了考虑计算机性能外,更多的仍是考虑业务逻辑,若是业务是运算密集型的,不适合开太多的线程,由于运算通常是cpu在算,cpu自己就是用于计算,极快,所以一个线程很快就能计算完毕。线程多了反而增长了资源的消耗。另外一种是IO密集型业务,这种业务就比较是适合开多一点线程,由于IO、通讯这些业务自己就是很是慢的,大部分的系统的瓶颈都集中这两方面。所以这些业务适合开多个线程。
二、配合cpu的核心和线程数
在咱们配置线程的时候,能够参考cpu的总线程,尽可能不超出总线程数。通常使用核心数。
这实际上是一个监视器。能够监视类和对象。
原理:能够这么理解,每一个实例化的对象都有一个公共的锁,该锁被该实例共享。所以对于该对象的全部被synchronized修饰的实例方法,是共享的同一个对象锁。同理,类锁也是同样的,伴随Class对象的生成,也会有一个类监视器,也就有一个默认的类锁了,被synchronized修饰的全部静态方法都共享一个类锁。
缺陷:同步锁关键子虽然方便,可是毕竟是被限制了修饰方式,所以不够灵活,另外修饰在方法上是修饰了整个方法,所以性能在并发量大且频繁的时候就显得不那么好了。
public synchronized void synchronizedMethod(){ // TODO: 2018/8/29 do something }
public static synchronized void synchronizedMethod(){ // TODO: 2018/8/29 do something }
public void synchronizedMethod(){ //Object.class为锁对象,其实就是锁的钥匙,使用同一把钥匙的锁是同步的 synchronized (Object.class){ // TODO: 2018/8/29 do something } }
因为synchronized的缺陷不够灵活,对应的天然有灵活的解决方案。Lock即是解决方案。Lock是java.util.concurrent.locks包下的一个接口。可是Lock是灵活了,可是既然都多线程了,咱们固然是最求性能啦。因为不少数据是对查看没有线程安全要求的,只须要对写入修改要求线程安全便可,因而有了ReadWriteLock,读写锁能够只对某一方加锁,把锁住的内容范围更加缩小了,提高了性能。从下图能够看到,ReentrantLock实现了Lock而ReentrantReadWriteLock实现了ReadWiteLock。咱们能够直接使用它们的实现类实现锁功能。
uml_java_lock
获取锁:lock()、tryLock()、lockInterruptibly()
释放锁:unLock()
直接上代码来学习效果是最快的
package com.example.demo; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.ReentrantLock; /** * Project <demo-project> * Created by jorgezhong on 2018/8/30 15:48. */ public class LockDemo { private static final Logger LOGGER = LoggerFactory.getLogger(LockDemo.class); /** * 两个线程争取同一把锁 */ @Test public void lockTest() throws InterruptedException { //造一把锁先 ReentrantLock reentrantLock = new ReentrantLock(); Thread thread0 = new Thread(() -> { for (int i = 0; i < 5; i++) { lockTestHandle(reentrantLock); } }); Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { lockTestHandle(reentrantLock); } }); thread0.start(); thread1.start(); while (thread0.isAlive() || thread1.isAlive()) {} } private void lockTestHandle(ReentrantLock reentrantLock) { try { // 加锁 reentrantLock.lock(); LOGGER.info("拿到锁了,持有锁5s"); Thread.sleep(5000); } catch (Exception e) { // TODO: 2018/8/30 do something } finally { // 记得本身释放锁,否则形成死锁了 reentrantLock.unlock(); LOGGER.info("释放锁了"); } } }
运行结果:咱们能够看到,循环的代码是连续的,没有被其余线程干扰。确实是锁上了,使用同一个锁,必须等一个释放了另外一个才能持有。一个线程持有锁,其余使用同一把锁的线程就会同步阻塞,从新持有锁以后才会结束阻塞的状态,才能往下执行代码。
16:36:05.740 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了 16:36:05.744 [Thread-0] INFO com.example.demo.LockDemo - 循环:0 持有锁 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:1 持有锁 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:2 持有锁 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:3 持有锁 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:4 持有锁 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 释放锁了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:0 持有锁 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:1 持有锁 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:2 持有锁 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:3 持有锁 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:0 持有锁 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:1 持有锁 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:2 持有锁 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:3 持有锁 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了 16:36:05.747 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了 ...... 16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁 16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:0 持有锁 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:1 持有锁 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:2 持有锁 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:3 持有锁 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:4 持有锁 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 释放锁了
/** * lockInterruptibly:加了可中断锁的线程,若是在获取不到锁,可被中断。 * <p> * 中断实际上是使用了异常机制,当调用中断方法,会抛出InterruptedException异常,捕获它可处理中断逻辑 */ @Test public void lockInterruptiblyTest() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); Thread thread0 = new Thread(() -> { try { lockInterruptiblyTestHandle(reentrantLock); } catch (InterruptedException e) { LOGGER.info("被中断了"); } }); Thread thread1 = new Thread(() -> { try { lockInterruptiblyTestHandle(reentrantLock); } catch (InterruptedException e) { LOGGER.info("被中断了"); } }); thread1.setPriority(10); thread1.start(); thread0.start(); Thread.sleep(500); thread0.interrupt(); while (thread0.isAlive() || thread1.isAlive()) {} } private void lockInterruptiblyTestHandle(ReentrantLock reentrantLock) throws InterruptedException { /* * 加锁不能放在try...finally块里面,会出现IllegalMonitorStateException,意思是当lockInterruptibly()异常的时候,执行了unlock()方法 * 其实就是加锁都抛出异常失败了,你还去解锁时不行的。放外面抛出异常的时候就不会去解锁了 */ reentrantLock.lockInterruptibly(); try { LOGGER.info("拿到锁了,持有锁5秒"); Thread.sleep(5000); } finally { // 释放锁 reentrantLock.unlock(); LOGGER.info("释放锁了"); } }
从结果能够看到,thread-0被中断了以后再也不继续执行
20:11:22.227 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了,持有锁5秒 20:11:22.742 [Thread-0] INFO com.example.demo.LockDemo - 被中断了 20:11:27.231 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了 Process finished with exit code 0
ReadWriteLock只是定义了读锁和写锁两个方法,其具体实现和拓展再默认实现ReentrantReadWriteLock中。简单来讲读写锁呢,提供读锁和写锁,将读和写要获取的锁类型分开,用一个对列来管理,全部的锁都会通过队列。当须要获取写锁的时候,后买的读写锁获取都须要等待,知道该写锁被释放才能进行。
@Test public void readWriteLockTest(){ ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); try { readEvent(); } catch (Exception e) { LOGGER.error(e.getMessage(),e); }finally { readLock.unlock(); } ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); try { writeEvent(); } catch (Exception e) { LOGGER.error(e.getMessage(),e); }finally { writeLock.unlock(); } } private void writeEvent() { // TODO: 2018/9/3 done write event } private void readEvent() { // TODO: 2018/9/3 done read event }
总的来讲:凡是遇到写,阻塞后面的线程队列,读与读是不阻塞的。
volatile可修饰成员变量,能保证变量的可见性,可是不能保证原子性,也就是说并发的时候多个线程对变量进行计算的话,结果是会出错的,保证可见性只是能保证每一个线程拿到的东西是最新的。
对于volatile来讲,保证线程共享区域内容的可见性能够这么来理解,堆内存的数据原来是须要拷贝到栈内存的,至关于复制一份过去,可是呢。再不加volatile的时候,栈区计算完以后在赋值给堆区,问题就产生了。加了volatile以后,线程访问堆区的数据以后,堆区必须等待,知道栈区计算完毕将结果返回给堆区以后,其余线程才能继续访问堆区数据。
public volatile String name = "Jorgezhong";