Java多线程3.0

多线程3.0

简单复习下多线程

线程启动方式

线程的启动当时Thread、Runnable,Runnable灵活解偶,能多继承
Callable支持返回类型的启动线程,用法类似于Runnable,简单如下:

class ThreadCallable implements Callable<T> {}
FutureTask<T> result = new FutureTask(new ThreadCallable());	//返回结果
new Thread(new Result()).start();
T value = result.get();			//阻塞方法

线程池

线程池ThreadPoolExecutor处理策略以及原理,等候队列是基于BlockingQueue队列存储等候任务队列,有ArrayBlockingueue、LinkedBlockingQueue、PriorityBlockingQueue线程安全,而且ArrayBlockingQueue支持公平锁和非公平锁,LinkedBlockingQueue内部是一个队列,分别有两把入队和出队两把锁,提高队列效率;PriorityBlockingQueue入队没有锁,出队是公平锁;对于无界的Queue都要注意生产者速率不要超过消费者,消耗完堆内存空间

java定义好的四个线程池:CachedThreadPool、FixedThreadPool、SchedualThreadPool和SignledThreadPool

ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new TestRunnable());

关闭线程池和线程

关闭线程

  1. 设置共享变量,关闭线程时修改共享变量为结束状态
  2. 线程阻塞状态时不能第一种无法立即结束,可以在此基础上调用Thread.interrupt()中断阻塞关闭

关闭线程池
线程池提供了2个关闭线程池的方法:shutdown和shutDownNow
shutdown 关闭后线程池拒绝接收任务,会等待正在执行的任务和队列里面的任务执行完成
shudownNow 关闭后拒绝接受新任务,等待队列的任务会清空,正在执行的任务也会使用interrupt产生中断迫使其执行退出

无论是线程还是线程池需要注意Interrupt中断只会在线程阻塞状态下触发产生中断异常,哪怕阻塞方法后调用也会产生中断,所以当我们中断后最好调用isInterrupt()获取是否触发中断,进而在决定处理原则

线程同步

线程同步之前首先要了解java内存模型:PC计数器、堆、栈、方法区;而我们在线程中进行对写操作,主要分为工作内存和主内存,主内存存放的是变量的真实值,工作内存是主内存的拷贝副本,每次进行读写操作时,都是先从主内存拷贝到工作内存,修改后再同步到主内存,但是这个过程不是立即马上发生的;当多个线程访问同一个内存资源时,就可能导致读写不一致的问题,这个时候就需要线程同步,保证同一时刻只有一个线程访问资源;

主要通过synchronized和ReentrantLock来锁定资源进行访问,除此之外volatile也可以实时更新或写入,还有一些院子操作的类AutomicInt等;以上几个关键字保证了对资源的:原子性、可见性、有序性

synchronized

synchronized是JVM层实现的,拿到的是对象的monitor监视器,拿到后➕1,退出同步块➖1,使用锁后自动释放锁,synchronize的锁机制是悲观锁、可冲入锁、非公平锁、偏向锁、轻量级锁、重量级锁;当程序发生异常时,synchronized会释放锁,不会造成死锁现象

jdk1.6后引入了偏向锁、轻量级锁、重量级锁的概念:

  1. 一段同步代码,总是有一个线程持有锁访问同步资源,线程把同步对象的偏向锁头id写入当前线程ID;下次线程再次同步时,只需要对比这个ID是否一致即可进入同步代码
  2. 当再次来了一个线程后,它发现同步对象里面偏向锁已经有一个ID了,并且和自己不一样;然后它回去查看那个线程是否存活,死亡则继续走偏向锁逻辑;如果存活并且占用着偏向锁,这个时候就升级为轻量级锁,利用CAS自旋去获取锁;轻量级锁认可竞争的情况,但是很轻,稍微自旋一下就可以了
  3. 当自旋次数超过一定次数了,或者又来了一个线程进入,则升级为重量级锁,线程进入阻塞状态

为什么要升级?
因为早期的synchronized锁是悲观锁,线程在处理同步代码时总认为有竞争的情况,都会去加锁;而且线程的阻塞切换到唤醒状态都是系统来完成的,涉及到用户态和核心态的转换,这是需要消耗系统资源的,有时候这些状态的切换甚至比执行用户代码还长;所以作出了此升级

ReentrantLock

ReentrantLock是语言层面实现的,java实现需要自己手动获取和释放锁,基于AQS和CAS原理实现的,lock在获取锁时支持获取不到可以立即返回(trylock);锁是悲观锁、可重入锁、支持公平/非公平锁;ReetrantWriteReadLock是内部有两把锁,读锁是共享锁,写锁是独享锁

AQS - AbstractQueuedSynchronized同步队列管理器
AQS内部维护了一个FIFO的双向队列,队首线程占有锁,队尾添加线程;当我们的线程访问同步资源时,如果资源没有被其他线程占用,当前线程直接获取锁并执行;反之,如果被其他线程占用,则当前线程进入阻塞状态并会将当前线程封装成Node节点添加到队尾;对于整个线程队列的管理采用CAS机制管理,其中int类型变量state表示获取锁的状态,每获取一次锁state加1,释放一次减一
Lock对于同步资源的管理方式有两种:

  • 独享方式 : 同一时间只能有一个线程访问同步资源,如ReetrantLock,又可分为公平锁和非公平锁
  • 共享方式 :同一时间可以有多个线程访问同步资源,如CutDownLatch、Semaphore、CyclicBarrier
    ReetrantReadWriteLock兼上面两种操作,内部的读锁是共享锁,写锁是独享锁

