进程
程序由指令和数据组成,到哪有些指令需要执行,有些要读写,句必须将指令加载到cpu,数据加载到内存,再指令运行过程中还要用到磁盘,网络等设备.进程就是用来加载指令.管理内存,管理io的
当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程
进程可以视为程序的实例,大部分程序可以同时运行多个实例进程,资源分配的最小单位
线程
通常在一个进程中,可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是cpu调度和执行的单位
java中线程是cpu调度的最小的单位,进程是资源分配的最小单位进程有独立的地址空间,线程没有单独的地址空间
两者对比
- 进程基本上是相互独立的,而线程存在于进程内,是一个进程的子集
- 进程拥有共享的志愿,如内存空间,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信成为ipc
- 不同计算机之间的进程通信需要通过网络并且遵守共同的协议
- 线程通信相对简单,因为他们共享进程的内存
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并发与并行
单核cpu下线程实际还是串行执行的,操作系统中有一个组件叫做任务调度器,将cpu的时间片分给不同的线程使用,只是由于cpu在线程间的切换非常快,人类感觉是同时运行的一般会将这种线程轮流使用cpu的做法称为并发(concurrent)
并行(parallel)同一时间动手做多件事情的能力,再多台处理器上同时处理多个任务
异步调用
从方法调用角度来讲:
- 如果需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
- 注意:同步在多线程中还有另外一层意思是让多个线程步调一致
Thread和Runnable的关系
Thread源码
用Runnable更容易与线程池的高级api配合
用Runnable让任务脱离了Thread继承体系,更灵活
使用futureTask
需要Callable配合
比Runnable多个返回值还能抛出异常
public class Main {public static void main(String[] args) {FutureTask<Integer> test = new FutureTask<>(() -> {System.out.println("test");return 1;});new Thread(test).start();Integer i = null;try {i = test.get();//get是阻塞式的System.out.println(i);} catch (InterruptedException e) {throw new RuntimeException(e);} catch (ExecutionException e) {throw new RuntimeException(e);}}
}
查看进程
taskkill /F /PID 进程id
linux: ps
线程上下文切换
因为一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
-
线程的时间片用完
-
垃圾回收
-
有更高优先级的线程需要运行
-
线程自己调用了sleep,yield,wait,parkmsynchrnized,lock等方法
需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念就是程序计数器,它的作用就是记住下一条jvm指令的执行地址,是线程私有的
- 状态包括程序计数器,虚拟机栈中每个栈帧的信息 ,如局部变量表,操作数栈,返回地址等
线程方法
1.start和run
start是启动线程,接着会调用run方法
如果直接调用run方法,则还是主方法调用不能起到一步作用.没有启动新的线程.
start如果多次调用就会抛出异常;
2.sleep()
-
调用该方法会让当前线程总running进入timedwaiting
-
其他线程可以使用interrput方法打断正在休眠的线程,这时候会抛出interrruptedException
-
睡眠结束后的线程未必会立刻得到执行
-
监理用timeunit的sleep代替thread中sleep获得更好的可读性
TimeUnit.SECONDS.sleep(1);
3.yield
- 会让线程从running进入runnable状态,让后调度其他同优先级的线程,如果没有同优先级的线程,那么不能保证让当前线程暂停的效果
- 具体的实现依赖系统中的任务调度器
程序优先级
- 线程优先会提示调度器优先调度该线程,但他仅仅是一个提示,调度器可以忽略它
- 如果spu较忙,那么优先级高的会分配更多的时间片
join
实现同步,等待线程完成后才继续执行
终止模式
park
interrupt可以打断park状态如果打断完后再执行park就不会生效可以使用interrupted改变标记
主线程和守护线程
只要有一个线程没有结束进程就不会结束
守护线程setDaemon如果没有非守护线程,那么守护线程会强制结束
线程的状态
- 初始状态:只是在语言层面创建了线程对象,还未与操作线程关联
- 可运行状态(就绪状态)指该线程已经被创建,可由cpu调度
- 运行状态获取了cpu时间片运行中的状态
- 当cpu时间片运行完会从运行状态装换到可运行状态会导致县城的上下文切换
- 阻塞状态:如果调用了api如bio读写文件这时线程实际不会用到cpu,会导致线程上下文切换等进入阻塞状态
- 等bio操作完毕,会由操作系统的环形阻塞的线程转换为可运行状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会转换为其他状态
临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码为临界区
竞态条件
多个线程在临界区内执行,由于代码的的执行序列不同导致结果无法预测,称之为发生了竞态条件
synchronized
实际上是用对象锁保证了临界区代码的原子性,临界区内的代码是不可分割的,不会被线程切换打断
此次介绍使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
//语法
synchronized(obj){//临界区
}
方法上的 synchronized
class Test{public synchronized void test() {}
}
等价于
class Test{public void test() {//表示调用的对象多个对象的话就不会互斥synchronized(this) {}}
}
//静态方法class Test{public synchronized static void test() {}
}
等价于
class Test{public static void test() {synchronized(Test.class) { // 锁住类对象 多个对象调用的话也会锁着,调用静态方法相当于类直接调用}}
}
sleep不是释放锁
monitor
1java对象头
被翻译成监视器,每个java对象都可以关联一个monitor对象,如果使用synchronized给对象上锁后,该对象头的Mark Word中就被设置指向monitor对象的指针
onwer锁的主人, entryList阻塞队列将请求的进入加入并进入阻塞状态最开始为null waitSet如果发生异常会释放锁
轻量级锁
使用场景:如果一个对象有虽然有多个对象访问,但多线程访问的时间是错开的(也就是没有竞争)那么可以使用轻量级锁来实现
轻量级锁对使用者是透明的,语法还是synchronized
锁膨胀
如果在尝试加轻量级锁的过程中cas操作无法成功,这种情况局势有其他对象对此对象加上了轻量级锁,这时需要所膨胀,将轻量级锁变为重量级锁
申请monitor锁,让object指向重量级锁,将onwer设置为null唤醒entryList中 的blocked线程
自旋优化
竞争的过程中,会先循环请求如果自旋成功,可以避免线程阻塞
偏向锁
只有第一次使用时修改markword,之后发现这个线程id是自己的就表示没有竞争,不用重新cas如果以后没有竞争,则所有者就是这个线程
java6之后默认是偏向锁,默认是延迟的如果想避免延迟可以调jvm参数标记可以偏向,
撤销偏向锁
1.调用hashcode的时候会撤销偏向锁
2.当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量锁
批量重偏向
当撤销偏向锁 阈值超过20后jvm会觉得是不是偏向错了,于是会在给这写对象加锁时
wait/notify
- onwer线程发现条件不满足时,调用wait方法,即可进入waitSet变为waiting状态
- blocked和wating的线程都会处于阻塞状态,不占用cpu时间片
- bloked想爱你成会在onwer想爱你成释放锁时会唤醒
- waiting想爱你成会在onwer想爱你成调用botify或notify时幻想,但唤醒后并不意味着立即获得锁仍需要继续竞争
notify 叫醒object上的waitset的一个线程
botifyAll表示唤醒所有waitSet里面的线程
sleep(long n) 和wait(long n)
- sleep是thread方法,而wait是object方法
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
- sleep再睡眠的同时,不会释放对象 ,而wait会释放资源
join原理
等待另一个线程的结束
//无参的join
public final synchronized void join(long millis)throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}//如果等于0就是一直等待if (millis == 0) {while (isAlive()) {wait(0);}} else {while (isAlive()) {//等待时间long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}}}
park() & unpark
lockSupport.park() //暂停当前线程
lockSupport.unpark(t1) //恢复线程的运行先执行unpark也会结束后面park的执行
线程的状态切换
1.待运行,就绪状态,运行状态,阻塞状态,死亡状态
running->waiting
- 调用wait()方法时调用join等待谁的调用谁的或者调用interrupt会转换回来
- 调用notify(),notifyAll(),interrupt()时
- 竞争成功 t waiting->runnable
- 失败就到blocked
死锁
一个线程需要同时获取多个锁,可能会产生死锁
t1拥有了a对象的锁,想获取b对象的锁
t2拥有了b对象的锁,想获取a的锁这样就能可能发生死锁
都不释放锁就造成了死锁
活锁
出现在两个线程互相改变对方的结束条件,最后谁也无法结束,交错指令
饥饿
一个线程由于优先级太低,一直获取不到cpu调度执行,也不能够结束,饥饿的情况不易演示
解决方案,按相同的顺序加锁,但是可能会出现饥饿问题
reentranlock
相对于synchronized具有的特点
- 可中断
- 可以设置超时时长
- 可以设置公平锁
- 支持多个条件变量
可重入:是指一个相乘如果首次获得了这把锁,那么他是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重复锁,南无第二次获取时,自己也会被锁住
可见性
如果线程多次访问主内存的值,jit编译器会将这个值缓存到自己的高数缓存区间,这样会出现问题,改了主存的但是读取的时候还是从高速缓冲区获取
解决:给变量加上valatile 这样只能从主存中获取变量的值.保持可见性,可以修饰成员变量和静态成员变量
使用synchronized也能使用保证可见性
可见性vs原子性
valocicate不能保证原子性,synchronized既可以保证原子性和可见性
有序性
jvm不影响正确性的前提下,会调整语句的顺序,加入volicate会禁止重排序
实现原理:内存屏障,写屏障:保证在该屏障之前的,对共享变量的改动,都同步到主存当中
读屏障,保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
Atomic
原子整数
1.AtomicBoolean,Integer,Long
AtomicInteger i = new AtomicInteger(0);int i1 = i.incrementAndGet(); //自增并且获取i++i.getAndDecrement(); //++ii.getAndDecrement(); //i--i.decrementAndGet(); //-ii.updateAndGet(x ->x*10); //使用lambda表达式 *10
原子引用
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
aba问题:主线程只能判断出共享变量的值是否与最初的值是否想等,不能感知a到b再到a这种的情况,这种情况下可以加一个版本号使用AtomicStampedReference
原子数组
享元模式
体现:使用valueOf()时如果在128-127之间会重用这个对象,超出了才会重新创建long对象
数据库连接池,线程池等
ThreadPoolExecutor
1.线程池状态
使用ThreadPoolExecutor来表示线程状态,低29为表示线程数量
构造方法
- corePoolSize核心线程数
- maximumPoolSizex最大线程数目(核心线程数+救急线程数)
- keepAliveTime生存时间,针对救急线程
- unit时间单位,针对救急线程
- workQueue阻塞队列
- threadFactory线程工厂-可以为线程创建时起个好名字
- handler拒绝策略
- 核心线程数:
- 核心线程数是线程池中始终保持存活的线程数量,即使这些线程处于空闲状态也会被保留。
- 当任务到达时,线程池会优先使用核心线程来处理任务,只有当核心线程都在忙碌时,才会创建新的线程。
- 核心线程数可以通过线程池的
corePoolSize
参数进行设置。
- 最大线程数:
- 最大线程数是线程池中允许存在的最大线程数量,包括核心线程和非核心线程。
- 当任务队列已满且当前线程数小于最大线程数时,线程池会创建新的线程来处理任务。
- 最大线程数可以通过线程池的
maximumPoolSize
参数进行设置。
救急线程和核心线程的区别
救急线程有生存时间,核心线程生存时间
Executors
线程池工具类
//创建线程池,参数为大小
java ExecutorService executorService = Executors.newFixedThreadPool(int n) //创建核心线程和最大线程数相等,没有救急线程,传入的阻塞队列是无界的,可以放置任意数量的任务,因此无序超时时间,用于不是很繁忙的业务 ExecutorService executorService = Executors.newCachedThreadPool(); //核心线程数为0最大线程数为integer_maxvalue 也就是创建的全部都是救急线程,过时时间为60s基本可以无限创建 //队列采用了synchronousQueue实现特点是,他没有容量,没有线程来取是放不进去的,适合线程数目大,并且自行次数小 ExecutorService executorService = Executors.newSingleThreadExecutor(); //核心和固定线程都为1,没有救急线程,会放无界队列派对,任务执行完毕,这唯一的线程也不会被释放,超时是60s //区别:如何直接创建任务执行失败而终止没有任何不就措施,而线程池还会继续工作,创建新的线程,保证线程池的正常工作
提交任务
//执行方法
void execute(Runnable command);
//提交方法
<T> Furure<T> submit(callable task);
//提交提盒中的所有task
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;//带超时时间<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException;
//提交所有只要一个执行完成就取消其他线程<T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException;//添加超时时间<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
其他方法
isshutdown();不在running的状态就返回true
isTerminated() //检查县城是否treminated
showdown();//关闭线程
shutdownNow();//立即关闭所有线程
任务调度线程池
fork/join
1.概念
是jdk1.7加入的新线程池实现,它体现的是一种分治思想,使用与能够进行任务拆分的cpu密集型运算
任务拆分:是将一个大任务,拆分为算法上相同的小任务,直到不能才分可以求解为止
使用
ublic class Main {public static void main(String[] args) throws IOException {ForkJoinPool forkJoinPool = new ForkJoinPool(4);System.out.println(forkJoinPool.invoke(new myTask(5)));}}
class myTask extends RecursiveTask<Integer>{private int n;public myTask(int n) {this.n = n;}//计算方法@Overrideprotected Integer compute() {if(n==1){return 1;}myTask t1 = new myTask(n - 1);t1.fork();//让一个线程去执行任务int res = n+ t1.join(); //获取结果return res;}
}
AQS
第一个阻塞锁和相关的同步器工具的框架
特点:
用state属性来表示资源的状态(分占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getstate 获取状态
- setstate设置状态
- compareandsetstate乐观锁设置state设置状态
- 提供了FIFO的等待队列,类似Monitor中的EntrySet
获取锁
if(!tryAcquire(arg)){//使用park和unpark来实现 可以选择阻塞当前线程
}
释放锁
//如果释放锁成功
if(tryRelease(arg)){//让阻塞线程恢复运行}
reentrantLock原理
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B;否则,什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
AQS使用一个FIFO的队列表示排队等待锁的线程。队列头节点,称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,有以下两种情况。
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取。
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
可重入锁。可重入锁是指:同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
可中断锁。可中断锁是指:线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
公平锁与非公平锁。公平锁是指:多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序。对于非公平锁,则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
ReentrantLock提供了两个构造器,分别是:
//true的话就是公平锁
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
//默认构造方式,使用的是不公平锁
public ReentrantLock() {sync = new NonfairSync();}//lock方法
public void lock() {sync.lock();
}
lock原理
final void lock() {//如果是0就表明没锁,把他设置为1if (compareAndSetState(0, 1))//将onwer设置为当前的线程setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
首先,用一个CAS操作,判断state是否是0(0表示当前锁未被占用),如果是0,则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能去排队。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
若当前有3个线程去竞争锁,假设线程A的CAS操作成功了,拿到锁就返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire方法。
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
第1步:尝试获取锁。如果尝试获取锁成功,方法直接返回。看下tryAcquire方法。
tryAcquire(arg)
final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//获取state变量值int c = getState();if (c == 0) { //没有线程占用锁if (compareAndSetState(0, acquires)) {//占用锁成功,设置独占线程为当前线程setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 更新state值为新的重入次数setState(nextc);return true;}//获取锁失败return false;
}
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用;若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
第2步:入队。上文中提到,由于线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁一直不释放,那么B和C就会被挂起。
先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)方法:
/*** 将新节点和当前线程关联并且入队列* @param mode 独占/共享* @return 新节点*/
private Node addWaiter(Node mode) {//初始化节点,设置关联线程和模式(独占 or 共享)Node node = new Node(Thread.currentThread(), mode);// 获取尾节点引用Node pred = tail;// 尾节点不为空,说明队列已经初始化过if (pred != null) {node.prev = pred;// 设置新节点为尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点enq(node);return node;
}
这里使用了经典的“自旋+CAS”组合,来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else代码块里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败,还需要第三轮循环。最终,所有线程都可以成功入队。
第3步:挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败,则会被挂起。
/*** 已经入队的线程尝试获取锁*/
final boolean acquireQueued(final Node node, int arg) {boolean failed = true; //标记是否成功获取锁try {boolean interrupted = false; //标记线程是否被中断过for (;;) {final Node p = node.predecessor(); //获取前驱节点//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁if (p == head && tryAcquire(arg)) {setHead(node); // 获取成功,将当前节点设置为head节点p.next = null; // 原head节点出队,在某个时间点被GC回收failed = false; //获取成功return interrupted; //返回是否被中断过}// 判断获取失败后是否可以挂起.若可以,则挂起if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())// 线程若被中断,设置interrupted为trueinterrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
code里的注释已经很清晰地说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么B和C的tryAcquire操作都会失败,因此会走到第2个if语句中。我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了什
/*** 判断当前线程获取锁失败之后是否需要挂起*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//前驱节点的状态int ws = pred.waitStatus;if (ws == Node.SIGNAL)// 前驱节点状态为signal,返回truereturn true;// 前驱节点状态为CANCELLEDif (ws > 0) {// 从队尾向前寻找第一个状态不为CANCELLED的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将前驱节点的状态设置为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}/*** 挂起当前线程,返回线程中断状态并重置*/
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}
线程入队后能够挂起的前提是:它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以,shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,有以下两种情况:
如果符合,则返回true,然后调用parkAndCheckInterrupt,将自己挂起;
如果不符合,再看前驱节点是否>0(CANCELLED),若是,则向前遍历直到找到第一个符合要求的前驱;若不是,则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下,看有没有机会去尝试竞争锁。
unlock原理
public void unlock() {sync.release(1);
}public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为:先尝试释放锁,如果释放成功,那么查看头结点的状态是否为SIGNAL,若是SIGNAL,则唤醒头结点的下个节点关联的线程;如果释放失败,那么返回false表示解锁失败。这里可以看到,每次都只唤起头结点的下一个节点关联的线程。
/*** 释放当前线程占用的锁* @param releases* @return 是否释放成功*/
protected final boolean tryRelease(int releases) {// 计算释放后state值int c = getState() - releases;// 如果不是当前线程占用锁,那么抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {// 锁被重入次数为0,表示释放成功free = true;// 清空独占线程setExclusiveOwnerThread(null);}// 更新state值setState(c);return free;
}
读锁和写锁
读读可以并发,读写不行
ReentranReadWriteLock实现了读读可以并发,读写不能并发
- 读锁不支持条件变量
- 重入是升级不支持:及时有杜所的情况下去获取写锁,会导致获取写锁永久等待
- 重入降级可以获取写锁后可以获取读锁
StampedLock
jdk8后在使用读锁写写锁的时候必须要配合[戳]使用
long stamp = lock.readLock();
lock.unlockRead(stamp)
-
不支持锁重入
-
不支持条件变量
semaphore
信号量,用来限制同时访问共享资源的线程上限.
package org.example;import java.awt.desktop.ScreenSleepEvent;
import java.io.PipedWriter;
import java.util.concurrent.Semaphore;class Main{public static void main(String[] args) throws InterruptedException {//最大三个同时访问Semaphore s = new Semaphore(3);for (int i = 0; i < 10; i++) {new Thread(()->{try {//如果超过最大数,就会阻塞s.acquire();try {System.out.println("a");Thread.sleep(2000);}finally {s.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();}}
}
源码:
//acquire方法public Semaphore(int permits) {sync = new NonfairSync(permits);}//不公平同步器
线程安全集合类
线程安全集合类可以分为三大类
- 遗留的线程安全集合如:hashTable,Vector
- 使用Collection修饰的线程安全集合
- Collections.synchronizedList…
同时访问
Semaphore s = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
//如果超过最大数,就会阻塞
s.acquire();
try {
System.out.println(“a”);
Thread.sleep(2000);
}finally {
s.release();
}
} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();}
}
}
源码:```java
//acquire方法public Semaphore(int permits) {sync = new NonfairSync(permits);}//不公平同步器
线程安全集合类
线程安全集合类可以分为三大类
- 遗留的线程安全集合如:hashTable,Vector
- 使用Collection修饰的线程安全集合
- Collections.synchronizedList…
[外链图片转存中…(img-1Oci7q4E-1710663731488)]