-
java中启动新线程的方式
-
概述:
-
java天生就是多线程的,只有两种新线程的方式面试亮点
- 派生自Thread
- 实现Runnable接口
- 这个Callable,Thread是不能接受这个实例的,没有这种实例
- 线程池里面也是不行的
-
-
-
java中的线程的状态(生命周期)
-
终止一个线程:
- 线程执行完run方法后抛出未被捕获的异常
- 调用interrupt
-
线程的状态:六种
-
初始:new出了一个新的线程对象
- 但是new 出了一个新的线程对象并不意味着这个线程启动了,需要调用start方法,这个时候线程进入运行态(就绪态,运行中)
-
运行态
- 运行中:线程被分配了CPU时间片
- 就绪态:时间片用完了,或者被OS剥夺,或者自己放弃;那么就等OS分配时间片
-
等待状态:调用wait()
- 调用:notify,notifyAll
-
等待超时:
- wait是自己是不带时间戳(长度),没有人唤醒他,那么它就会一直等待
- 此时可以在调用wait方法的时候传入一个时间长度,超过了这个时间还没有被唤醒,那么就自己从等待态恢复到就绪或者运行(要拿到CPU分配的时间片)
-
阻塞态:
- 被同步代码块包裹(synchronized)的时候没有拿到锁,就阻塞;重新获取锁就进入运行态
-
终止状态:线程执行结束
示意图:
-
-
- 常见问题:
1. 调用sleep(1000)后,线程等待1S,这个时候线程进入什么状态?
- 等待状态(主动调用某个方法后进来的)
- 不会进入等待超时,sleep方法一般是带有时间戳的
1. 如果使用了显示锁:比如说调用了lock但是没有拿到锁,那么线程进入什么状态?
- 不会进入阻塞(被动进入,拿不到锁,不得不进来)态:只有在调用synchronized关键字之后,拿不到锁,才会进入阻塞态
- lock这种显示锁,在底层中使用的是LookSupport;
- 没有拿到锁,这个时候会进入等待态或超时等待
- 底层就很麻烦,在Linux底层中有API
1. 在操作系统中会将运行态中的两个分开,java是合起来
-
死锁:在计算机诞生的那一天就存在
-
概述:
- 两个(或以上)的进行在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象
- 如果没有外力作用,那么他们无法继续推进下去,此时称系统处于死锁状态
-
死锁的必要条件:
-
多个操作者(M>=2),争夺多个资源(N>=2),N<=M
- 当单操作者的情况下,不会造成死锁
- 当资源数只有一个,也不会造成死锁
-
操作者争夺资源的顺序不对
- 规定了资源竞争循序,也不会造成死锁
-
操作者拿到资源不放手
- 放手就不会有死锁
-
-
学术化定义:满足死锁
- 互斥条件,资源别操作者独享
- 请求与保持,拿到了一个资源请求其他线程的资源
- 资源不剥夺,线程拿到的资源不能被剥夺
- 环路等待,拿了A想拿B,另外一个拿了B想拿A
-
避免死锁的算法
- 有序资源分配法
- 银行家算法
-
代码实现
-
定义两个锁
private static Object No1 = new Object(); private static Object No2 = new Object(); -
构建两个拿锁的方法
//第一个拿锁的方法 private static void thread1Catch() throws InterruptedException{ String threadName = Thread.currentThread().getName(); synchronized (No1){ System.out.println("thread1Catch catach the No1"); Thread.sleep(1000); synchronized (No2){ System.out.println("thread1Catch catch the No2"); } } } //第二个拿锁的方法 private static void thread2Catch() throws InterruptedException{ String threadName = Thread.currentThread().getName(); synchronized (No2){ System.out.println("thread2Catch catach the No1"); Thread.sleep(1000); synchronized (No1){ System.out.println("thread2Catch catch the No2"); } } } -
添加两个线程
//子线程:Thread1 private static class Thread1 extends Thread{ private String name; public Thread1(String name){this.name = name;} @Override public void run() { Thread.currentThread().setName("name"); try { thread1Catch(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { //主线程,代表Thread2 Thread.currentThread().setName("Thread2"); Thread1 thread1 = new Thread1("Thread1"); thread1.start(); thread2Catch(); }
-
-
死锁的表现:
- 程序活的好好的,但是线程不干事情;
- 程序无法自己解决这个问题,只能重开
- 没有任何的报错信息,有相关的日志
-
死锁的解决办法:
-
强制规定资源的竞争顺序,每次竞争同一个资源
-
解决拿到资源不放手的问题?
-
Lock接口:这个是在synchronized之外的,并且这个是花样拿锁
-
花样拿锁
- 拿锁的时候可以中断
- 也可以尝试去拿锁
-
-
-
-
使用Lock解决死锁问题
-
概述:采用尝试拿锁的机制,tryLock():返回true,代表拿到了锁;然后再尝试去拿第二把锁;如果说拿到了,那么就进行业务代码;但是在第二次拿锁的时候,如果拿不到第二把锁,就将第一把锁也放弃;将这个代码块放在循环中;整体来说,同一时刻,只能有一个线程,拿到两把锁 即使两个线程拿锁的顺序不同也无所谓了
//代码示例: private static Lock No13 = new ReentrantLock();//第一个锁 private static Lock No14 = new ReentrantLock();//第二个锁 //先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉 private static void fisrtToSecond() throws InterruptedException { String threadName = Thread.currentThread().getName(); Random r = new Random(); while(true){ if(No13.tryLock()){ System.out.println(threadName +" get 13"); try{ if(No14.tryLock()){ try{ System.out.println(threadName +" get 14"); System.out.println("fisrtToSecond do work------------"); break; }finally{ No14.unlock(); } } }finally { No13.unlock(); } } //Thread.sleep(r.nextInt(3)); } } -
实现细节:为什么在while循环的末尾要加上短暂的休眠时间
-
如果说去掉这个,将导致拿锁的时间被大大延长,加一个sleep(让出CPU,不会释放锁,写代码的时候一般将其放在synchronized之外),让他们拿锁的时间错开一点(很少的时间),此时引入活锁的概念
-
活锁:一直在拿锁,释放锁
-
概述:两个线程看起来十分忙碌,但是没有干什么实际的事情
-
死锁:就是一直阻塞掉了,
//线程A拿到了锁1<1>,此时尝试去拿锁2,拿不到,将锁1释放 A<1>(2)---()()---<1><2>-----()()---<1><2>--…… B<2>(1)---()()---<2>(1)---()()---<2>(1)…… -
资源:锁,打印机的外设,句柄等能够被多线程持有的
-
-
-
-
重要概念
- 线程饥饿:线程的优先级太低了,一直拿不到CPU分配的时间片
- 锁的初始化:
- 锁的优化:
-
-
ThreadLocal
-
概述:线程本地变量,也可以叫线程的本地存储;可以让线程拥有一份属于自己的本地变量副本,不会与其他的线程的变量副本冲突,实现了线程的数据隔离
-
近似理解:跟Map做类比
-
相当于搞了一个Map(<thread1,value>):根据线程拿到value,处理好了,然后再将value扔进去就行了
public class MyThreadLocal<T> { /*存放变量副本的map容器,以Thread为键,变量副本为value,而且为了线程安全都加上锁*/ private Map<Thread,T> threadTMap = new HashMap<>(); public synchronized T get(){ return threadTMap.get(Thread.currentThread()); } public synchronized void set(T t){ threadTMap.put(Thread.currentThread(),t); } } -
测试系统的ThreadLoacal
package cn.enjoyedu.concurrent.base.threadlocal; /** *@author Mark老师 享学课堂 https://enjoy.ke.qq.com * *类说明:演示ThreadLocal的使用 */ public class UseThreadLocal { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); // static MyThreadLocal<String> threadLocal = new MyThreadLocal<>(); /** * 运行3个线程,每个线程都给他搞一个id,第0号进程的id就是0 */ public void StartThreadArray(){ Thread[] runs = new Thread[3]; for(int i=0;i<runs.length;i++){ runs[i]=new Thread(new TestThread(i)); } for(int i=0;i<runs.length;i++){ runs[i].start(); } } /** *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响; * 将线程的自定义id(0号进程的id就是0)通过set传进去,通过get打印出来 */ public static class TestThread implements Runnable{ int id; public TestThread(int id){ this.id = id; } public void run() { String threadName = Thread.currentThread().getName(); threadLocal.set("线程"+id); if(id==1) { threadLocal2.set(id);//线程1才会执行 } System.out.println(threadName+":"+threadLocal.get()); } } public static void main(String[] args){ UseThreadLocal test = new UseThreadLocal(); test.StartThreadArray(); } } -
运行截图(系统的ThreadLocal):
-
-
-
测试自定义的ThreadLocal
``` package cn.enjoyedu.concurrent.base.threadlocal; *类说明:演示ThreadLocal的使用 */ public class UseThreadLocal { // static ThreadLocal<String> threadLocal = new ThreadLocal<>(); // static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); static MyThreadLocal<String> threadLocal = new MyThreadLocal<>(); /** * 运行3个线程,每个线程都给他搞一个id,第0号进程的id就是0 */ public void StartThreadArray(){ Thread[] runs = new Thread[3]; for(int i=0;i<runs.length;i++){ runs[i]=new Thread(new TestThread(i)); } for(int i=0;i<runs.length;i++){ runs[i].start(); } } /** *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响; * 将线程的自定义id(0号进程的id就是0)通过set传进去,通过get打印出来 */ public static class TestThread implements Runnable{ int id; public TestThread(int id){ this.id = id; } public void run() { String threadName = Thread.currentThread().getName(); threadLocal.set("线程"+id); // if(id==1) { // threadLocal.set(id);//线程1才会执行 // } System.out.println(threadName+":"+threadLocal.get()); } } public static void main(String[] args){ UseThreadLocal test = new UseThreadLocal(); test.StartThreadArray(); } } ```-
运行截图(测试自定义的ThreadLocal):
-
自定义ThreadLocal的问题?
- 只能放一个数据(将key换成Map就解决了)
- 线程并发:加了锁的
- 内部会产生激烈的竞争:使用ThreadLocal的本意就是降低竞争,让线程都有属于自己的一套副本,用了变量,然后放回去;但是用了HashMap,看起来是解决了这个问题了,但是实际上不是的;这个就相当于将每个线程的变量副本都放到一个柜子里面了;这个时候,线程竞争的就不是变量了,竞争的是这个柜子了;竞争到柜子后,再取出变量,处理好,再放回去;自定义的ThreadLocal会对Map产生激烈的竞争,JDK中不是这样去实现的
-
ThreadLocal的JDK内部实现原理:static ThreadLocal threadLocal = new ThreadLocal<>();
-
set方法
public void set(T value) { //拿到当前线程,并将其作为参数传给getMap()函数 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } } -
getMap(t)
ThreadLocalMap getMap(Thread t) { return t.threadLocals; } -
threadLocals
//ThreadLocalMap是当前线程的成员变量 ThreadLocal.ThreadLocalMap threadLocals = null; -
ThreadLocalMap(ThreadLocal.java):定义了Entry数组(以Thread作为键,Object作为value)
static class ThreadLocalMap { Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; //Entry型的数组 private Entry[] table; -
持有关系:类的定义位置没有任何问题,ThreadLocalMap是静态内部类,不依赖外部类
class Thread{ ThreadLocalMap threadLocals; } class ThreadLocalMap{ Entry[] } class Entry{ ThreadLocal<?>k,Object v; } -
为什么要使用数组?每一个线程中有不同的容器(Entry型的数组,同时具体的threadLocal实例作为独立确定一条Entry)用来存放变量;同时对于每一个线程来说,是可以存放多个threadLcoal的实例的,所以在每个线程独有的ThreadLocalMap里面搞多个Entry数据条(也就是存在Entry[]):所以说,实现了线程的数据隔离;
-
在所有的并发工具类中ThreadLocal的性能最好,这个就没有竞争了
-
当调用set的时候,向Entry中加入一条数据,key就是ThreadLocal,value就是set的参数
-
-
-
CAS:现代CPU中提供的指令
-
概述:比较交换,基于这个实现了很多的原子类(Atomic开头的都是原子类型的变量)
-
重要概念:
-
原子:不可再分的
-
原子操作:要么内部操作都完成,要么内部操作一个都不干
- synchronized包裹的代码块就是,没有插一脚的说法
-
如何实现原子操作:
- synchronized:是比较重,主要是去拿锁,当同步代码块的操作简单,那么就引入了轻量级原子操作(CPU中的CAS,比较交换(CPU决定的原子操作,安全))
-
-
CAS的工作表现:以i++,四个线程为例
-
四个线程拿到资源的初值
-
四个线程在线程内部进行计算(得到i++的结果)
-
此时引入比较交换(CAS)
-
比较:将线程中保存的资源的初值与当前资源的值进行比较(检测在线程计算过程中,这个资源有没有被修改(ABA问题))
-
交换:如果比较相等,那么执行交换操作,线程将自己计算出来的值,拿给这个资源
- 不对的话,再把这个值取出来,再重复之前的操作,相当于一个循环,直到所有的线程都结束
-
-
-
乐观锁与悲观锁
-
悲观锁:认为总有人要改我的东西(synchronized)
-
乐观锁:认为没有人改我的东西(直接拿值出来,先处理了),如果发生了改变,那么就执行CAS操作
-
自旋:死循环,不断在这个上面访问
-
性能:
- 一般来说CAS(原子变量)都是比锁快的,因为线程拿不到锁,进入阻塞(存在上下文切换),一次情况下一次切换会消耗五千到两万个时钟周期;JDK的并发编程发展会向着无锁化发展,包括在synchronized底层也是大量使用了这种自旋的操作
- synchronized在特定的场景下(竞争异常激烈,人为设置的)性能优于原子操作,开发基本遇不到
-
-
CAS的三大问题:不会淘汰锁
-
ABA:在线程1写入内存的时候发生,看程序猿是否介意变量是被改过(加一个版本戳)
- 背景:变量需要从A成为B
- 场景:线程1,执行A--->B,但是在线程1执行期间(这个时候,线程1将A作为参数进行计算B),此时闯入线程2(A--->C--->A),当线程1在写入内存的时候(进行比较交换)发现此时资源为A,跟线程1之前保存的资源初值相同,此时线程1就认为该资源没有被修改,线程1将B放入资源(但是是发生了改变的)
- 就像有杯水,我喝了,然后接满了,杯子主人就认为没有人喝过;
-
开销问题:自旋长期不成功(就考虑加锁)
- 一直在尝试,每次都跑的慢,每次都要比较
-
只能保证一个共享变量的原子操作:CAS一般是针对于计算机内存的具体地址
- 多个线程共享变量,那就用锁了
-
-
-
CAS中的基本类型使用
-
版本戳(解决ABA问题):在安卓开发中基本很少用到原子变量
- AtomicMarkableReference:变量有没有被动过
- AtomicStampedReference:变量被动过几次
-
更新引用类型:(AtomicMarkableReference、AtomicStampedReference、AtomicReference)
-
AtomicReference:解决CAS只能保证一个共享变量的同步问题
- 加入需要同时修改两个变量,想办法将这两个变量放到一个对象中去,将修改这两个变量替换为修改这个对象,改了之后直接用对象去替换原来的对象即可
-
-
整形变量的处理
-
getAndIncrement与incrementAndGet:就是i++与++i的区别(最终会调用到native,看不到具体的实现,在java中的i++会创建新的对象)
public class UseAtomicInt { static AtomicInteger ai = new AtomicInteger(10); public static void main(String[] args) { //i++ ai.getAndIncrement(); System.out.println(ai); //++i ai.incrementAndGet(); System.out.println(ai); } } -
运行截图:
-
compareAndSet:同样执行变量加法,返回值不同(加了的新值,旧值)
public class UseAtomicInt { static AtomicInteger ai = new AtomicInteger(10); public static void main(String[] args) { //新值 ai.addAndGet(24); System.out.println(ai); ai.getAndAdd(24); System.out.println(ai); } } -
运行截图:
-
-