java多线程
进程
-
独立性
- 拥有独立资源
- 独立的地址
- 无授权其他进程无法访问
-
动态性
- 与程序的区别是:进程是动态的指令集合,而程序是静态的指令集合
- 加入时间概念
- 有自己的生命周期和不同的状态
-
并发性
- 多个进程可以在单核处理器并发执行
- 多个进程互不影响
- 不并行的区别是:并行是同一时刻多个进程在多个处理器同时执行,而并发是指在同一个时刻只能执行一条执行,但相互切换迅速,宏观上看是执行多个指令
线程
-
轻量级进程(Light Weight Process)
- 程序中的一个顺序控制流程,同时也是CPU的基本调度单位。
-
多线程
- 进程由多个线程组成,彼此间完成不同的工作,交替执行, 称为多线程。
-
执行特点:
-
线程抢占式执行
- 效率高
- 可防止单一线程长时间独占CPU
-
在单核CPU中,宏观上同时执行,微观上顺序执行。
-
-
组成
-
CPU时间片:操作系统(OS)会为每个线程分配执行时间。
-
运行数据:
- 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
- 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
-
线程的逻辑代码。
-
线程
- 用户态(应用程序)
- 内核态(系统调用cpu)
- 1:1
-
-
创建线程
-
创建线程第一种方式:继承Thread, 重写run()方法
- 1.继承Thread类
- 2.覆盖run()方法
- 3.创建子类对象
- 4.调用start()方法
-
创建线程第二种方式:实现Runnable接口
- 1.实现Runnable接口
- 2.覆盖run()方法
- 3.创建实现类对象
- 4.创建线程对象
- 5.调用start()方法
-
-
线程ID和名称
-
获取线程线程ID和线程名称
- 在Thread的子类中调用this.getId()或this.getName()
- 使用Thread.currentThread().getId()和Thread.currentThread().getName()
-
修改线程名称
- 调用线程对象的setName()方法
- 使用线程子类的构造方法赋值
-
-
线程方法
-
休眠:
-
public static void sleep(long millis)
-
当前线程主动休眠 millis 毫秒。
- sleep是线程进入休眠状态
- sleep释放cpu, 没有释放锁对象
-
-
-
放弃:
-
public static void yield()
- 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
-
-
加入:
-
public final void join()
- 允许其他线程加入到当前线程中。当前会阻塞,直到加入线程执行完毕。
-
-
优先级
-
线程对象.setPriority(int)
- 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。
-
-
线程打断
-
线程对象.interrupt()
- 打断线程,被打断线程抛出InterruptedException异常。
-
-
守护线程
- setDaemon(true)设置为守护线程。
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)。
- 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
- 垃圾回收器线程属于守护线程。
-
等待:
-
必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放 其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
-
public final void wait()
- wait是线程进入等待队列等待
- wait释放了cpu, 锁也被释放了
-
public final void wait(long timeout)
-
-
通知(唤醒):
- 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身 没有任何影响。
- public final void notify()
- public final void notifyAll()
- 注意: 唤醒的线程随机
-
-
线程同步
-
线程不安全:
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
-
同步代码块:
- synchronized(临界资源对象){ //对临界资源对象加锁 //代码(原子操作) }
- monitor
- 每个对象都有一个互斥锁标记,用来分配给线程的。
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
- 线程退出同步代码块时,会释放相应的互斥锁标记。
-
-
线程的状态
-
新建状态(New)
- 当一个线程的实例bai被创建即使用new关键字和duThread类或其子类创建一个线程对象后,此时该线程处于新zhi生(new)状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(notalive)。
-
就绪状态(Runnable)
-
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。 处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。此时线程是活着的(alive)。
-
JDK5之后就绪、运行统称Runnable
- 初始状态 Ready 就绪状态
- Running 运行状态
-
-
-
阻塞状态(Blocked)
-
阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。处于Blocking状态的线程仍然是活着的(alive)。
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
-
-
无限期等待(Waiting )
-
通过调用join() 使线程进入无限期等待状态,处于Blocking状态的线程仍然是活着的(alive)。
- 运行(running)的线程执行join()时,JVM会把该线程置为Waiting状态。当sleep()状态超时线程重新转入可运行(runnable)状态。
- 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。被唤醒时抢占cpu使用权,抢到以后继续执行后续代码,
-
-
限期等待(Timed Waiting)
-
通过调用Thread.sleep(long ms)使线程进入无限期等待状态,处于Blocking状态的线程仍然是活着的(alive)。
- 运行(running)的线程执行Thread.sleep(long ms)时,JVM会把该线程置为阻塞状态。当sleep()状态超时,线程重新转入可运行(runnable)状态。
-
-
终止状态(Terminated)
-
当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达终止状态。此时可能仍然存在一个该Thread的实例对象,当该Thread已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的callstack已经被dissolved。一旦某一线程进入终止状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于终止状态的线程调用start()方法,会出现一个运行期(runtimeexception)的异常;处于Dead状态的线程不是活着的(notalive)。 为了确定线程在当前是否存活着,需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
-
有两个原因会导致线程死亡:
-
- run方法正常退出而自然死亡,
-
- 一个未捕获的异常终止了run方法而使线程猝死。
-
-
-
-
-
生命周期
-
新建
- new一个Thread
-
就绪
- 执行start方法
-
运行
- 开始执行run方法
-
阻塞
-
进入阻塞
- 调用sleep方法主动放弃处理器资源
- 想获得一个同步检测器,但该同步检测器被其他资源所占有
- 调用一个阻塞时的IO方法,在该方法返回前,线程阻塞
- 等待某个notify通知
- 调用suspend挂起
-
解除阻塞
- 调用阻塞式IO方法已返回
- 成功后的了试图要得到的同步检测器
- 等待某个通知时,其他线程发出来通知
- 处于挂起的线程调用了resume方法
-
-
死亡
- run方法结束
- 程序抛出了一个未捕获的异常
- 调用stop方法
-
-
线程通信
-
等待:
- public final void wait()
- public final void wait(long timeout)
- 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放 其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
-
通知:
- public final void notify()
- public final void notifyAll()
- 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身 没有任何影响。
-
高级多线程
-
线程池
-
现有问题:
- 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
-
概念
- 线程容器,可设定线程分配的数量上限。
- 将预先创建的线程对象存入池中,并重用线程池中的线程对象。
- 避免频繁的创建和销毁。
-
原理
- 将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程。
-
-
步骤:
-
获取线程池
- 通过Executors工厂类获取线程池
-
创建线程
- Runnable runnable = new Runnable()
-
将线程(任务)提交给线程池
- executorService.submit(runnable);
-
关闭线程池
-
executorService.shutdown();
- //shutdown() 关闭 等待所有的任务执行完毕后才关闭
-
//shutdownNow() 尝试关闭当前正在执行的线程 关闭所有没有完成的任务
-
-
-
获取线程池
-
所在包java.util.concurrent
-
Executor:线程池的顶级接口。
-
ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。
-
Executors工厂类:通过此类可以获得一个线程池。
-
1 newFixedThreadPool() 固定大小线程池
- ExecutorService executorService = Executors.newFixedThreadPool(4);
-
2 newCacheThreadPool() 动态线程池
- ExecutorService executorService1 = Executors.newCachedThreadPool();
-
3 newSingleThreadPool() 单线程池
- ExecutorService executorService2 = Executors.newSingleThreadScheduledExecutor();
-
4 newScheduledThreadPool() 调度线程池
-
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1) ;核心线程数
-
延迟执行,只执行一次
- public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);1.线程对象2.延迟时间3.时间单位(枚举值)
-
周期执行,一秒执行一次,不能关闭线程池。
-
固定频率
- public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period, TimeUnit unit);1.线程对象2.线程开始延迟时间3.线程间延迟时间4.时间单位
-
固定延迟
- public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);1.线程对象2.线程开始延迟时间3.线程间延迟时间4.时间单位
-
-
-
-
使用ThreadPoolExecutor类 创建线程池
-
创建方法
-
public ThreadPoolExecutor(int corePoolSize,
- 核心线程数
-
int maximumPoolSize,
- 最大线程数
-
TimeUnit unit,
- 时间单位 枚举值 TimeUnit
-
long keepAliveTime,
- 线程保持存活时间
-
BlockingQueue workQueue,
- 请求队列
-
ThreadFactory threadFactory,
- 线程创建工厂
-
RejectedExecutionHandler handler);
-
拒绝策略(四种)
-
AbortPolicy 抛弃当前任务 抛出异常
- AbortPolicy 抛弃任务,并抛出异常 默认的拒绝策略
-
- 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。
-
- 如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
-
DiscardPolicy 放弃当前任务 不抛出异常
- DiscardPolicy 放弃任务,没有异常
-
- 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
-
DiscardOldestPolicy 放弃老的任务 添加拒绝的任务 放弃等待队列里面的
- DiscardOldestPolicy 放弃老的任务,添加拒绝的任务
-
- 此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
-
CallerRunsPolicy 线程池的创建线程程序执行,调用任务的线程执行
- CallerRunsPolicy 线程池的创建线程执行,主线程执行
-
- 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。
-
-
-
-
创建线程池演示代码
- ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
- threadPoolExecutor.submit(new Runnable()
- 创建三个任务 有两个在运行 一个在等待队列 在创建一个 到达最大线程数执行任务 在创建一个 会看拒绝策略
-
-
-
线程安全
-
Collections工具类中提供了多个可以获得线程安全集合的方法。
- public static Collection synchronizedCollection(Collection c)
- public static List synchronizedList(List list)
- public static Set synchronizedSet(Set s)
- public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
- public static SortedSet synchronizedSortedSet(SortedSet s)
- public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
-
JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchonized实现。
-
-
CAS算法
-
CAS:Compare And Swap(比较交换算法)
-
其实现方式是基于硬件平台的汇编指令,是靠硬件来实现的,效率高。(底层cpu指令)
-
并且比较和交换过程是同步的。
-
CAS是一种乐观锁。
-
CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)
- V:要更新的变量、E:预期值、N:新值。
- 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。
-
乐观锁:
- 总是认为是线程安全,不怕别的线程修改变量,如果修改了再重新尝试,直到成功。
-
悲观锁:
- 总是认为线程不安全,不管什么情况都进行加锁,要是获取锁失败,就阻塞。
- synchronized是悲观锁。
-
-
Callable接口
-
public interface Callable{ public V call() throws Exception; }
-
JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。
-
Callable具有泛型返回值、可以声明异常。
-
步骤:
-
//1创建Callable对象
- Callable callable=new Callable()
-
//2把可调用对象转成任务
- FutureTask task=new FutureTask<>(callable)
-
//3创建线程对象,把任务交给线程
- Thread thread=new Thread(task);
-
//4启动线程
- thread.start();
-
//5获取线程执行的结果,get()等待任务执行完毕才会继续执行。
- Integer sum = task.get();
-
-
-
Future接口
-
概念:
- 异步接收ExecutorService.submit()所返回的状态结果,当中包含了 call()的返回值。
-
方法:
- V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
-
-
Lock接口
-
JDK5加入,与synchronized比较,显示定义,结构更灵活。
-
提供更多实用性方法,功能更强大、性能更优越。
-
常用方法:
-
//创建锁
- Lock lock=new ReentrantLock();
-
void lock() //获取锁,如锁被占用,则等待。
-
boolean tryLock() //尝试获取锁(成功返回true。失败返回false,不阻塞)。
-
void unlock() //释放锁。
-
-
Condition
- Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现 等待/通知模式。
- Condition可以通俗的理解为条件队列。当一个线程在调用了await方法以 后,直到线程等待的某个条件为真的时候才会被唤醒。
-
JDK1.6之后synchronize的优化:
-
JDK1.6之前只有两种状态
- 无锁
- 重量级锁
-
JDK1.6之后四种状态
(只能升级不能降级)-
用户态(效率高)
- 无锁
- 偏向锁
- 轻量级锁
-
内核态
- 重量级锁
-
-
优化策略:
-
锁消除
-
局部变量
-
JVM配合
-
开启逃逸分析-XX:+DoEscapeAnalysis
-
开启锁消除标记-XX:+EliminateLocks
- JDK1.8自动开启
-
开启标量替换-XX:+EliminateAllocations
-
-
锁粗化
- for 循环里面上锁 经过锁粗化 在for循环外上锁
-
自旋锁(JDK1.4包含)
- 自旋锁是在轻量级锁升级重量级时使用的优化策略,在没有获得锁对象的情况下,先自旋等待,单位时间内获得锁对象,就不升级,否则升级成重量级锁
-
自适应自旋锁
-
-
-
synchronized和Lock区别
-
-
ReentrantLock(重入锁)
-
Lock接口的实现类,与synchronized一样具有互斥锁功能。
-
步骤
-
创建重入锁对象
- Lock lock = new ReentrantLock();
-
显示开启锁
- lock.lock();
-
显示释放锁
- lock.unlock();
-
-
考虑可能出现异常,释放锁必须放入 finally代码块中,避免无法释放。
-
-
ReentrantReadWriteLock(读写锁)
-
一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
-
支持多次分配读锁,使多个读操作可以并发执行。
-
互斥规则:
- 写-写:互斥,阻塞。
- 读-写:互斥,读阻塞写、写阻塞读。
- 读-读:不互斥、不阻塞。
- 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
-
代码
-
-
CopyOnWriteArrayList
- 线程安全的ArrayList,加强版读写分离。
- 写有锁,读无锁,读写之间不阻塞,优于读写锁。
- 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
- 使用方式与ArrayList无异。
- List list=new CopyOnWriteArrayList<>();
-
CopyOnWriteArraySet
- 线程安全的Set,底层使用CopyOnWriteArrayList实现。
- 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组,
- 如存在元素,则不添加(扔掉副本)。
- Set set = new CopyOnWriteArraySet<>();
-
ConcurrentHashMap
- 初始容量默认为16段(Segment),使用分段锁设计。
- 不对整个Map加锁,而是为每个Segment加锁。
- 当多个对象存入同一个Segment时,才需要互斥。
- 最理想状态为16个对象分别存入16个Segment,并行数量16。
- 使用方式与HashMap无异。
- JDK1.8改为CAS无锁算法。
- HashMap<String,String> map=new ConcurrentHashMap<>();
Queue接口(队列)
-
Collection的子接口,表示队列FIFO(First In First Out)
-
常用方法:
-
抛出异常:
- boolean add(E e) //顺序添加一个元素(到达上限后,再添加则会抛出异常)
- E remove() //获得第一个元素并移除(如果队列没有元素时,则抛异常)
- E element() //获得第一个元素但不移除(如果队列没有元素时,则抛异常)
-
返回特殊值:推荐使用
- boolean offer(E e) //顺序添加一个元素 (到达上限后,再添加则会返回false)
- E poll() //获得第一个元素并移除 (如果队列没有元素时,则返回null)
- E keep() //获得第一个元素但不移除 (如果队列没有元素时,则返回null)
-
-
ConcurrentLinkedQueue
-
创建:
- Queue queue = new ConcurrentLinkedQueue<>();
- queue.offer(“hello”);
- queue.poll();
- queue.peek();
-
线程安全、可高效读写的队列,高并发下性能最好的队列。
-
无锁、CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)
-
V:要更新的变量、E:预期值、N:新值。
-
只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。
-
-
BlockingQueue接口(阻塞队列)
-
Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法。
-
方法:
- void put(E e) //将指定元素插入此队列中,如果没有可用空间,则等待。
- E take() //获取并移除此队列头部元素,如果没有可用元素,则等待。
-
可用于解决生产生、消费者问题。
-
子类
-
ArrayBlockingQueue:
- BlockingDeque queue = new ArrayBlockingQueue<>(6);
-
LinkedBlockingQueue:
- BlockingDeque queue = new LinkedBlockingDeque<>(6);
-
-
多线程的三个特性
-
原子性(互斥性)
-
一个或多个操作不能被分割,要么全部执行,要么就都不执行
-
Atomic
- AtomicInteger integer2=new AtomicInteger(0);
- AtomicStampedReference integer=new AtomicStampedReference<>(0, 0);带版本号 解决ABA问题(第一个线程修改值,第二个线程又修改回来,第三个线程获取的值和期望值一样)
-
-
-
可见性
- 多个线程访问同一个变量,一个线程修改了这个变量,别的线程能立即看到修改的值
- volatile关键字保证内存可见性
-
有序性
- 程序执行的顺序按照代码的先后顺序执行,但是处理器为了提高程序运行效率,可能会对输入代码进行优化,他不保证程序中各个语句的执行顺序和编写顺序一致,但最终结果是一致的
- 处理为了优化会进行重排,单线程没有问题,多线程可能有问题,所以禁止重排序
-
synchronized 可保证原子性和可见性,但不能保证有序性.
-
volatile可保证可见性和禁止指令重排,但不能保证原子性.
-
Lock接口间接借助了volatile关键字实现了可见性和有序性