公平锁和非公平锁的区别:

  • 公平锁 获取锁的按照排队的先后顺序获得锁,队首的线程使用锁完后,唤醒它的下一个节点,由他持有锁进行工作,并把它设置为队首
  • 非公平锁 获取锁的时机并不一定由排队的顺序决定,当一个线程进入lock代码块时,首先会CAS进行抢锁,如果没抢到,会在进行一次tryAcquire进行抢锁,没抢到就乖乖排队;非公平锁有可能导致队列中的线程迟迟得不到锁,但是这个利用率高,如果抢到了就不用进行排队

CAS Compare And Swap
所谓CAS即比较交换、比较设置,主要有两个动作,而且这两个动作都是原子性的

  1. 拿到内存中的变量值
  2. while循环将再次从内存中取值变与上一步取的值比较,如果相同就更新变量,反之就继续循环这两步,也就是所谓的自旋

CAS会造成ABA问题,解决办法是加一个版本号,CAS使用场景竞争不是很激烈的情况
例子有:AtomicBoolean、AtomicInteger、AtomicLong

自旋
某些同步代码块比较简单,同步时会引起线程阻塞,CPU进行线程切换,引起资源开销,如果这个操作比较频繁,很有可能CPU切换线程的花销比本身执行代码还多,此时引入自旋,现场不必要进入阻塞状态,自旋等待,减少切换时间;CAS也就是使用这一些特性进行值的修改,这也会有一个缺点,如果修改值时一直被别的线程占用,可能导致长时间处于自旋状态

Object.wait notify notifyAll Thread.join interrrupt yeild
Condition await signal signalAll


Object lock = new Object();
lock.wait()
lock.notify()

Lock lock = new Lock();
此时lock负责锁,
Conditional con = lock.newConditional()
con.await()
con.signal()
con专门负责控制等待/释放;与此同时,我们还可以创建另一个con,在两个线程间进行条件控制


可重入锁、互斥锁、悲观锁、乐观锁、独享锁、共享锁、无锁、偏向锁、轻量级锁、重量级锁、公平锁、非公平锁还有个自旋概念

CountDownLatch计数器

简单解释

一个或者一组线程CountDownLatch.await()等待CountDownLatch计数器为0的时候开始执行;形象比喻有点类似出游人数计数器,统计出游报到人数,来一个,总数减一,人来完了,就可以出发去旅游了

public void Test() throws InterruptedException {
		final CountDownLatch latch = new CountDownLatch(3);
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				latch.countDown();	
			}
		};
		
		for(int i = 0; i < 3; i++){
			new Thread(runnable).start();
		}
		
		latch.await();		//必须等待上面3个thread执行完成后才能执行
	}

适用场景

  1. 可以管理多个线程,任务处理完成,关闭这组线程
  2. 子线程初始化一些子模块,初始化完成后,主线程就可以使用这些模块,防止模块没有被初始化,可用于不同页面App首次进入,等待第三方定位结果,完成后叫定位坐标请求后台服务

CountDownLatch是一次性的,使用过后不可重新使用

CyclicBarrier

简单解释

一组线程相互等待CyclicBarrier.wait(),直到大家都处于统一状态后再继续执行

public void Test() throws InterruptedException {
		final CyclicBarrier barrier = new CyclicBarrier(3);
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				try {
					barrier.await();			//线程都会停在这里,当3个线程都处于这里了,大家在开始一起执行
				} catch (BrokenBarrierException e) {
					e.printStackTrace();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};

		for(int i = 0; i < 3; i++){
			new Thread(runnable).start();
		}
		//重置后CyclicBarrier可以继续使用
		barrier.reset();
	}

cyclicBarrier.await内部原理,调用await后–dowait(),内部有一个count计数变量和Condition(条件)的trip变量,每次进入方法,count–,然后判断是否为0,为0就trip.singleAll释放所有等待线程,不为0就进入for死循环,如果有超时等待就trip.awaitNaos(time)超时后返回,否则就trip.await()等待

使用场景

借用别人举得例子,确实很形象,英雄联盟进入游戏加载时,加载进度到100%也不会马上进入游戏,等待所有玩家都加载100%,大家在进入游戏;提炼一下,cyclicBarrier更多使用在现场等待后还需要继续执行下去的场景

Semaphore

定义资源被同时访问的线程数量,使用前使用semaphore.acquire获取许可,结束后semaphore.release释放许可,如果许可证已经用完则进入等待队列,而且Semaphore还支持公平和非公平锁

acquire从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。

final Semaphore semaphore = new Semaphore(3);
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				try {
					semaphore.acquire();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				semaphore.release();
			}
		};

		for(int i = 0; i < 3; i++){
			new Thread(runnable).start();
		}
posted @ 2019-05-22 21:42  帅气好男人_jack  阅读(5)  评论(0编辑  收藏  举报  来源