多线程讲解:什么是进程什么是线程--->进程与线程的区别(实际问题该如何选择)--->线程的实现原理-->线程实现方式-->线程的中断-->线程状态转换--->同步异步阻塞非阻塞含义--->线程之间通信(锁、信号量)--->volatile关键字、hapens before原则
线程池讲解顺序:为什么使用线程池--->由图来讲解线程池的框架以及框架中各个接口的作用与含义--->使用线程池(使用其中的方法)以及什么情况下选取哪种线程池--->线程池的状态-->如何中断线程池-->线程池的工作流程---->线程池的内部原理(参数含义以及工作原理(addWorker()、runWorker()源码的大致讲解))-->详细分析各个线程池中的参数-->合理使用线程池
提问上节课学习内容--->查看作业--->提问预习任务--->具体讲解
帮助同学理解多线程
实现方式:实现runable、callable接口 继承hread类
案例:
public class ThreadWay { public static void main(String[] args) { //方式一 继承Thread类 Demo threadDemo01 = new Demo(); threadDemo01.setName("我是自定义的线程1"); threadDemo01.start(); System.out.println(Thread.currentThread().toString()); //方式二 实现runnable接口 Thread t1 = new Thread(new DemoOne()); t1.setName("我是自定义的线程2"); System.out.println(Thread.currentThread().getName()); t1.start(); //方式三 实现callable接口 Callable |
几种实现方式的区别:
// Callable 和 Runnable接口的区别
//(1)Callable规定的方法是call(),而Runnable规定的方法是run().
//(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
//(3)call()方法可抛出异常,而run()方法是不能抛出异常的。
//(4)运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
// 通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。Callable是类似于Runnable的接口,
// 实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
中断一个线程:
(1)调用interrupt()方法使线程终止。使用这个方法能够使线程终止的前提条件是线程处于等待状态。其实是用interrupt中断线程就是修改状态位
Executor是最基础的执行接口;
ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;
AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;
TheadPoolExecutor继承了AbstractExecutorService,是线程池的具体实现;
ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
ScheduledThreadPoolExecutor既继承了TheadPoolExecutor线程池,也实现了ScheduledExecutorService接口,是带"周期执行"功能的线程池;
Executors是线程池的静态工厂,其提供了快捷创建线程池的静态方法。所有的线程池都通过这个工厂类来创建
线程池中常用方法详解:
1. shoutDown与shoutDownNow,以及两者的区别。
shutdown关闭线程池
方法定义:public void shutdown()
(1)线程池的状态变成SHUTDOWN状态,此时不能再往线程池中添加新的任务,否则会抛出RejectedExecutionException异常。
(2)线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
注意这个函数不会等待提交的任务执行完成,要想等待全部任务完成,可以调用:
public boolean awaitTermination(longtimeout, TimeUnit unit)
2.shutdownNow关闭线程池并中断任务
方法定义:public List
(1)线程池的状态立刻变成STOP状态,此时不能再往线程池中添加新的任务。
(2)终止等待执行的线程,并返回它们的列表;
(3)试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
通俗点讲:举个工人吃包子的例子,一个厂的工人(Workers)正在吃包子(可以理解为任务),假如接到shutdown的命令,那么这个厂的工人们则会把手头上的包子给吃完,没有拿到手里的笼子里面的包子则不能吃!而如果接到shutdownNow的命令以后呢,这些工人们立刻停止吃包子,会把手头上没吃完的包子放下,更别提笼子里的包子了。
3.cancel()停止一个线程的执行
如上面所介绍的,传入true会中断线程停止任务,传入false则会让线程正常执行至完成,刚开始我难以理解传入false的作用,既然不会中断线程,那么这个cancel方法不就没有意义了吗?后来查阅了许多资料,在stackoverflow上找到了一个比较好的解释,终于恍然大悟。
简单来说,传入false参数只能取消还没有开始的任务,若任务已经开始了,就任由其运行下去。
当创建了Future实例,任务可能有以下三种状态:
等待状态。此时调用cancel()方法不管传入true还是false都会标记为取消,任务依然保存在任务队列中,但当轮到此任务运行时会直接跳过。
完成状态。此时cancel()不会起任何作用,因为任务已经完成了。
运行中。此时传入true会中断正在执行的任务,传入false则不会中断。
总结:
Future.cancel(true)适用于:
a. 长时间处于运行的任务,并且能够处理interruption
Future.cancel(false)适用于:
a. 未能处理interruption的任务
b. 不清楚任务是否支持取消
c. 需要等待已经开始的任务执行完成
线程如何提交,这两种方式的区别:
execute只能接受Runnable类型的任务
submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null
(2)返回值
由Callable和Runnable的区别可知,execute没有返回值,submit有返回值,所以需要返回值的时候必须使用submit。execute只能接受Runnable类型的任务 submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。 submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线 程一段时间后立即返回,这时候有可能任务没有执行完。
如何获得返回值:只有实现callable接口才能获得返回值。通过Future实现。演示。
(1) newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:
阻塞队列:SynchronousQueue
SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。 FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
任务传递示意图:
任务执行步骤示意图:
(2) newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:
阻塞队列: SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同,这里就不赘述了。
(3) newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下的修改。(1)使用DelayQueue作为任务队列。(2)获取任务的方式不同(后文会说明)。(3)执行周期任务后,增加了额外的处理(后文会说明)。
阻塞队列:
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。
注:此线程池讲解的时候要注重他和别的线程池在是线上的区别以及它实现周期性的,实现周期性的这几个方法有什么区别。
任务传递示意图:
任务执行步骤:
1)线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
2)线程1执行这个ScheduledFutureTask。
3)线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。
4)线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
获取任务的方式:
1)获取Lock。
2)获取周期任务。
·如果PriorityQueue为空,当前线程到Condition中等待;否则执行下面的2.2。
·如果PriorityQueue的头元素的time时间比当前时间大,到Condition中等待到time时间;否则执行下面的2.3。
·获取PriorityQueue的头元素(2.3.1);如果PriorityQueue不为空,则唤醒在Condition中等待的所有线程(2.3.2)。
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
阻塞队列:LinkedBlockingQueue
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。
线程池的实现方式:
ExecutorService executor = Executors.newCachedThreadPool(); ExecutorService executorSingle = Executors.newSingleThreadExecutor(); ExecutorService executorFixed = Executors.newFixedThreadPool(2); ExecutorService executorScheduled = Executors.newScheduledThreadPool(5); ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); |
ThreadPoolExecutor类讲解:
参数:
private final BlockingQueue |
线程池详细讲解Executors类:参数每种线程池的创建方法
饱和策略:
1.Abort策略:默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。在之上的代码中已写。程序抛出了RejectedExecutionException,并且一共运行了8个任务(线程池开始能运行3个任务,工作队列中存储5个队列)。当工作队列满了的时候,直接抛出了异常,而且JVM一直不退出。我们可以看到执行任务的线程全是线程池中的线程。
2.CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。所有的任务都被运行,且有2(10 - 3 -5)个任务是在main线程中执行成功的,8个任务在线程池中的线程执行的。也有可能main线程只执行一个,因为可能出现最后一个加入线程池的时候,第一个已经执行完毕。
3.Discard策略:新提交的任务被抛弃。通过上面的结果可以显示:没有异常抛出,后面提交的2个新任务被抛弃,只处理了前8(3+5)个任务,JVM退出.
4.DiscardOldest策略:队列的是“队头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)
阻塞队列:存放任务的队列。
线程池中常用方法详解:
shutdown可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
(1)多线程有几种实现方式
我会选择讲解的第一个集合是ArrayList,再讲这个集合之前我会想把这个集合实现的接口继承的类所具有的特点讲解一遍。
(2)然后讲解具体的方法:
如何使用——>构造函数——>常用方法讲解实现方法讲解——>如何遍历(迭代器)
(3)功能相似集合特点比较
(4)学以致用:
① 提问:你认为今天学的新集合能够在哪些场景下使用?
② 布置作业:利用所学内容完成相应作业。
先掌握了使用之后,先讲解线程池工作流程-->调度模型-->状态转换流程-->如何合理分配线程池
(1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
(2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步
(3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。
Executor框架的结构Executor框架主要由3大部分组成如下:
(1)任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
(2)任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和 ScheduledThreadPoolExecutor)。
(3)异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。 ·任务的性质:CPU密集型任务、IO密集型任务和混合型任务。 ·任务的优先级:高、中和低。 ·任务的执行时间:长、中和短。 ·任务的依赖性:是否依赖其他系统资源,如数据库连接。 性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。
什么时候该选用哪种线程池。
通过讲一个故事让学生明年什么是同步异步、阻塞非阻塞。通过银行家算法讲解什么是死锁,
生产者消费者模型讲解线程之间通信过程及方式。
几种形式的生产者消费者模型
通过wait和notifyAll实现:
public class ProducerConsumerInJava { public static void main(String args[]) { System.out.println("How to use wait and notify method in Java"); System.out.println("Solving Producer Consumper Problem"); Queue buffer = new LinkedList(); int maxSize = 10; Thread producer = new Producer(buffer, maxSize, "PRODUCER"); Thread consumer = new Consumer(buffer, maxSize, "CONSUMER"); producer.start(); consumer.start(); } } class Producer extends Thread { private Queue queue; private int maxSize; public Producer(Queue queue, int maxSize, String name) { super(name); this.queue = queue; this.maxSize = maxSize; } @Override public void run() { while (true) { synchronized (queue) { while (queue.size() == maxSize) { try { System.out.println("Queue is full, " + "Producer thread waiting for " + "consumer to take something from queue"); queue.wait(); } catch (Exception ex) { ex.printStackTrace(); } } Random random = new Random(); int i = random.nextInt(); System.out.println("Producing one "); queue.add(i); queue.notifyAll(); } } } } class Consumer extends Thread { private Queue queue; private int maxSize; public Consumer(Queue queue, int maxSize, String name) { super(name); this.queue = queue; this.maxSize = maxSize; } @Override public void run() { while (true) { synchronized (queue) { while (queue.isEmpty()) { System.out.println("Queue is empty," + "Consumer thread is waiting" + " for producer thread to put something in queue"); try { queue.wait(); } catch (Exception ex) { ex.printStackTrace(); } } queue.remove(); System.out.println("Consuming one"); queue.notifyAll(); } } } } //1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。 //2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。 //3. 永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。 //4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。 //5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。 |
通过Condition实现:
public class ConProducerAndConsumer { private int queueSize = 10; private PriorityQueue |
通过示例证明加上volatile关键字每次会从主内存中读取变量的值,通过示例证明volatile关键字不能保证计算的原子性。
从现象引出原理:
java 内存模型 在Java中有工作内存和主内存。主内存中的变量如果被线程用到,则线程会保存一份主内存变量的副本拷贝。
volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
缓存一致性协议
(1)要明确volatile不能保证原子性,实现原理,解释什么是原子性。
(2)什么是
打开HashTable和ConurrentHashMap源码讲解java中各种锁的使用,以及各种锁的实现过程。
先演示没有使用锁会是一个怎样的效果---->通过不同的加锁方式演示加上之后会达到怎样的效果
代码示例:
package LockTest.SynLockTest; //首先演示这个方法没有使用锁的时候是什么效果 public class SynchronizedTest { public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } public void method1() { System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2() { System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } } 运行结果: Method 1 start Method 1 execute Method 2 start Method 2 execute Method 2 end Method 1 end |
修饰类
//修饰一个类 - 类锁 //其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象 public void putTest() { synchronized (类名.class) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } } |
修饰普通方法
//修饰一个方法 - 对象锁 /* * 在定义接口方法时不能使用synchronized关键字。 * 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。 * synchronized 关键字不能被继承 。如果子类覆盖了父类的 被 synchronized 关键字修饰的方法,那么子- * 类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。 * */ public synchronized void getTest() { //对方法上锁 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } |
修饰静态方法
//修饰静态方法 - 类锁 public synchronized static void method() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } |
修饰代码块
public void run() { synchronized (this) { //synchronized 修饰代码块 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } } |
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
private byte[] lock = new byte[0]; // 特殊的instance变量 public void method1() { synchronized (lock) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } } |
实现原理:
monitorenter : Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership. 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 monitorexit: The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。 |
局限性:
ReadAndWriteLock
讲解读写锁的特性和作用以及优势,通过synchronized实现读写锁:
public class MyReadAndWriteLock { private int writers = 0; private int readers = 0; private int writerequest = 0; public synchronized void lockRead() throws InterruptedException { while (writers > 0 || writerequest > 0) { wait(); } readers++; } public synchronized void unLockRead() { readers--; notifyAll(); } public synchronized void lockWrite() throws InterruptedException { writerequest++; while (readers > 0 || writers > 0) { wait(); } writers++; writerequest--; } public synchronized void unLockWrite() { writers--; notifyAll(); } } |
读写锁的使用:(将这块代码改成用synchronized实现用自己实现的读写锁实现然后分别比较速率)
public class JustRead { // ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); MyReadAndWriteLock lock = new MyReadAndWriteLock(); public static void main(String[] args) { final JustRead justRead = new JustRead(); new Thread(new Runnable() { @Override public void run() { justRead.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { justRead.get(Thread.currentThread()); } }).start(); } public void get(Thread thread) { //lock.readLock().lock(); try { lock.lockRead(); } catch (InterruptedException e) { e.printStackTrace(); } try { System.out.println(thread.getName() + " start time:" + System.currentTimeMillis()); for (int i = 0; i < 5; i++) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + ":正在进行读操作……"); } System.out.println(thread.getName() + ":读操作完毕!"); System.out.println(thread.getName() + " end time:" + System.currentTimeMillis()); } finally { //lock.readLock().unlock(); lock.unLockRead(); } } } |
ReentrantLock
代码实现:
public class ReentrantLocakTest implements Runnable { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); @Override public void run() { testMethod(); } public void testMethod() { lock.lock(); try { System.out.println("开始wait"); //condition.await(); for (int i = 0; i < 5; i++) { System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1))); } } finally { lock.unlock(); } } public void signal() { try { lock.lock(); condition.signal(); } finally { lock.unlock(); } } // public void testMethod() { // lock.lock(); // for (int i = 0; i < 5; i++) { // System.out.println("ThreadName=" + Thread.currentThread().getName() // + (" " + (i + 1))); // } // lock.unlock(); // } public static void main(String[] args) { ReentrantLocakTest test = new ReentrantLocakTest(); //ReentrantLocakTest test1 = new ReentrantLocakTest(); Thread t1 = new Thread(test,"A"); Thread t2 = new Thread(test,"B"); t1.start(); t2.start(); } } |
Condition的使用:
public class ReenCondition { public static void main(String[] args) { final ReentrantLock reentrantLock = new ReentrantLock(); final Condition condition = reentrantLock.newCondition(); Thread thread = new Thread((Runnable) () -> { try { reentrantLock.lock(); System.out.println("我要等一个新信号"+reentrantLock.getClass()); condition.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("拿到一个信号!!"+reentrantLock.getClass()); reentrantLock.unlock(); }, "waitThread1"); thread.start(); Thread thread1 = new Thread((Runnable) () -> { reentrantLock.lock(); System.out.println("我拿到锁了"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } condition.signalAll(); System.out.println("我发了一个信号!!"); reentrantLock.unlock(); }, "signalThread"); thread1.start(); } } |
主要相同点: Lock 能完成 synchronized 所实现的所有功能
主要不同点: Lock 有比 synchronized 更精确的线程语义和更好的性能。 synchronized 会自
动释放锁,而 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。 Lock 还有更
强大的功能,例如,它的 tryLock 方法可以非阻塞方式去拿锁。